52 KiB
Lab 02 · 실패를 모으고(DLQ), 알리기 (Error Workflow)
- 관련 강의: 2단계 · 모으고 알리기
- 소요 시간: 50분 (4교시 한 시간) · 예제 1 12분 · 예제 2 12분 · 예제 3 6분 · 과제 1 10분 · 과제 2 10분
- 준비: Lab 01의
my-work/intake.json(없으면cp reference/lab01-example2-intake.json my-work/intake.json후 push. 그 파일은 Lab 01 예제 2까지) - 목표: 다시 하면 될 실패는 잃지 않게 모아 두고(DLQ), 예상하지 못한 오류는 n8n이 대신 알리게(Error Workflow) 한다. 어느 실패가 어느 그물에 걸리는지 보고 그물 사이의 구멍을 막고, 처리한 실패도 사람에게 알리되 같은 알림이 쏟아지지 않게 한다.
flowchart LR
E1["예제 1<br/>실패한 요청은 DLQ"] --> E2["예제 2<br/>Error Workflow"] --> E3["예제 3<br/>어느 실패가<br/>어느 그물에"] --> T1["과제 1<br/>처리한 실패도 알림<br/>DLQ가 멈추면?"] --> T2["과제 2<br/>알림 폭주 막기"]
복사해 붙일 요청문은 없습니다. 막히면
▶를 하나씩 펼칩니다. 연습이 끝나면uv run tools/flow.py ops --reset.
예제 1 · 실패한 요청은 DLQ에 (12분)
이 실습의 의도
한 줄로: 데이터 API가 멈춘 동안 들어온 문의를 잃지 않게 따로 모아 두는 곳(DLQ)을 만듭니다.
생활 속 예 · 택배 보관소. 집에 아무도 없으면 기사는 상자를 버리지 않고 보관소에 상자 그대로 · 못 전한 까닭과 함께 둡니다. 지금
my-intake는 "배달 실패"라고 말만 하고 상자를 길에 두고 가는 기사와 같습니다. 예제 1은 보관소(DLQ)를 만드는 실습입니다.
상황. Lab 01까지 고친 my-intake는 API가 멈추면 그 문의를 failed로 표시하고 200으로 답합니다. 정직한 답이지만, 그 답을 받은 화면이 닫히면 끝입니다. 문의 내용은 데이터 API에도, n8n에도, 어디에도 남지 않습니다. 원인을 고친 뒤 "아까 실패한 것 다시 해 줘"라고 할 대상이 없습니다.
먼저 볼 것 → 고칠 것.
| 순서 | 명령 | 볼 것 |
|---|---|---|
| 1 | fault tickets error → call my-intake batch-two |
답은 failed: 2. 여기까지는 Lab 01과 같음 |
| 2 | fault tickets ok → ops |
DLQ가 0 — 실패한 두 문의가 어디에도 없음 |
| 3 | test cases/lab02.json (고치기 전) |
DLQ 케이스가 ✘ |
| 4 | 요청문 → push → 다시 test | DLQ에 R-301 · R-302가 원래 요청 그대로 |
무엇을 넣고 무엇을 안 넣나. 다시 하면 될 실패(failed)만, 처음 받은 요청 그대로 넣습니다. 그래야 Lab 03에서 같은 요청을 my-intake에 다시 보낼 수 있습니다. 거절(rejected)은 다시 해도 같으니 넣지 않습니다.
끝나면 알게 되는 것. 실패를 "알리는 것"과 "잃지 않는 것"은 다르다는 것, DLQ에 무엇을 담아야 나중에 다시 돌릴 수 있는지. 이 DLQ는 Lab 03의 재처리가 읽어 갑니다.
uv run tools/flow.py ops --reset
uv run tools/flow.py fault tickets error
uv run tools/flow.py call my-intake requests/batch-two.json # failed 2 — 그다음은?
uv run tools/flow.py fault tickets ok
uv run tools/flow.py ops # DLQ는?
uv run tools/flow.py test cases/lab02.json -v # 고치기 전
API가 멈춘 동안 들어온 두 문의는 결과에 failed로만 남았습니다. 사용자 화면을 닫으면 사라집니다.
만들 것: 실패한(failed) 요청을 원래 요청 그대로 데이터 API의 DLQ(POST /dlq)에 넣기.
| 조건 | 값 |
|---|---|
| 넣는 것 | 본문 {items: [...]}, 실패한 항목마다 {item: 원래 요청 {id, user, text}, reason, workflow: "intake"} |
거절(rejected)은 |
넣지 않는다 (다시 해도 같으니까) |
| 실패가 없으면 | DLQ를 부르지 않는다 |
| 기대 결과 | cases/lab02.json 앞 두 케이스 |
이해하기
DLQ(dead letter queue)는 "배달하지 못한 편지 보관함"입니다. 우체국은 주소가 틀린 편지를 버리지 않고 따로 모아 둡니다. 메시지 시스템(Amazon SQS · RabbitMQ · Kafka)도 처리하지 못한 메시지를 버리지 않고 DLQ로 옮깁니다. 나중에 원인을 고친 사람이 같은 메시지를 다시 처리할 수 있게요.
flowchart LR
subgraph 지금
S1["Summarize<br/>failed 2"] --> O1(["응답 · 그리고 끝"])
end
subgraph 바꾼 뒤
S2["Summarize"] --> F2{"failed > 0?"}
F2 -->|예| D2[("POST /dlq<br/>원래 요청 · 이유")] --> O2(["응답"])
F2 -->|아니오| O2
end
무엇을 넣나.
| 넣는 것 | 까닭 |
|---|---|
원래 요청 그대로 (id · user · text) |
나중에 같은 요청으로 다시 돌림 (Lab 03). 분류 결과 같은 중간 값이 아니라 처음 받은 것 |
이유 (reason) |
사람이 원인을 압니다 (500 - tickets service is failing, timeout) |
어느 워크플로우 (workflow) |
여러 워크플로우가 같은 DLQ를 쓸 때 |
거절은 넣지 않습니다. 빈 글은 다시 해도 빈 글입니다. DLQ는 "나중에 다시 하면 될 것"만 모읍니다. 거절까지 넣으면 재처리할 때마다 같은 실패가 되풀이되고, 정말 살려야 할 것이 묻힙니다.
같은 요청이 또 오면. 오늘 데이터 API는 같은 item.id가 다시 들어오면 새 줄을 만들지 않고 attempts를 하나 올립니다. 재처리(Lab 03)에서 또 실패할 때 쓰입니다.
테스트 케이스 (예제 1 몫).
| 케이스 | 하는 것 | 기대 |
|---|---|---|
| API가 멈추면 | tickets error · batch-two |
200 · created: 0, failed: 2 |
| DLQ 확인 | /dlq/list |
size: 2, R-301 · R-302 |
기대 결과. 고치기 전 lab02 2/4, 예제 1 뒤 3/4 (남은 ✘는 예제 2의 알림).
▶ 1단계 · 왜 원래 요청 그대로
나중에 다시 돌릴 때(3단계) 같은 요청을 그대로 my-intake에 보내야 합니다. Lab 01의 결과에서 실패한 항목은 request(원래 요청)를 가지고 있습니다. 그것을 모아 DLQ 본문으로.
▶ 2단계 · DLQ에 넣다가 실패하면
DLQ 저장마저 실패하면? 최소한 사용자 응답은 나가야 합니다. 어떤 설정을 둘까요? (그다음은 예제 3에서 직접 봅니다)
▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대
요청문에 꼭 들어갈 것: ① 언제 DLQ로 가나 ② DLQ 주소와 본문 모양 ③ 넣지 않는 것 ④ DLQ가 실패해도 응답 ⑤ 기대 결과. 빈칸 4개를 채웁니다.
my-work/intake.json 의 Summarize 뒤에 Any failed?(If) 를 넣어 failed 가 1 이상일 때만 Send to DLQ(HTTP POST http://data-api:8800/ ____① ) 로.
본문은 {items: [...]}, 실패한 항목마다 { item: ____② {id, user, text}, reason, workflow: "intake" }. rejected 는 ____③ .
DLQ 저장이 실패해도 응답은 나가게 options.response 에 ____④ , 두 갈래 모두 Public result 로 가서 같은 결과를 응답.
기대 결과 cases/lab02.json 앞 두 케이스.
빈칸 힌트:
- ① AGENTS.md 2절 데이터 API 표에서 실패한 요청을 넣는 주소입니다.
- ② 나중에 다시 돌릴 때 그대로 보낼 것입니다 (▶ 1단계).
- ③ 다시 해도 같은 실패입니다.
- ④ AGENTS.md 5절 "DLQ · 알림" 노드의 응답 설정입니다. HTTP 오류를 노드 오류로 만들지 않는 값.
uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab02.json -v
uv run tools/flow.py ops
실행 결과 · 실패한 요청은 DLQ에
수업 준비 때(10/29) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다. 시간은 실행마다 조금씩 다르고, ✔ · ✘는 같아야 합니다.
고치기 전 · Lab 01 과제 2까지 고친 my-intake
실행한 명령:
uv run tools/flow.py ops --reset
uv run tools/flow.py fault tickets error
uv run tools/flow.py call my-intake requests/batch-two.json
uv run tools/flow.py fault tickets ok
uv run tools/flow.py ops
uv run tools/flow.py test cases/lab02.json -v
출력 (줄임):
HTTP 200 · 2.8s
{ "total": 2, "created": 0, "failed": 2, "rejected": 0, "duplicates": 0, "unclassified": 0,
"results": [ { "id": "R-301", "status": "failed", "reason": "500 - … tickets service is failing (fault switch)" },
{ "id": "R-302", "status": "failed", "reason": "500 - … tickets service is failing (fault switch)" } ] }
fault switch: all ok
created tickets: 0
DLQ: 0 (parked 0)
alerts: 0
✔ 예제 1 · API가 멈추면 실패로 표시 · 200 · 2.8s
✘ 예제 1 · 실패한 2건은 DLQ에 · 0.0s · size = 0 (expected 2); items has none of ['R-301', 'R-302']
✔ 예제 2 · 예상 못 한 오류 (nightly-report) · 0.0s
✘ 예제 2 · Error Workflow가 알림을 남김 · 0.0s · severity = null (expected critical); ...
Passed 2/4
왜 이렇게 나왔나 (고치기 전)
- 응답은 정직합니다. 200 ·
failed: 2, 항목마다 이유(tickets service is failing)까지 있습니다. Lab 01에서 이미 이렇게 고쳤습니다. 그래서 "실패로 표시" 케이스는 ✔입니다. - 그런데
ops에는 아무것도 없습니다. 접수 건 0, DLQ 0, 알림 0. 결과를 받은 화면(사용자의 브라우저, 부른 프로그램)이 닫히면 R-301 · R-302 두 문의는 어디에도 남지 않습니다. 고장 스위치를ok로 되돌려도 다시 보낼 것이 없습니다. - DLQ 케이스 ✘
size = 0: DLQ에 넣는 갈래가 아직 없으니 당연합니다. - Error Workflow 케이스 ✘: 예제 2에서 고칠 것입니다. 지금은 신경 쓰지 않습니다.
- 2/4는 예상한 결과입니다.
고친 뒤
실행한 명령:
uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab02.json -v
uv run tools/flow.py ops
출력 (결과 줄과 ops):
✔ 예제 1 · API가 멈추면 실패로 표시 · 200 · 4.1s
✔ 예제 1 · 실패한 2건은 DLQ에 · 0.0s
✔ 예제 2 · 예상 못 한 오류 (nightly-report) · 0.1s
✘ 예제 2 · Error Workflow가 알림을 남김 · 0.0s · severity = null (expected critical); ...
Passed 3/4
DLQ: 2 (parked 0)
D-1 R-301 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
D-2 R-302 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
고친 뒤 이렇게 나온 까닭
| 볼 것 | 고치기 전 | 고친 뒤 | 무엇이 바꿨나 |
|---|---|---|---|
| 응답 | failed: 2 |
failed: 2 (그대로) |
응답은 바꾸지 않음 |
| DLQ | 0 | 2 (D-1 · D-2) | failed 항목을 DLQ에 넣는 갈래 |
| 다시 돌릴 수 있나 | 없음 | 원래 요청 그대로 있음 | DLQ에 요청 전체를 저장 |
- 응답은 똑같이
failed: 2입니다. 달라진 것은 실패가 남았느냐입니다. DLQ 한 줄마다R-301(원래 요청 id),attempts 1(DLQ에 들어간 횟수),parked False(아직 사람이 볼 것으로 멈추지 않음), 실패 이유가 있습니다. - 원래 요청이 그대로 있으니, 원인(데이터 API 장애)을 고친 뒤 같은 요청을 다시 돌릴 수 있습니다. 그것이 Lab 03의 재처리입니다. 다시 돌려도 키가 요청 id라 중복은 생기지 않습니다.
- 남은 ✘ 하나는
nightly-report의 오류를 아무도 알리지 않아서입니다. 예제 2에서 고칩니다.
예제 2 · Error Workflow (12분)
이 실습의 의도
한 줄로: 내가 예상하지 못한 오류로 워크플로우가 멈췄을 때, n8n이 대신 알림을 남기게 합니다.
생활 속 예 · 자동차 경고등. 운전자는 모든 고장을 미리 알 수 없지만, 무슨 일이 생기면 계기판에 경고등이 켜져 정비소에 가게 됩니다. 경고등이 고장을 고치지는 않습니다. 예제 2는
nightly-report의 버그(고장)를 고치지 않고, 버그가 나면 켜지는 경고등(Error Workflow → 알림)을 다는 실습입니다.
상황. 예제 1의 DLQ는 "API가 멈출 수 있다"를 예상하고 미리 길을 만들어 둔 것입니다. 하지만 버그, 모르는 응답 모양, 누가 바꾼 설정 같은 오류는 미리 길을 만들 수 없습니다. 그런 오류로 워크플로우가 멈추면 부른 쪽에 500 하나가 갈 뿐, 운영하는 사람은 아무도 모릅니다.
연습 대상. nightly-report는 일부러 버그를 넣어 둔 제공 워크플로우입니다. 버그는 고치지 않습니다. 오늘의 목표는 버그를 고치는 것이 아니라 "버그가 나면 누가 아느냐"를 바꾸는 것입니다.
| 순서 | 명령 | 볼 것 |
|---|---|---|
| 1 | call nightly-report |
500 |
| 2 | ops |
alerts: 0 — 아무도 모름 |
| 3 | 에러 처리기를 만들어 push → test cases/lab02.json |
3초 뒤 critical 알림: 어느 워크플로우 · 어느 노드 · 무슨 오류 |
n8n이 해 주는 것과 내가 할 것. 워크플로우 설정에 errorWorkflow(처리기 id)가 있으면, 그 워크플로우가 멈출 때 n8n이 처리기를 부릅니다. 나는 처리기(Error Trigger로 시작 → 알림 남기기)를 만들어 정해진 id로 게시하고, 내 my-intake에도 같은 설정을 넣습니다.
끝나면 알게 되는 것. "처리한 실패"(DLQ)와 "처리하지 못한 실패"(Error Workflow)는 받는 곳이 다르고 둘 다 있어야 한다는 것, 알림 한 줄에 무엇이 있어야 고칠 곳을 찾을 수 있는지.
이제 확인합니다. nightly-report는 일부러 버그가 있는 워크플로우입니다 (고치지 않습니다).
uv run tools/flow.py call nightly-report # HTTP 500 — 누가 알까?
uv run tools/flow.py ops # alerts: 0
만들 것: 처리하지 못한 오류로 워크플로우가 멈추면 n8n이 부르는 에러 처리기. 알림을 데이터 API에 남긴다. 그리고 내 접수 워크플로우도 이 처리기를 쓰게.
| 조건 | 값 |
|---|---|
| 파일 | my-work/error-handler.json, 맨 위에 "id": "errHandler76001" (AGENTS.md 3 · 4절) |
| 알림 | POST /alerts {workflow, workflow_id, node, message, execution_id, severity: "critical", reason: "unexpected"} |
| 내 접수 워크플로우 | settings.errorWorkflow: "errHandler76001" |
| 기대 결과 | cases/lab02.json 전체, lab01도 그대로 |
이해하기
처리한 실패와 처리하지 못한 실패. 예제 1의 DLQ는 내가 예상한 실패(API가 멈춤)를 그 자리에서 처리한 것입니다. 하지만 모든 실패를 예상할 수는 없습니다. 버그, 모르는 응답 모양, 누가 바꾼 설정. 그런 오류로 워크플로우가 멈추면, 응답 500 하나만 남고 아무도 모릅니다.
| 처리한 실패 | 처리하지 못한 실패 | |
|---|---|---|
| 예 | API가 멈춤 (예상함) | nightly-report의 버그 |
| 워크플로우 | 계속, 200 | 멈춤, 500 |
| 받는 곳 | DLQ + 알림 (내가 만듦, 과제 1) | Error Workflow (n8n이 부름) |
Error Workflow는 마지막 그물입니다. n8n은 워크플로우 설정에 errorWorkflow가 있으면, 그 워크플로우가 처리하지 못한 오류로 멈출 때 지정한 워크플로우를 부릅니다. 받는 쪽은 Error Trigger 노드로 시작하고, 어느 워크플로우 · 어느 실행 · 어느 노드 · 무슨 오류인지를 받습니다.
flowchart LR
W["nightly-report<br/>settings.errorWorkflow"] -->|처리 못 한 오류로 멈춤| E["On error<br/>(Error Trigger)"] --> C["Make alert"] --> A[("POST /alerts<br/>critical · unexpected")]
W -.->|응답 500| U(["부른 사람"])
| Error Trigger가 넘기는 것 | 알림에 |
|---|---|
workflow.name · workflow.id |
어느 워크플로우 |
execution.lastNodeExecuted |
어느 노드 |
execution.error.message |
무슨 오류 |
execution.id |
n8n 화면 Executions에서 그 실행을 연다 |
처리기의 id는 고정합니다. 다른 워크플로우가 처리기를 id로 부르므로, 파일 맨 위에 "id": "errHandler76001"을 씁니다 (70일차 모듈과 같음). 처리기도 **게시(push)**해야 합니다. 게시를 내린 처리기는 n8n이 부르지 않습니다 (수업 준비 때 확인).
알림은 늦게 생깁니다. Error Workflow는 웹훅 응답(500)이 나간 뒤에 돕니다. 테스트는 3초를 기다린 뒤({"sleep": 3}) 알림을 봅니다.
테스트 케이스 (예제 2 몫).
| 케이스 | 하는 것 | 기대 |
|---|---|---|
| 예상 못 한 오류 | nightly-report |
500 |
| 알림 | 3초 뒤 /alerts/latest |
severity: critical, reason: unexpected, 워크플로우 이름, 노드 Count by category |
기대 결과. lab02 4/4. ops에 critical 알림 하나.
▶ 1단계 · 둘 다 있어야 하는 이유
DLQ만 있으면? 예상 못 한 버그는 아무도 모릅니다. Error Workflow만 있으면? API가 멈출 때마다 워크플로우가 멈춰야 알림이 오고, 멈추면 그 실행의 다른 항목도 함께 잃습니다.
▶ 2단계 · 알림에 무엇을
받는 사람이 "어느 워크플로우의 어느 노드에서 무슨 오류"를 바로 알아야 합니다. 실행 번호가 있으면 n8n 화면 Executions에서 그 실행을 엽니다. Error Trigger가 넘기는 값은 AGENTS.md 5절.
▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대
요청문에 꼭 들어갈 것: ① 파일과 고정 id ② 시작 노드 ③ 알림에 담을 값과 등급 ④ 부르는 쪽 설정 ⑤ 기대 결과. 빈칸 4개를 채웁니다.
my-work/error-handler.json 을 새로 만들어 줘. 파일 맨 위에 "id": "____①", 첫 노드는 ____② .
Make alert(Code) 가 workflow.name · workflow.id · execution.lastNodeExecuted · execution.error.message · execution.id 를
{workflow, workflow_id, node, message, execution_id, severity: "____③", reason: "unexpected"} 로 만들어 POST http://data-api:8800/alerts.
my-work/intake.json 의 settings 에 ____④ : "errHandler76001" 을 넣어 줘.
기대 결과 cases/lab02.json 전체.
빈칸 힌트:
- ① 위 조건 표의 파일 줄입니다.
nightly-report의 설정에 이미 적혀 있는 값과 같아야 합니다. - ② AGENTS.md 5절 "Error Workflow"의 노드 종류(
type의 끝 부분)입니다. - ③ 처리하지 못한 오류의 등급입니다. 강의 2단계 알림 등급 표.
- ④ AGENTS.md 4절 워크플로우 파일 규칙의
settings안 이름입니다.
uv run tools/flow.py push my-work/error-handler.json my-work/intake.json
uv run tools/flow.py test cases/lab02.json -v
uv run tools/flow.py ops
uv run tools/flow.py test cases/lab01.json
실행 결과 · Error Workflow
수업 준비 때(10/29) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다.
고치기 전 · 에러 처리기 없음
실행한 명령:
uv run tools/flow.py call nightly-report
uv run tools/flow.py test cases/lab02.json -v # 예제 1을 고친 뒤의 결과
출력 (줄임):
HTTP 500 · 0.1s
{ "message": "Error in workflow" }
✔ 예제 2 · 예상 못 한 오류 (nightly-report) · 0.1s
✘ 예제 2 · Error Workflow가 알림을 남김 · 0.0s · severity = null (expected critical); ...
Passed 3/4
왜 이렇게 나왔나 (고치기 전)
nightly-report에는 일부러 넣은 버그가 있어 0.1초 만에 500으로 끝납니다. 이것은 DLQ로 받을 수 없는 실패입니다. 항목 하나가 아니라 워크플로우가 통째로 멈춘 것이고, 오늘 만든 갈래들은my-intake안에만 있기 때문입니다.- 500을 본 것은
call을 한 우리뿐입니다. 밤에 스케줄로 돌았다면 아무도 보지 못했을 것입니다. 알림 케이스의severity = null은 "알림이 하나도 없음"이라는 뜻입니다. ops를 봐도alerts: 0입니다. 실패는 했는데 누구도 모르는 상태입니다.
고친 뒤
실행한 명령:
uv run tools/flow.py push my-work/error-handler.json my-work/intake.json
uv run tools/flow.py test cases/lab02.json -v
uv run tools/flow.py ops
uv run tools/flow.py test cases/lab01.json
출력 (결과 줄과 ops):
✔ 예제 1 · API가 멈추면 실패로 표시 · 200 · 4.8s
✔ 예제 1 · 실패한 2건은 DLQ에 · 0.0s
✔ 예제 2 · 예상 못 한 오류 (nightly-report) · 0.1s
… before: wait 3s
✔ 예제 2 · Error Workflow가 알림을 남김 · 0.0s
Passed 4/4
alerts: 1
[critical] 76 Nightly report (provided, has a bug) · unexpected · Count by category · rows is not iterable [line 4]
(lab01 6/6)
고친 뒤 이렇게 나온 까닭
| 볼 것 | 고치기 전 | 고친 뒤 | 무엇이 바꿨나 |
|---|---|---|---|
nightly-report |
500 | 500 (그대로) | 버그는 고치지 않음 |
| 알림 | 0 | critical 1 | 에러 처리기 errHandler76001을 게시 |
| 누가 아나 | call한 사람만 |
알림을 보는 사람 모두 |
nightly-report는 여전히 500입니다. 버그를 고친 것이 아니라 누가 아느냐를 바꿨습니다.… before: wait 3s: 알림은 응답이 나간 뒤에 생깁니다. n8n이 워크플로우가 멈춘 것을 보고 에러 처리기를 따로 부르기 때문입니다. 그래서 테스트가 3초 기다린 뒤 확인했습니다.- 알림 한 줄에 어느 워크플로우(
76 Nightly report) · 어떤 종류(unexpected, 예상 못 한 오류) · 어느 노드(Count by category) · 무슨 오류(rows is not iterable [line 4])가 다 있습니다. 응답의tickets를items로 잘못 읽어rows가 비어 있었던 것입니다. 알림만 보고 고칠 곳을 찾을 수 있습니다. nightly-report의 설정에는 처음부터errorWorkflow: errHandler76001이 있었습니다. 그래서 처리기를 그 id로 게시하는 것만으로 알림이 시작되었습니다.- 내
my-intake도 같은 처리기를 씁니다. 하지만 오늘의 실패를 모두 스스로 처리하고 200으로 끝나므로 처리기가 불리지 않습니다.lab016/6도 그대로입니다.
예제 3 · 어느 실패가 어느 그물에 (6분, 에이전트 없이)
이 실습의 의도
한 줄로: 지금까지 만든 그물 셋이 각각 어떤 실패를 잡는지 확인하고, 그물 사이로 빠지는 구멍을 찾습니다. 고치지 않고(에이전트 없이) 재기만 합니다.
생활 속 예 · 건전지가 다 된 화재경보기. 경보기가 있어도 건전지가 다 되면 불이 나도 조용합니다. 예제 3은 그물(거절 · 보관소 · 경고등)을 하나씩 시험하다가, 보관소(DLQ) 문까지 잠겼을 때 아무 소리도 나지 않는 것을 직접 확인하는 실습입니다.
지금 있는 그물 셋.
| 그물 | 언제 만들었나 | 잡는 것 |
|---|---|---|
결과의 rejected |
Lab 01 예제 2 · 과제 2 | 잘못된 항목 |
| DLQ | 예제 1 | API가 멈춰 처리 못 한 항목 |
| Error Workflow | 예제 2 | 워크플로우를 멈추게 한 예상 못 한 오류 |
실패 넷을 일으킵니다. 빈 글 · API 오류 · 버그, 그리고 마지막으로 API와 DLQ가 함께 멈춘 경우. 앞의 셋은 각자 그물에 걸립니다. 네 번째는 어디에 걸릴까요? DLQ에 넣지 못했고, 워크플로우는 멈추지 않았으니(neverError) Error Workflow도 불리지 않습니다.
lab02-nets.json의 6 · 7번은 지금 ✘가 맞습니다. 이 예제는 구멍을 보여 주는 것이고, 막는 것은 과제 1입니다. 5/7이 기대 결과입니다.
끝나면 알게 되는 것. 실패를 처리하는 장치(DLQ)도 실패할 수 있고, 그때 요청이 소리 없이 사라진다는 것 ("누가 감시자를 감시하나"). 그물 지도를 표로 그려 두면 구멍이 보인다는 것.
적을 곳. my-work/RUNBOOK.md 1.3 그물 지도 (실패 · 어떻게 알아채나 · 자동으로 하는 일 · 사람이 할 일).
uv run tools/flow.py test cases/lab02-nets.json -v
uv run tools/flow.py ops
이해하기
그물 지도.
| 실패 | 걸리는 그물 | 사람이 아는 방법 |
|---|---|---|
| 잘못된 항목 (빈 글) | 결과의 rejected + 이유 |
부른 쪽이 결과를 보고 고쳐 보냄 |
| API 오류 | failed → DLQ |
(아직 없음 → 과제 1) |
버그 (nightly-report) |
Error Workflow → critical 알림 | 알림 |
| DLQ도 멈춤 | ? | ? |
flowchart LR
F1["빈 글"] --> R["rejected"]
F2["API 오류"] --> D[("DLQ")]
F3["버그"] --> E["Error Workflow"] --> A[("알림")]
F4["API 오류 +<br/>DLQ 오류"] -.->|neverError로<br/>조용히 지나감| X(["200 · failed 3<br/>어디에도 없음"])
구멍은 그물 사이에 있습니다. 예제 1에서 DLQ 저장이 실패해도 응답은 나가게 neverError를 두었습니다. 그래서 DLQ가 멈추면 워크플로우는 멈추지 않고(→ Error Workflow도 안 불림), DLQ에도 없고, 결과는 failed만 말합니다. 실패를 처리하는 장치 자체가 실패할 때를 생각해야 합니다. 이것을 "누가 감시자를 감시하나"라고 부르기도 합니다.
테스트 케이스. 지금 워크플로우를 바꾸지 않고 재기만 합니다. 6 · 7번은 지금은 ✘가 맞습니다 (과제 1에서 고침).
| 케이스 | 하는 것 | 기대 |
|---|---|---|
| 1 | batch-bad-item |
rejected: 1, created: 2 |
| 2 · 3 | tickets error · batch-two, /dlq/list |
failed: 2, DLQ size: 2 |
| 4 · 5 | nightly-report, 3초 뒤 알림 |
500, critical · unexpected |
| 6 | tickets error + dlq error · batch-ok |
failed: 3, dlq_saved: false |
| 7 | /alerts/latest |
critical · dlq_down |
기대 결과. 5/7. 6번은 결과에 dlq_saved가 없어서, 7번은 마지막 알림이 5번의 unexpected라서 ✘. ops의 DLQ는 2건 그대로 (3건이 사라짐).
▶ 1단계 · 사라진 3건을 찾을 수 있나
6번 뒤 ops를 봅니다. DLQ는 몇 건인가요? n8n 화면 Executions에서 그 실행을 열면 Send to DLQ 노드의 응답에 무엇이 있나요? 사람이 매번 Executions를 열어 볼 수 있을까요?
실행 결과 · 어느 실패가 어느 그물에
수업 준비 때(10/29) 강사 노트북에서 예제 2 정답으로 실제로 나온 출력입니다.
이 예제는 고치지 않습니다. 예제 2까지 만든 그물(결과 · DLQ · 알림)에 실패 넷을 던져 보고, 어디에 걸리는지 봅니다. 6 · 7번의 구멍은 과제 1에서 막습니다.
지금 상태 · 예제 2까지
실행한 명령:
uv run tools/flow.py test cases/lab02-nets.json -v
uv run tools/flow.py ops
출력 (결과 줄과 ops):
✔ 1 · 잘못된 항목 → 결과에만 (rejected) · 1.7s
✔ 2 · API 오류 → failed · 2.8s
✔ 3 · failed는 DLQ에 · 0.0s
✔ 4 · 버그 → 500 · 0.0s
… before: wait 3s
✔ 5 · Error Workflow → critical 알림 · 0.0s
✘ 6 · DLQ마저 멈추면 결과가 알려야 · 4.1s · dlq_saved = null (expected false)
✘ 7 · DLQ가 멈춘 것을 누가 아나 (critical · dlq_down) · 0.0s · reason = unexpected (expected dlq_down)
Passed 5/7
created tickets: 2
T-2001 key R-201 장비/보통 프린터 용지가 걸려요
T-2002 key R-203 소프트웨어/보통 메일 첨부가 안 열려요
DLQ: 2 (parked 0)
D-1 R-301 … D-2 R-302 …
alerts: 1
[critical] 76 Nightly report (provided, has a bug) · unexpected · Count by category · rows is not iterable [line 4]
왜 이렇게 나왔나 (케이스마다)
| 케이스 | 던진 실패 | 걸린 그물 | 남은 흔적 |
|---|---|---|---|
| 1 | 빈 글 항목 | 결과 (rejected) |
응답에만. 보낸 쪽이 고칠 일 |
| 2 · 3 | 데이터 API 오류 (fault tickets error) |
결과 (failed) + DLQ |
D-1 · D-2 |
| 4 · 5 | nightly-report의 버그 |
Error Workflow | critical 알림 |
| 6 · 7 | 데이터 API 와 DLQ 모두 오류 (fault dlq error) |
없음 | 없음 |
- 1~5: 그물 셋이 각자 맡은 실패를 잡았습니다.
ops의 접수 건 2개(R-201 · R-203)는 1번 요청의 정상 항목, DLQ 2건은 2번, 알림은 4번의 것입니다. - 6 · 7이 오늘의 구멍입니다. 6번에서
batch-ok의 세 문의(R-101~103)는 접수 건 만들기도 DLQ 저장도 500으로 실패했습니다. 응답은 200 ·failed: 3이지만, DLQ는 2건 그대로입니다. DLQ 저장 노드가 실패해도 멈추지 않게(neverError) 해 두었기 때문에 워크플로우는 아무 일 없다는 듯 끝났고, 결과에도 "DLQ에 못 넣었다"(dlq_saved)는 말이 없습니다. - 7번의
reason = unexpected: 마지막 알림이 여전히 5번의 버그 알림이라는 뜻입니다. DLQ가 멈췄다는 알림(dlq_down)은 없습니다. 세 문의를 잃었는데 아무도 모릅니다. - 5/7은 예상한 결과입니다. 그물이 있어도 그물이 고장 나면 실패가 빠져나갑니다.
과제 1을 고친 뒤 · 같은 테스트
실행한 명령 (과제 1 끝에서):
uv run tools/flow.py test cases/lab02-nets.json
출력:
✔ 1 · 잘못된 항목 → 결과에만 (rejected) · 2.1s
✔ 2 · API 오류 → failed · 2.9s
✔ 3 · failed는 DLQ에 · 0.0s
✔ 4 · 버그 → 500 · 0.1s
✔ 5 · Error Workflow → critical 알림 · 0.0s
✔ 6 · DLQ마저 멈추면 결과가 알려야 · 3.7s
✔ 7 · DLQ가 멈춘 것을 누가 아나 (critical · dlq_down) · 0.0s
Passed 7/7
고친 뒤 이렇게 나온 까닭: 과제 1에서 DLQ 저장 결과를 확인해 결과에 dlq_saved: false를 넣고, 저장에 실패하면 dlq_down critical 알림에 잃어버린 id를 적게 했습니다. 그래서 6 · 7번이 ✔가 되었습니다. 1~5는 그대로입니다. 자세한 것은 과제 1의 실행 결과에 있습니다.
과제 1 · 처리한 실패도 알리기, DLQ가 멈추면 (10분)
이 실습의 의도
한 줄로: DLQ에 쌓인 것을 사람이 바로 알게 하고, 예제 3에서 찾은 구멍(DLQ도 멈춤)을 더 큰 알림으로 막습니다.
생활 속 예 · 응급실과 "건전지 부족" 삑 소리. 응급실은 급한 정도에 따라 환자를 나누고, 화재경보기는 건전지가 떨어지면 따로 삑 소리를 냅니다. 과제 1은 보관소에 넣었으면 "확인해 주세요"(high), 보관소에도 못 넣었으면 "지금 바로"(critical, 잃을 뻔한 송장 번호와 함께)로 등급을 나눠 알리는 실습입니다.
문제 둘.
| 문제 | 지금 | 바라는 것 |
|---|---|---|
| DLQ에 쌓여도 아무도 모름 | 다음 날 아침에 ops를 쳐야 앎 |
DLQ에 넣을 때 high 알림 하나 (몇 건인지) |
| DLQ도 멈추면 요청이 사라짐 | 결과는 failed만, 알림 없음 |
critical 알림, 메시지에 잃을 뻔한 요청 id |
왜 등급을 나누나. DLQ에 넣었으면 요청은 안전합니다. 원인을 고친 뒤 재처리하면 됩니다(high). DLQ에도 못 넣었으면 요청은 지금 어디에도 없습니다. 알림에 적힌 id가 유일한 단서라 바로 봐야 합니다(critical).
지킬 것. 실패가 없으면 알림을 보내지 않습니다. 실패한 항목마다가 아니라 요청 하나에 알림 하나(개수를 담아)입니다. 그리고 결과에 dlq_saved · alert를 더해, 부른 쪽도 "DLQ에 들어갔나"를 알게 합니다.
끝나면 알게 되는 것. 알림 등급을 무엇으로 정하나(요청을 잃을 수 있나), DLQ에 저장됐는지를 상태 코드가 아니라 응답 내용으로 판단하는 법. 고치면 예제 3의 lab02-nets가 7/7이 됩니다.
"DLQ에 쌓이는 걸 아무도 몰라서 다음 날 아침에야 봤어요. DLQ에 넣었으면 바로 알려 주세요. 그리고 아까처럼 DLQ에도 못 넣었으면 그건 정말 급한 거니 더 크게 알려 주세요. 대신 문제가 없을 때는 알림을 보내지 마세요."
uv run tools/flow.py test cases/lab02-challenge.json -v # 고치기 전
| 조건 | 값 |
|---|---|
| DLQ에 넣었으면 | POST /alerts {workflow: "intake", severity: "high", reason: "dlq", count, message} |
| DLQ에 못 넣었으면 | {severity: "critical", reason: "dlq_down", count, message} — message에 잃을 뻔한 요청 id들 |
| 실패가 없으면 | 알림 없음 |
| 결과에 | dlq_saved (실패가 있을 때 true/false), alert (sent · none) |
| 기대 결과 | cases/lab02-challenge.json, lab02-nets 7/7, lab02 그대로 |
이해하기
알림 등급. 모든 알림이 같은 무게면 받는 사람은 무엇부터 볼지 모릅니다.
| 등급 | 오늘 | 받으면 |
|---|---|---|
critical |
예상 못 한 오류(버그), DLQ가 멈춤 (요청을 잃을 수 있음) | 바로 본다 |
high |
DLQ에 쌓임, parked (Lab 03) | 원인(데이터 API 등)을 확인하고 재처리 |
| (알림 없음) | 거절, 미분류 | 결과 · 보고서로 셈 |
요청 하나에 알림 하나. 실패한 항목마다 알림을 보내면 50건 실패에 알림 50개입니다. 요청 하나에 알림 하나, 개수(count)를 담습니다.
구멍 막기. DLQ의 응답에 stored가 있으면 저장된 것, 없으면(500의 error) 저장되지 않은 것입니다. 저장되지 않았다면 잃을 뻔한 요청의 id를 알림에 직접 적습니다. 그러면 DLQ가 멈춰도 사람이 알림만 보고 그 요청들을 살릴 수 있습니다. 결과의 dlq_saved: false는 부른 쪽에게도 "이건 아직 아무 데도 없다"고 알려 줍니다.
flowchart LR
F{"failed > 0?"} -->|아니오| N(["alert: none"])
F -->|예| D[("POST /dlq")] --> S{"stored?"}
S -->|예| H["high · dlq · count"] --> A[("POST /alerts")]
S -->|아니오| C["critical · dlq_down<br/>잃을 뻔한 id들"] --> A
A --> R(["dlq_saved · alert: sent"])
테스트 케이스.
| 케이스 | 하는 것 | 기대 |
|---|---|---|
| API가 멈춤 | tickets error · batch-two |
failed: 2, dlq_saved: true, alert: sent |
| high 알림 | /alerts/latest |
high · dlq · count 2 · intake |
| 실패 없음 | batch-ok |
alert: none |
| 알림은 그대로 | /alerts/list |
count: 1 |
| DLQ도 멈춤 | tickets error + dlq error · batch-bad-item |
failed: 2, dlq_saved: false |
| critical 알림 | /alerts/latest |
critical · dlq_down · count 2, message에 R-201 |
기대 결과. 고치기 전 0/6, 고친 뒤 6/6. 예제 3의 lab02-nets도 7/7.
먼저 생각해 보기 (2분): 알림이 너무 많으면 어떻게 될까요? critical과 high는 받는 사람에게 무엇이 달라야 할까요? (런북 1절)
▶ 1단계 · 한 번에 하나
DLQ 노드는 요청 하나에 한 번 돕니다 (items 목록 하나). 그 뒤에 알림을 하나 만들면 저절로 "요청 하나에 하나"가 됩니다.
▶ 2단계 · 저장됐는지 어떻게 아나
curl -s -XPOST localhost:8800/dlq -d '{"items": [{"item": {"id": "X-1"}, "reason": "test"}]}'과, fault dlq error 뒤 같은 명령을 해 봅니다. 응답에 무엇이 있고 무엇이 없나요? (끝나면 ops --reset)
▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대
요청문에 꼭 들어갈 것: ① 어디에 붙이나 ② 저장됐을 때 알림 ③ 저장 못 했을 때 알림 ④ 알림 수 ⑤ 결과에 더할 칸 ⑥ 기대 결과. 빈칸 4개를 채웁니다.
my-work/intake.json 의 Send to DLQ 뒤에 Make alert(Code) 와 Send alert(HTTP POST http://data-api:8800/alerts) 를 넣어 줘.
DLQ 응답에 ____① 가 있으면 {workflow: "intake", severity: "____②", reason: "dlq", count: failed 수, message},
없으면(DLQ 저장 실패) severity "critical", reason " ____③ ", message 에 잃을 뻔한 요청 id 들.
알림은 요청 하나에 하나 (항목마다가 아님). 실패가 없으면 알림을 보내지 않는다.
결과에 dlq_saved(실패가 있을 때 true/false) 와 ____④ ("sent" / "none") 를 더해 줘.
기대 결과 cases/lab02-challenge.json, cases/lab02-nets.json, cases/lab02.json.
빈칸 힌트:
- ① ▶ 2단계에서 본 DLQ 응답의 칸입니다. 저장된 수.
- ② 팀장 말의 "바로 알려 주세요"는 critical보다 한 단계 아래입니다 (위 등급 표).
- ③ 위 조건 표의 reason 그대로입니다 (테스트가 찾음).
- ④ 위 조건 표의 결과 칸 이름입니다.
uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab02-challenge.json -v
uv run tools/flow.py test cases/lab02-nets.json
uv run tools/flow.py test cases/lab02.json
uv run tools/flow.py ops
실행 결과 · 처리한 실패도 알리기
수업 준비 때(10/29) 강사 노트북에서 실제로 나온 출력입니다.
고치기 전 · 예제 2까지 고친 my-intake
실행한 명령:
uv run tools/flow.py test cases/lab02-challenge.json -v
출력 (결과 줄만):
✘ API가 멈춤 → DLQ · 2.7s · dlq_saved = null (expected true); alert = null (expected sent)
✘ 처리한 실패도 알림 (high · 개수) · 0.0s · severity = null (expected high); reason = null (expected dlq); ...
✘ 실패가 없으면 알림 없음 · 2.8s · alert = null (expected none)
✘ 알림은 그대로 1개 · 0.0s · count = 0 (expected 1)
✘ DLQ도 멈추면 critical · 2.4s · dlq_saved = null (expected false)
✘ 잃어버린 id를 알림에 · 0.0s · severity = null (expected critical); ... message has none of ['R-201']
Passed 0/6
왜 이렇게 나왔나 (고치기 전)
- 첫 줄 ✘: DLQ에는 실제로 넣었습니다. 하지만 결과에
dlq_saved·alert칸이 없어null입니다. 보낸 쪽은 실패한 것이 안전하게 보관됐는지 알 수 없습니다. - 처리한 실패 알림 ✘: 예제 2의 Error Workflow는 워크플로우가 멈춘 실패만 알립니다.
my-intake는 실패를 스스로 처리하고 200으로 끝나니 처리기가 불리지 않습니다. 그래서 DLQ에 2건이 쌓여도 알림은 0개입니다. DLQ를 누가 들여다보지 않으면 아무도 모릅니다. - 실패가 없으면 ✘ · 알림 1개 ✘: 결과에
alert: "none"이 없고, 앞 케이스에서 알림이 0개라 "그대로 1개"도 맞지 않습니다. - DLQ도 멈추면 ✘ · 잃어버린 id ✘: 예제 3의 구멍 그대로입니다.
- 0/6은 예상한 결과입니다. 결과와 알림의 새 칸을 아직 만들지 않았으니 모두 ✘입니다.
고친 뒤
실행한 명령:
uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab02-challenge.json -v
uv run tools/flow.py test cases/lab02-nets.json
uv run tools/flow.py test cases/lab02.json
uv run tools/flow.py ops
출력 (결과 줄과, 응답에 있던 알림 두 개):
✔ API가 멈춤 → DLQ · 4.3s
✔ 처리한 실패도 알림 (high · 개수) · 0.0s
✔ 실패가 없으면 알림 없음 · 2.9s
✔ 알림은 그대로 1개 · 0.0s
✔ DLQ도 멈추면 critical · 2.4s
✔ 잃어버린 id를 알림에 · 0.0s
Passed 6/6
(lab02-nets 7/7 · lab02 4/4)
[high] intake · dlq · count 2 · "2 request(s) could not be created and wait in the DLQ"
[critical] intake · dlq_down · count 2 · "DLQ save failed: dlq service is failing (fault switch) · lost ids R-201, R-203"
고친 뒤 이렇게 나온 까닭
| 상황 | 고치기 전 | 고친 뒤 결과 | 알림 |
|---|---|---|---|
| API가 멈춤 (2건 실패) | DLQ에만 | dlq_saved: true, alert: "sent" |
high dlq 하나 (count 2) |
| 실패 없음 | 칸 없음 | alert: "none" |
없음 (1개 그대로) |
| DLQ도 멈춤 | 아무 흔적 없음 | dlq_saved: false |
critical dlq_down + lost ids R-201, R-203 |
- 실패 2건 → 알림 하나: 실패마다 하나씩이 아니라 요청 하나에 하나, 개수(
count 2)를 담았습니다. 알림을 받은 사람은 "DLQ에 2건이 기다린다"를 압니다. 이 알림은 high입니다. 문의는 DLQ에 안전하게 있고, 원인을 고친 뒤 재처리하면 되기 때문입니다. - 실패가 없으면 알림 없음: 정상 요청은 알림을 만들지 않습니다. 알림이 "무슨 일이 있다"는 뜻을 잃지 않게 하는 규칙입니다.
- DLQ도 멈추면 critical: 이번에는 문의가 DLQ에도 없습니다. 그래서 등급이 critical이고, 메시지에 잃어버린 id(R-201 · R-203)를 적었습니다. 사람이 이 알림만 보고 보낸 쪽에 연락하거나 손으로 다시 넣을 수 있습니다.
lab02-nets7/7: 예제 3의 구멍(6 · 7번)이 막혔습니다.lab024/4: DLQ와 Error Workflow는 그대로입니다.
과제 2 · 알림 폭주 막기 (10분)
이 실습의 의도
한 줄로: 같은 원인의 high 알림은 10분에 한 번만 보내, 사람이 알림을 꺼 버리지 않게 합니다. 급한 것(critical)과 실패의 기록(DLQ)은 줄이지 않습니다.
생활 속 예 · 알림을 꺼 버린 단톡방. 같은 공지가 백 번 오면 사람들은 단톡방 알림을 끄고, 정말 중요한 공지도 못 봅니다. 그래도 대화 기록은 남아 있습니다. 과제 2는 같은 원인의 알림은 10분에 한 번만 보내되(알림 줄이기), DLQ(대화 기록)는 그대로 두고, 정말 급한 것(critical)은 늘 보내는 실습입니다.
상황. 과제 1로 "DLQ에 넣으면 알림"이 생겼습니다. 그런데 API가 30분 멈추면, 그동안 들어온 요청마다 알림이 하나씩 갑니다. 같은 원인을 백 번 알리는 셈이고, 받는 사람은 결국 알림을 끕니다. 그러면 정말 급한 알림도 못 봅니다 (알림 피로).
| 줄이는 것 | 줄이지 않는 것 |
|---|---|
10분 안에 같은 reason의 high 알림이 이미 있으면 보내지 않음 (alert: suppressed) |
DLQ에는 모두 남음 |
critical은 늘 보냄 (요청을 잃을 수 있으니) |
어떻게 아나. 알림을 보내기 바로 전에 데이터 API에 "10분 안에 같은 알림이 있었나"를 묻습니다 (/alerts/list의 within).
시험하는 것. 첫 실패 → 보냄, 10분 안에 또 실패 → 보내지 않음, 그래도 DLQ에는 모두, DLQ가 멈춘 critical은 그래도 보냄.
끝나면 알게 되는 것. 알림이 많은 것이 안전한 것은 아니라는 것, 운영 도구의 묶기 · 반복 간격 · 중복 없애기가 무엇을 하는지 (그 가장 작은 판), 줄이는 것은 알림이지 기록이 아니라는 것.
"어제 API가 30분 멈췄는데, 그동안 요청마다 알림이 와서 휴대폰이 백 번 울렸어요. 그다음부터는 다들 알림을 꺼 버렸고요. 같은 문제면 10분에 한 번만 알려 주세요. 단, 정말 급한 건(critical)은 늘 보내 주세요."
uv run tools/flow.py test cases/lab02-challenge2.json -v # 고치기 전
| 조건 | 값 |
|---|---|
| 같은 알림 | workflow: intake이고 같은 reason의 알림이 10분(600초) 안에 이미 있으면 |
| 그때 high는 | 보내지 않고 결과에 alert: "suppressed" |
| critical은 | 늘 보냄 |
| 10분 안의 알림 수 | POST /alerts/list 본문 {workflow, reason, within: 600} (AGENTS.md 2절) |
| 기대 결과 | cases/lab02-challenge2.json, lab02-challenge 그대로 |
이해하기
알림 피로 (alert fatigue). 알림이 너무 많으면 사람은 알림을 보지 않게 됩니다. 그러면 정말 급한 알림도 묻힙니다. 알림이 많다는 것은 안전하다는 뜻이 아니라, 아무도 보지 않는다는 뜻이 되기 쉽습니다.
같은 문제는 한 번, 대신 숨기지 않기.
| 하는 것 | 하지 않는 것 |
|---|---|
| 같은 문제의 high 알림은 10분에 한 번 | 실패를 DLQ에서 빼기 (DLQ에는 모두 남음) |
결과에 alert: suppressed (보내지 않은 것도 기록) |
critical 줄이기 (급한 것은 늘 보냄) |
운영 도구들(Prometheus Alertmanager · PagerDuty 등)은 이것을 묶기(grouping) · 반복 간격(repeat interval) · 중복 없애기(deduplication)라고 부릅니다. 오늘은 그 가장 작은 판입니다: "같은 이유의 알림이 10분 안에 있었나?"
flowchart LR
M["Make alert<br/>high · dlq"] --> L[("POST /alerts/list<br/>intake · dlq · within 600")] --> D{"high이고<br/>이미 있음?"}
D -->|예| S(["alert: suppressed"])
D -->|아니오| A[("POST /alerts")] --> O(["alert: sent"])
테스트 케이스.
| 케이스 | 하는 것 | 기대 |
|---|---|---|
| 첫 실패 | tickets error · batch-two |
alert: sent |
| 10분 안에 또 | batch-ok |
failed: 3, alert: suppressed |
| dlq 알림 수 | /alerts/list {reason: dlq} |
count: 1 |
| DLQ에는 모두 | /dlq/list |
size: 5 |
| critical은 늘 | dlq error · batch-bad-item |
dlq_saved: false, alert: sent |
| 고친 뒤 | batch-dup-id |
failed: 0, alert: none |
기대 결과. 고치기 전 4/6 (두 번째에도 보내서), 고친 뒤 6/6.
▶ 1단계 · 어디서 세나
알림을 보내기 바로 전에 데이터 API에 "10분 안에 같은 알림이 있나"를 묻습니다. curl -s -XPOST localhost:8800/alerts/list -d '{"workflow": "intake", "reason": "dlq", "within": 600}'로 모양을 봅니다.
▶ 2단계 · 왜 critical은 예외인가
DLQ가 멈춘 알림을 10분 동안 한 번만 보내면, 그 10분 동안 들어온 다른 요청의 id는 어디에 남나요? (critical 메시지에 id를 적은 까닭)
▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대
요청문에 꼭 들어갈 것: ① 묻는 곳과 본문 ② 보내지 않는 조건 ③ 예외 ④ 결과 값 ⑤ 기대 결과. 빈칸 4개를 채웁니다.
my-work/intake.json 의 Make alert 뒤에 Recent alerts(HTTP POST http://data-api:8800/alerts/list) 를 넣어
본문 {workflow: "intake", reason: 이번 알림의 reason, within: ____① } 로 10분 안의 같은 알림 수를 받아 줘.
Decide alert(Code): severity 가 ____② 이고 그 수가 1 이상이면 suppressed true. critical 은 ____③ .
Send it?(If) 로 suppressed 가 아닐 때만 Send alert, 결과의 alert 는 "sent" / "____④" / "none".
기대 결과 cases/lab02-challenge2.json, cases/lab02-challenge.json.
빈칸 힌트:
- ① 10분을 초로.
- ② 팀장이 줄이라고 한 등급입니다.
- ③ 팀장 말의 "단," 뒤입니다.
- ④ 위 조건 표의 값 그대로입니다.
uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab02-challenge2.json -v
uv run tools/flow.py test cases/lab02-challenge.json
uv run tools/flow.py test cases/lab01-challenge2.json
uv run tools/flow.py ops --reset
실행 결과 · 알림 폭주 막기
수업 준비 때(10/29) 강사 노트북에서 실제로 나온 출력입니다.
고치기 전 · 과제 1까지 고친 my-intake
실행한 명령:
uv run tools/flow.py test cases/lab02-challenge2.json -v
출력 (결과 줄만):
✔ 첫 실패 → 알림 보냄 · 2.9s
✘ 10분 안에 또 실패 → 보내지 않음 · 3.7s · alert = sent (expected suppressed)
✘ dlq 알림은 1개 · 0.0s · count = 2 (expected 1)
✔ 그래도 DLQ에는 5건 모두 · 0.0s
✔ DLQ가 멈춘 것은 늘 보냄 (critical) · 2.5s
✔ 고친 뒤 · 실패 없음 · 알림 없음 · 2.0s
Passed 4/6
왜 이렇게 나왔나 (고치기 전)
- 10분 안에 또 실패 ✘
alert = sent: 과제 1의 규칙은 "실패가 있으면 보낸다" 하나뿐입니다. 같은 장애가 이어져도 요청마다 알림을 또 보냅니다. 그래서 dlq 알림이 2개입니다. - 장애가 30분 이어지고 요청이 1분에 한 번 오면 같은 알림이 30개 쌓입니다. 사람은 곧 알림을 읽지 않게 되고(알림 피로), 정말 새로운 알림도 놓칩니다.
- ✔ 넷: DLQ에 5건 모두 들어간 것, DLQ가 멈춘 critical을 보낸 것, 실패가 없을 때 알림이 없는 것은 과제 1에서 이미 됩니다. 고친 뒤에도 그대로여야 합니다.
- 4/6은 예상한 결과입니다.
고친 뒤
실행한 명령:
uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab02-challenge2.json -v
uv run tools/flow.py test cases/lab02-challenge.json
uv run tools/flow.py test cases/lab01-challenge2.json
uv run tools/flow.py ops --reset
출력 (결과 줄만):
✔ 첫 실패 → 알림 보냄 · 4.5s
✔ 10분 안에 또 실패 → 보내지 않음 · 3.7s
✔ dlq 알림은 1개 · 0.0s
✔ 그래도 DLQ에는 5건 모두 · 0.0s
✔ DLQ가 멈춘 것은 늘 보냄 (critical) · 2.4s
✔ 고친 뒤 · 실패 없음 · 알림 없음 · 2.8s
Passed 6/6
(lab02-challenge 6/6 · lab01-challenge2 4/4)
고친 뒤 이렇게 나온 까닭
| 케이스 | 고치기 전 | 고친 뒤 | 까닭 |
|---|---|---|---|
| 첫 실패 | sent |
sent |
10분 안에 같은 이유의 알림이 없음 |
| 10분 안에 또 실패 | sent |
suppressed |
같은 이유(dlq)의 알림이 이미 있음 |
| dlq 알림 수 | 2 | 1 | 같은 까닭 |
| DLQ | 5건 | 5건 (그대로) | 줄인 것은 알림뿐 |
| DLQ가 멈춤 | critical sent |
critical sent |
급한 것은 줄이지 않음 |
- 보내기 전에 지난 10분 안에 같은 이유의 알림이 있었는지 보고, 있으면
suppressed로 정했습니다. 결과에suppressed라고 쓰니 보내지 않은 것도 기록에 남습니다. - DLQ에는 5건 모두 있습니다. 줄인 것은 사람에게 가는 알림이지, 실패의 기록이 아닙니다. 첫 알림을 본 사람이 DLQ를 열면 그 뒤에 쌓인 것까지 다 보입니다.
- DLQ가 멈춘 critical은 10분 안이라도
sent입니다. 문의를 잃고 있는 중이라, 줄이면 안 되는 알림입니다. - 마지막 케이스(
batch-dup-id)는failed: 0·alert: none입니다.lab01-challenge24/4로 Lab 01 과제 2의 같은 id 거절도 그대로입니다.
막혔을 때
| 증상 | 할 일 |
|---|---|
| DLQ가 비어 있음 | 실패가 있을 때만 가는 If의 true 쪽에 DLQ 노드가 있는지, 본문이 {items: [...]}인지 |
| 알림이 안 생김 | 에러 처리기를 push(게시)했는지, 파일의 id가 errHandler76001인지, 부르는 쪽 settings.errorWorkflow |
| 알림이 늦게 생김 | 정상. 응답이 나간 뒤에 돈다 (테스트는 {"sleep": 3}) |
| 알림이 두 개 | 테스트의 url 케이스가 /alerts에 POST하면 알림이 생김 — 읽기는 /alerts/list · /alerts/latest |
dlq_saved가 늘 true |
DLQ 응답의 stored를 보는지 (상태 코드는 neverError라 늘 성공처럼 보임) |
늘 suppressed |
앞 테스트의 알림이 남았는지 (ops --reset), within이 초 단위인지 |
fault dlq error 뒤 다른 테스트가 이상함 |
uv run tools/flow.py fault dlq ok 또는 ops --reset |
| 계속 안 됨 | reference/lab02-example1-dlq.json, reference/lab02-example2-*.json (예제까지) |
확인
- 예제 1:
cases/lab02.json앞 두 케이스,ops로 DLQ 확인. - 예제 2:
cases/lab02.json4/4,nightly-report의 알림을 봤다. - 예제 3:
cases/lab02-nets.json5/7, 그물 지도와 구멍을RUNBOOK.md에 적었다. - 과제 1:
cases/lab02-challenge.json6/6,lab02-nets7/7,lab02도 통과. - 과제 2:
cases/lab02-challenge2.json6/6,lab02-challenge도 통과. RUNBOOK.md1절 (알아채는 법 · 자동으로 하는 일 · 알림 등급).