Grafana Cloud에서 기준선 없이 알림 규칙 만들기
이전 글 마지막에 알림은 기준선이 쌓인 뒤에 만들겠다고 썼다. 근거 없는 임계값은 오발화만 만든다고 봤기 때문이다. 그런데 막상 착수해 보니 기준선을 기다릴 필요가 없었다. 기다려야 했던 것은 임계값 자체가 아니라 비율로 알림을 걸겠다는 전제였다.
이번에는 규칙을 세 개만 만들었다. 그 과정에서 임계값을 0으로 둔 규칙이 정작 첫 한 건을 놓치고 있다는 것도 코드 리뷰에서 발견했다. 원인은 알림 쿼리가 아니라 Micrometer가 카운터를 등록하는 시점이었다.
환경은 이전과 같다. Java 21, Kotlin 2.2.21, Spring Boot 4.0.6, Micrometer 1.16.5를 쓰고, 메트릭은 EC2 t3.small 한 대에서 Grafana Cloud로 OTLP push한다.
알림은 대시보드와 판단 기준이 다르다
먼저 정한 것은 무엇을 알림으로 만들지가 아니라 어떤 기준으로 걸러낼지였다.
대시보드 패널은 궁금하면 보면 그만이다. 정보가 하나 더 있어서 손해 볼 일이 없다. 알림은 반대다. 울린 뒤 내가 지금 할 행동이 없으면 그 자체가 손해다. 그런 알림이 몇 번 반복되면 진짜 장애 때도 화면을 열지 않게 된다.
그래서 후보를 놓고 “울리면 지금 무엇을 하는가”만 물었다. 이 질문을 통과한 것이 세 개였다.
멘토링에서 받은 조언도 같은 방향이었다. 지표를 잘게 나누는 것보다 AI 서비스가 죽는 것을 가장 강하게 경고해야 한다는 이야기, 그리고 지금 단계에서는 특정 시간 동안 4xx·5xx가 몇 건 이상이면 보내는 정도로 단순하게 만들고 실제로 알림이 오는지부터 확인하라는 이야기였다. 복잡한 규칙은 서비스가 잘 돌아가기 시작한 다음에 필요하고, 그전에 넣으면 알림 폭탄만 된다는 말도 함께 들었다.
10초 창은 만들 수 없었다
조언을 그대로 옮기려다 바로 막혔다. OTLP export 주기가 60초라 원본 데이터 포인트가 분당 하나다. 10초 창에는 샘플이 하나 있거나 아예 없다.
이건 이전 글에서 전송 주기를 60초로 정하며 알고 받아들인 대가였다. 1분 미만의 짧은 스파이크는 대시보드에 나타나지 않고 로그나 Sentry의 몫으로 넘어간다고 적어 뒀는데, 알림에도 똑같이 적용된다는 것까지는 생각하지 않았다.
그래서 조언의 형식이 아니라 취지만 가져왔다. 창은 10분으로 넓히고, 대신 비율 대신 건수를 쓰고, 만들자마자 실제로 울려 보는 것까지 했다.
비율을 버리고 건수를 쓴 이유
원래 계획한 후보는 5xx 오류율, AI 실패율처럼 전부 비율이었다. 지금 트래픽에서는 쓸 수 없는 형태다.
새벽에 요청이 5건 들어와 1건이 실패하면 오류율은 20%다. 대시보드에서는 “요청률 패널과 함께 보라”는 주의 한 줄로 충분하다. 화면을 보는 사람이 옆 패널을 같이 보면 되기 때문이다. 알림은 그 맥락 없이 혼자 도착한다.
건수 기준은 이 문제가 없다. 10분에 5건은 트래픽이 적든 많든 같은 뜻이다.
덤으로 따라온 것이 더 컸다. 기준선을 기다릴 이유가 사라졌다. 비율 임계값을 정하려면 평일과 주말, 캠페인 유무별 분포가 며칠은 쌓여야 한다. “10분에 5건”은 그런 것 없이도 말이 된다. 착수를 미루게 만든 선행 조건이 알고 보니 특정 접근 방식에만 딸린 조건이었다.
규칙 세 개
| 규칙 | 조건 | pending | No Data |
|---|---|---|---|
| 수집 끊김 | 절대 참이 안 됨 | 10m | Alerting |
| AI 실패 급증 | > 5 |
없음 | Normal |
| 환불 실패 | > 0 |
없음 | Normal |
수집 끊김
sum(rate(http_server_requests_milliseconds_count{service_name="manyak-server"}[5m]))
임계 조건은 IS BELOW 0으로 뒀다. 절대 참이 되지 않는 조건이다. 이 규칙은 값을 보지 않고 값이 오는지만 본다. 데이터가 끊기면 No Data가 되고, No Data를 Alerting으로 매핑해 둔 덕에 발화한다.
이 쿼리를 고른 이유는 절대 비지 않는 시계열이기 때문이다. 로드밸런서 헬스체크가 주기적으로 들어와 사용자 트래픽이 0인 새벽에도 바닥값이 깔린다. 실제로 규칙을 만들고 그래프를 보니 0.15 req/s 언저리에서 평평했다. 여기가 비었다는 것은 트래픽이 없다는 뜻이 아니라 서버나 OTLP push가 죽었다는 뜻이다.
서버가 죽은 것과 push만 죽은 것은 구분하지 못한다. 어느 쪽이든 사람이 인스턴스를 봐야 하니 구분할 필요가 없었다.
AI 실패 급증
sum(increase(manyak_ai_call_duration_milliseconds_count{service_name="manyak-server", outcome="failure"}[10m]))
feature별로 쪼개지 않았다. 알림이 답해야 하는 질문은 “AI가 지금 망가졌나” 하나이고, 어느 기능인지는 울린 뒤 대시보드에서 본다. 쪼개면 규칙이 네 개가 되고 같은 장애에 네 번 울린다.
이 메트릭의 outcome은 두 값뿐이다. 공통 호출 경계에서 AI 호출만 감싸므로 크레딧 부족이나 권한 거부가 섞일 여지가 없다. 이전 글에서 고생했던 rejected 분리를 여기서는 신경 쓰지 않아도 됐다.
능동 헬스체크는 따로 두지 않았다. AI 서버가 죽었는데 호출이 없으면 사용자 영향도 없고, 첫 사용자가 실패하는 순간 이 규칙이 잡는다. 프로버를 하나 더 띄워 운영할 이유를 찾지 못했다.
환불 실패
sum(increase(manyak_chat_turn_refund_total{service_name="manyak-server", outcome="failure"}[10m]))
셋 중 성격이 다르다. 이건 장애 신호가 아니라 정산 정확성 신호다. 채팅 턴은 크레딧을 먼저 차감하고 실패하면 되돌리는데, 되돌리지 못했다는 것은 사용자가 실패한 턴에 과금된 채 남았다는 뜻이다.
임계값 0인 알림은 보통 위험하지만 여기서는 안전하다고 봤다. 정상 운영에서 이 값은 0이고, 0이 아닌 순간이 곧 조사 대상이다.
이 판단이 나중에 뒤집힌다. 뒤에서 다시 다룬다.
뺀 것
- 5xx 오류율 — AI 실패가 502로 나가므로 두 번째 규칙과 같은 사건을 두 번 울린다. 남는 것은 AI 밖의 500인데 드물다.
- AI 호출 p95 — 외부 의존이라 새벽에 울려도 할 행동이 없다. 느려진 사실은 대시보드에서 본다.
- 스토리 완성 실패율 — 실패 원인 대부분이 AI라 두 번째 규칙에 흡수된다.
- active series 한도 — 하루 단위로 움직이는 값이라 알림보다 주기적 확인이 맞다. 대시보드에 임계선이 이미 있다.
- 야간 mute — 규칙이 세 개뿐이라 야간 발화 자체가 드물다. 조용히 하고 싶으면 Slack 채널 알림 설정이 이미 그 기능이다.
AI 응답의 JSON 형식 오류로 502가 상시 조금씩 발생하는데, 개별 건은 재시도로 풀리므로 알림 대상이 아니다. 두 번째 규칙의 임계값 5가 “가끔 나는 일회성”과 “지금 계속 깨지는 중”을 가르는 선 역할을 한다.
No Data를 한 규칙에만 몰아준 이유
이전 글에서 로컬 실습용 알림을 Pause했던 이유가 이것이었다. 서버를 끄면 데이터가 끊기고 No data 처리 설정에 따라 이상으로 판단돼 메일이 왔다.
운영에서 다시 마주한 문제는 같지만 답이 달랐다. 서버가 내려가면 모든 규칙이 동시에 No Data가 된다. 전부 Alerting으로 두면 장애 하나에 알림이 세 개 온다. 전부 무시하면 서버가 죽어도 아무도 모른다.
그래서 No Data를 발화 신호로 쓰는 규칙을 하나만 뒀다.
| 수집 끊김 | AI 실패·환불 실패 | |
|---|---|---|
| 데이터 없음의 뜻 | 서버가 죽었다 | 그 사건이 안 일어났다 |
| No Data 동작 | Alerting | Normal |
두 번째와 세 번째 규칙의 쿼리는 실패가 한 건도 없으면 분자 시계열이 아예 없어 increase()가 빈 벡터를 낸다. 무장애 상태가 곧 No Data다. 여기서 No Data를 발화시키면 평온한 새벽마다 울린다.
이전 글에서 패널에 쓴 or vector(0)을 규칙에는 쓰지 않았다. 0을 채워 넣으면 조건이 정상적으로 거짓이 되어 결과는 같은데, 대신 수집이 끊겨도 규칙이 조용해진다. 패널은 사람이 보면서 “이게 0인가 끊긴 건가”를 구분해야 하니 채우는 게 맞고, 알림은 그 공백을 첫 번째 규칙이 덮으니 채우지 않는 게 맞다. 같은 상황에 대한 처방이 도구에 따라 갈렸다.
pending은 배포를 삼키기 위한 것
첫 번째 규칙의 pending을 10분으로 뒀다. 배포 때문이다.
컨테이너를 교체하면 push가 몇 분 끊긴다. pending이 없으면 릴리스마다 알림이 울리고, 그러면 곧 아무도 보지 않는다. 대가는 진짜 다운을 10분 늦게 아는 것인데, 자동 복구 수단이 없는 단일 인스턴스라 어차피 사람이 손으로 붙어야 하고 10분 차이가 대응을 바꾸지 않는다고 봤다.
다만 pending이 No Data 전이에도 적용되는지가 자명하지 않았다. 임계값을 넘긴 상태가 지속되는 것과, 데이터가 아예 없는 상태가 지속되는 것은 구현상 다르게 처리될 수 있다. 적용되지 않는다면 pending 10분이 배포를 전혀 막아 주지 못하고 다른 수단이 필요했다.
그래서 재 봤다. pending을 1분으로 낮추고 쿼리의 service_name을 존재하지 않는 값으로 바꿔 강제로 No Data를 만들었더니, 저장 시각으로부터 1분 30초 뒤에 알림이 도착했다. pending 1분에 기본 group wait 30초를 더한 값이다. 적용되지 않았다면 30초 안에 왔을 것이다.
발화 테스트가 규칙마다 다르다
만든 알림의 유일한 실패 모드는 조용히 안 오는 것이고, 그건 진짜 장애 때 알게 된다. 그래서 세 규칙 모두 실제로 울려 봤는데 방법이 갈렸다.
가장 싼 방법은 임계값을 뒤집는 것이다. > 5를 > 0으로 바꿔 저장하고 알림이 오는지 보면 된다. 그런데 두 번째 규칙에는 이게 통하지 않았다. 실패가 0건이면 increase(...{outcome="failure"}[10m])가 빈 벡터라, 임계값을 아무리 낮춰도 비교할 값 자체가 없다. No Data로 빠져 Normal이 된다.
그래서 쿼리를 잠깐 통째로 바꿨다. 항상 값이 있는 HTTP 요청률로 갈아 끼우고 > 0을 걸었더니 다음 평가에서 바로 발화했다.
첫 번째 규칙은 조건이 아니라 No Data 경로로 발화하므로 임계값을 건드려도 아무 일이 없다. 앞서 쓴 대로 service_name을 없는 값으로 바꾸는 방법을 썼다.
Slack에 도착한 메시지에는 값과 라벨, 애노테이션, 규칙 링크와 무음 처리 링크가 함께 왔다. No Data 경로로 발화한 쪽에는 grafana_state_reason = NoData가 붙어 있어 어느 경로로 울린 것인지 구분됐다.
한 가지 헷갈린 것이 있었다. 원복한 뒤 해제 알림이 바로 오지 않았다. 발화는 group wait 30초 뒤 나가지만 해제는 group interval 5분 tick에 묶인다. 두 규칙 모두 발화에서 해제까지 정확히 5분이었다. 원복 직후 해제가 안 온다고 실패로 판단하면 안 된다.
발화 직전 규칙 상태 뱃지가 Inhibited로 표시되는 구간도 있었는데, 알림은 정상적으로 나갔다. 뱃지만 보고 억제됐다고 판단했다가 한 번 헛짚었다.
알림 채널을 나눈 이유
Slack incoming webhook을 새로 발급했다. 기존에 사용자 피드백 알림용 webhook이 이미 있어서 재사용을 검토했지만 나눴다.
Slack incoming webhook은 생성 시점에 채널이 고정된다. 재사용하면 같은 채널에 섞이고, 피드백이라는 좋은 소식 사이에 장애라는 나쁜 소식이 묻힌다. 알림이 세 개뿐이라 채널이 조용한 편인데, 조용한 것이 오히려 장점이다.
채널 이름은 팀에 이미 있던 규칙을 따랐다. 봇 알림 채널이 센트리-데일리-리포트, 앰플리튜드-데일리-리포트, 피드백-알림 식으로 되어 있었다. 도구명은 한글로 쓰고 접미사가 성격을 나타내는데, -리포트는 정기 요약이고 -알림은 사건이 생길 때만 오는 푸시였다. Grafana 규칙은 후자라 그라파나-알림으로 만들었다.
기존 센트리 채널과 역할이 겹치지 않는지도 따져 봤다. 센트리는 예외 단위로 쌓아 하루치를 요약하고, 이 규칙은 10분에 몇 건이라는 빈도 판정이다. 상시 발생하는 AI 응답 형식 오류는 센트리에 매번 남지만 여기서는 조용하다. 그게 의도한 동작이다.
규칙을 레포에 두기
Grafana 알림은 provisioning YAML로 내보낼 수 있다. 대시보드 JSON처럼 서버 레포에 두기로 했다.
이전 글에서 대시보드를 레포에 둔 이유는 쿼리가 서버 코드의 메트릭 이름과 라벨에 의존하기 때문이었다. 알림은 그 이유가 더 강하다. 계약이 바뀌면 대시보드는 빈 패널로 눈에 띄지만 알림은 조용히 안 울리는 상태가 된다.
손으로 YAML을 먼저 쓰지 않고 화면에서 만든 뒤 export했다. 규칙 UID와 표현식 모델 보일러플레이트를 사람이 정확히 재현할 이유가 없다.
내보낸 파일에는 contact point 이름만 남고 webhook URL은 들어가지 않아 그대로 커밋해도 비밀값이 새지 않았다. 다만 화면과 필드 이름이 달라 대조표가 필요했다.
conditions:
- evaluator:
params: [0]
type: lt # 화면의 IS BELOW 0
for: 10m # 화면의 Pending period. 없으면 None
noDataState: OK # 화면의 No data → Normal
Normal을 골랐는데 저장되면 OK로 적힌다. 이런 이름 차이는 나중에 파일만 보고 규칙을 재구성할 때 걸린다.
임계값 0인 알림이 첫 한 건을 놓치고 있었다
여기까지 만들고 코드 리뷰를 받았다. 세 번째 규칙에 대한 지적이 하나 왔는데, 읽고 나서 확인해 보니 맞았다.
Micrometer의 Counter는 첫 increment() 시점에 등록된다. 코드가 배포돼 있어도 그 경로가 한 번 호출돼야 시계열이 생긴다. 이전 글에서 이미 겪은 사실이고 문서에도 적어 뒀는데, 그때는 “대시보드에 라인이 안 보인다” 정도의 의미로만 이해했다.
환불 실패가 한 번도 없으면 outcome="failure" 시계열은 존재하지 않는다. 그러다 첫 실패가 나면 이렇게 보인다.
t0 t1 t2 t3 ← 60초 간격 푸시
· · · 1 ← 앞은 시계열 자체가 없다. 첫 샘플이 곧 값 1
increase()는 창 안에 샘플이 두 개 이상 있어야 계산한다. 하나뿐이면 뺄 이전 값이 없어 빈 결과가 된다. 그러면 규칙은 No Data가 되고, noDataState를 Normal로 둔 탓에 조용히 정상으로 넘어간다.
한 스텝 더 가도 울리지 않는다.
t3 t4
1 1 → increase = 0 (값이 안 늘었으니까)
두 번째 실패가 나서 1에서 2로 올라야 비로소 > 0이 된다. 임계값을 0으로 둔 이유가 첫 한 건을 잡기 위해서인데 정확히 그 한 건을 놓치고 있었다. 첫 건은 지연되는 것이 아니라 사라진다.
미터는 프로세스마다 새로 등록되므로 이 사각지대는 재시작할 때마다 다시 열린다. 배포 후 첫 환불 실패는 또 보이지 않는다. 드물게 일어나는 사건이라 실제 발생분의 상당수가 “재시작 후 첫 건”에 해당한다.
고치기 전에 실패하는 테스트로 재현부터 했다. 환불 실패가 없는 컨텍스트에서 failure 카운터를 찾으면 null이 나와 테스트가 깨졌다. 이론이 아니라 실제 동작이라는 것이 이 시점에 확인됐다.
쿼리로 우회하지 않은 이유
먼저 알림 쿼리만 고치는 방법을 검토했다. 서버 배포 없이 오늘 적용된다는 장점이 있었다.
increase()가 비었을 때 카운터 값 자체로 떨어지게 하는 방법이 있다.
sum(increase(m[10m])) or sum(m)
시계열이 갓 생겼을 때는 왼쪽이 비어 오른쪽으로 떨어지니 발화한다. 그런데 다음 평가에서 샘플이 두 개가 되면 increase가 0을 반환하고, 0도 값이므로 왼쪽이 이겨 바로 해제된다. 참인 구간이 평가 한 번뿐이라 타이밍에 기대게 된다.
현재 누적값에서 10분 전 누적값을 빼는 방법도 있다.
sum(last_over_time(m[10m])) - (sum(last_over_time(m[10m] offset 10m)) or vector(0))
첫 실패도 잡고 10분 뒤 해제도 된다. 대신 increase()가 공짜로 해 주던 카운터 리셋 감지를 잃는다. 배포로 카운터가 초기화된 직후 창에서는 결과가 음수가 되어 놓친다.
둘 다 사각지대를 다른 사각지대로 바꾸는 것이었다. 돈이 오간 기록을 확인하는 규칙에서 그 교환은 남는 장사가 아니라고 봤다.
카운터를 0으로 먼저 등록하기
근본 원인은 쿼리가 아니라 등록 시점이므로 그쪽을 고쳤다. Micrometer에는 이런 용도의 인터페이스가 있다.
@Component
class ChatTurnRefundMeters : MeterBinder {
override fun bindTo(registry: MeterRegistry) {
listOf(ChatService.OUTCOME_SUCCESS, ChatService.OUTCOME_FAILURE).forEach { outcome ->
Counter.builder(ChatService.METRIC_CHAT_TURN_REFUND)
.description("채팅 턴 선차감 환불 결과")
.tag("outcome", outcome)
.register(registry) // increment 없이 등록만 → 0에서 시작
}
}
}
register()만 하고 increment()를 하지 않으면 값 0인 미터가 만들어진다. Spring Boot가 MeterBinder 타입 빈을 모든 레지스트리에 자동으로 적용하므로 배선할 것도 없다.
그러면 기동 직후부터 이렇게 나간다.
t0 t1 t2 t3
0 0 0 1 → increase = 1 → 발화. 10분 창 내내 참으로 유지
서비스 클래스의 init 블록에 넣어도 동작은 같다. MeterBinder를 고른 이유는 Spring 컨텍스트 없이 레지스트리 하나만 만들어 검증할 수 있어서였다.
같이 손본 것이 하나 더 있다. 메트릭 이름이 문자열 리터럴로 박혀 있었는데 상수로 뽑고, 사전 등록과 증가 양쪽이 같은 상수를 쓰게 했다. 미터의 신원은 이름과 태그라, 한쪽에 오타가 나면 시계열이 둘로 갈린다. 사전 등록한 쪽은 영원히 0으로 남고 실제 카운트는 원래 버그로 돌아가는데, 그 사실이 화면에는 드러나지 않는다. 컴파일러가 막게 하는 편이 낫다고 봤다.
success도 함께 등록했다. 실패율의 분모이자 대시보드 계열이라 초반에 비어 있으면 해석이 흔들린다.
배포하고 나서 확인한 것
이번 수정은 서버 코드 변경이라 릴리스가 필요했다. 운영 이미지는 main 기준으로 빌드되므로 dev 머지만으로는 카운터가 사전 등록되지 않는다. 이전 글에서 두 번 밟은 함정이라 이번에는 릴리스 티켓 설명에 미리 적어 뒀다.
배포 후 확인할 것은 하나였다.
manyak_chat_turn_refund_total{service_name="manyak-server", outcome="failure"}
환불 실패가 한 건도 없는데 값 0으로 존재하면 성공이다. 실제로 배포 시각부터 선이 시작됐다. 대시보드의 환불 패널에 failure와 success 두 계열이 모두 보이는 것도 같은 증거였다. 이전에는 failure 라인 자체가 없었다.
부수 효과가 하나 있다. 이제 이 규칙이 Normal이라는 사실에 의미가 생겼다. 사전 등록 전에는 “실패가 없다”와 “규칙이 죽었다”가 화면상 구분되지 않았는데, 시계열이 0으로 존재하면 규칙이 실데이터로 평가되고 있다는 뜻이 된다.
배포 중에 첫 번째 규칙이 울리지 않은 것도 함께 확인했다. pending 10분이 컨테이너 교체 구간을 실제로 삼켰다.
테스트 하나가 배포를 막았다
첫 배포는 실패했다. 테스트 단계에서 914개 중 1개가 깨졌고, 그 결과 배포 잡이 통째로 skip됐다.
깨진 것은 실제 HTTP를 태우는 SSE 통합 테스트였다. 서버 어서션이 아니라 클라이언트가 응답을 읽다 터진 형태였다.
ResourceAccessException
└ IOException
└ IllegalArgumentException (Netty DefaultHttpHeadersFactory)
이번 변경은 미터 두 개를 미리 등록한 것과 상수 가시성을 넓힌 것이 전부라 HTTP 헤더 경로와 관계가 없었다. 로컬에서는 914개가 모두 통과했고, 최근 실패한 워크플로 실행에서도 이 테스트가 깨진 적은 없었다.
그래도 플래키로 단정하지 않고 같은 커밋을 그대로 재실행했다. 코드가 동일한데 결과가 갈리면 테스트 쪽 문제이고, 또 깨지면 내 변경을 의심해야 한다. 재실행에서는 전부 통과했다.
한 번의 우연으로 넘기기는 애매한 종류다. 이 테스트가 깨질 때마다 릴리스가 막히는 구조이기 때문이다. 재발 빈도를 기준으로 별도 작업으로 다루기로 하고 판단 기준만 기록해 뒀다.
정리
기준선이 쌓여야 알림을 만들 수 있다고 생각했는데, 그 전제는 비율로 걸겠다는 선택에 딸린 것이었다. 건수로 바꾸니 착수를 미룰 이유가 사라졌다. 미뤄 둔 작업의 선행 조건이 정말 그 작업의 조건인지, 아니면 특정 접근 방식의 조건인지는 구분해 볼 필요가 있었다.
알림에서 가장 많이 판단한 것은 무엇을 넣을지가 아니라 무엇을 빼는지였다. 후보 다섯 개 중 셋만 남겼고, 기준은 “울리면 지금 무엇을 하는가” 하나였다. 대시보드에서는 정보가 하나 더 있어서 손해 볼 일이 없지만 알림은 하나 늘 때마다 나머지 알림의 신뢰도가 깎인다.
같은 상황에 대한 처방이 도구에 따라 갈린 것도 이번에 정리됐다. 빈 결과를 0으로 채우는 것은 패널에서는 맞고 규칙에서는 틀렸다. No Data는 한 규칙에서는 가장 중요한 신호이고 나머지에서는 무시해야 할 정상 상태였다.
가장 크게 배운 것은 알림이 계측 코드의 세부 동작에 의존한다는 사실이다. 카운터가 첫 증가 시점에 등록된다는 것은 이미 알고 문서에도 적어 둔 사실이었는데, 그게 임계값 0인 알림을 무력화한다는 데까지 연결하지 못했다. 대시보드에서는 라인이 늦게 보이는 정도의 불편이 알림에서는 조용한 오작동이 된다. 알림이 카운터의 첫 한 건에 의존한다면 그 카운터는 미리 등록돼 있어야 한다.
만들고 나서 반드시 한 번 울려 봐야 한다는 것도 확인했다. 규칙마다 발화 경로가 달라 임계값을 뒤집는 것으로는 검증되지 않는 규칙이 있었고, 해제 알림이 5분 늦게 오는 것처럼 실제로 눌러 보지 않으면 오해할 동작도 있었다. 알림의 유일한 실패 모드는 조용히 안 오는 것이라, 그 사실을 진짜 장애 때 알게 되면 이미 늦다.