처음에는 요청 하나가 단순하다.
사용자 요청
→ API
→ DB 저장
→ 응답
문제가 생기면 서버 로그만 확인해도 어느 정도 원인을 찾을 수 있다.
하지만 시스템에 자동화가 붙기 시작하면 흐름이 달라진다.
관리자 주문 상태 변경
→ API
→ DB Transaction
→ Event 생성
→ Queue
→ Worker
→ 알림톡 API
→ Audit Log
→ Webhook
→ Webhook Worker
→ DB 상태 변경
여기에 AI 자동화까지 붙는다면:
Git Push
→ Automation Trigger
→ AI Task
→ AI Run
→ 코드 분석
→ 코드 수정
→ Test
→ Review
→ Report 생성
→ Notion 업로드
어느 한 단계에서 문제가 발생했을 때 단순히
Error occurred
라는 로그만 있어서는 원인을 찾기 어렵다.
그래서 필요한 것이 Observability다.
Monitoring은 보통
CPU 사용률
Memory 사용량
HTTP Error Rate
응답 시간
Queue 길이
등 이미 알고 있는 지표를 감시한다.
반면 Observability는
문제가 발생했을 때 시스템 내부에서 무슨 일이 있었는지 외부 데이터만으로 추적할 수 있는 능력
에 가깝다.
즉,
Monitoring
→ 문제가 있다는 사실을 발견
Observability
→ 왜 문제가 발생했는지 추적
이라고 생각하면 된다.
전통적으로 다음 세 가지를 많이 본다.
Logs
Metrics
Traces
개별 사건 기록이다.
NotificationJob #1920 발송 시작
Notification API timeout
Retry scheduled
숫자로 집계되는 운영 상태다.
API Error Rate = 2.3%
Queue Pending = 38
Notification Failure = 4
한 요청이 여러 시스템을 지나가는 전체 흐름이다.
Request
→ API
→ DB
→ Queue
→ Worker
→ External API
현재 투게더몰처럼 기능이 점점 연결되는 구조에서는 세 가지 모두 필요하다.
다음과 같은 로그를 많이 남겨도 실질적인 도움이 적다.
Request received
Processing...
Processing...
API called
API success
Done
운영 중 정말 필요한 것은
어떤 요청인가?
어떤 주문인가?
어떤 사용자가 실행했는가?
어떤 Worker가 처리했는가?
어떤 외부 요청과 연결되는가?
같은 작업의 이전 Retry는 무엇인가?
최종 결과가 무엇인가?
이다.
따라서 로그의 양보다 연결 가능성이 중요하다.
Correlation ID는 여러 시스템에 흩어진 로그를 하나의 흐름으로 묶기 위한 ID다.
예를 들어 관리자에서 주문 상태를 변경한다.
PATCH /admin/orders/321/status
이 요청에 다음 ID를 부여한다.
correlationId = cor_7f31a91
그 이후 발생하는 모든 작업에 같은 ID를 전달한다.
HTTP Request
cor_7f31a91
↓
Order Update
cor_7f31a91
↓
Domain Event
cor_7f31a91
↓
NotificationJob
cor_7f31a91
↓
Worker
cor_7f31a91
↓
Alimtalk API
cor_7f31a91
그러면 운영 중
cor_7f31a91
만 검색해도 요청 전체 흐름을 찾을 수 있다.
둘을 같은 개념으로 사용할 수도 있지만 규모가 커지면 구분하는 것이 편하다.
Request ID
= 하나의 HTTP Request 식별
Correlation ID
= 여러 Request / Job / Event를 묶는 전체 흐름 식별
예를 들어:
Correlation ID
cor_order_192
├─ Request ID
│ req_001
│
├─ Queue Job
│ job_813
│
├─ External Request
│ ext_392
│
└─ Webhook Request
req_822
Webhook은 몇 초 뒤 다른 HTTP 요청으로 들어오더라도 같은 업무 흐름으로 연결할 수 있다.
OpenTelemetry 같은 분산 추적 시스템에서는 흔히 다음 개념을 사용한다.
Trace ID
Span ID
Trace는 하나의 전체 요청 흐름이다.
Trace
├─ HTTP Request
├─ Prisma Query
├─ Queue Publish
├─ Worker
└─ External API
각각의 작은 작업이 Span이다.
Trace ID
abc123
Span
HTTP Request
Span
DB Update
Span
Notification Worker
Span
External API
소규모 프로젝트에서는 처음부터 완전한 Distributed Tracing까지 구축할 필요는 없다.
현재 단계에서는 우선
Correlation ID
Request ID
Job ID
만 제대로 연결해도 상당한 효과가 있다.
운영 로그에서 다음 정도를 일관되게 가지고 있으면 좋다.
requestId
correlationId
userId / adminId
orderId
consultId
jobId
eventId
runId
externalRequestId
단 개인정보는 직접 로그에 남기지 않는 것이 중요하다.
예:
phone = 01012345678
보다는
orderId = order_123
을 기준으로 추적한다.
편하다고 이런 로그를 남기기 쉽다.
김동준 010-1234-5678 알림톡 발송
하지만 운영 로그는 생각보다 많은 곳에 복제될 수 있다.
CloudWatch
Local Log
CI Log
Error Tracking
Backup
AI 분석 시스템
따라서 고객 개인정보 자체보다 내부 ID를 활용해야 한다.
Bad
name = 홍길동
phone = 01012345678
Better
customerId = cus_921
orderId = order_4421
필요하면 관리자 시스템에서 ID로 고객을 조회한다.
다음 로그보다
알림톡 발송 실패했습니다.
이런 구조가 낫다.
{
"level": "error",
"event": "notification.send.failed",
"correlationId": "cor_123",
"jobId": "job_392",
"orderId": "order_1022",
"provider": "kakao",
"errorCode": "PROVIDER_TIMEOUT",
"attempt": 2
}
이를 Structured Logging이라고 한다.
사람만 읽기 위한 문장이 아니라 프로그램으로 검색·집계 가능한 데이터 형태다.
예를 들어:
order.created
order.status.changed
notification.queued
notification.send.started
notification.send.succeeded
notification.send.failed
export.started
export.completed
export.failed
webhook.received
webhook.processed
webhook.failed
ai.run.started
ai.run.completed
ai.run.failed
이렇게 하면 로그 검색이 쉬워진다.
예:
event = notification.send.failed
만 검색해서 전체 알림톡 장애를 확인할 수 있다.
좋지 않은 예:
logger.error('알림톡 발송 실패');
조금 더 나은 예:
logger.error({
event: 'notification.send.failed',
jobId: job.id,
correlationId: job.correlationId,
provider: job.provider,
errorCode: error.code,
});
운영 로그에서는 문장보다 Context가 더 중요하다.
Middleware나 Interceptor를 통해 요청마다 ID를 생성할 수 있다.
개념적으로는:
request.requestId =
request.headers['x-request-id']
?? generateRequestId();
그리고 응답에도 반환한다.
X-Request-Id: req_f3912
사용자가 오류를 문의할 때도
오류 번호 req_f3912
형태로 활용할 수 있다.
외부에서 이미 Correlation ID가 넘어왔다면 유지하고,
없다면 새로 만든다.
const correlationId =
request.headers['x-correlation-id']
?? generateCorrelationId();
이 값을
Service
Repository
Event
Queue
Worker
로 계속 전달한다.
처음에는 이렇게 구현하기 쉽다.
service.updateOrder(
orderId,
status,
correlationId,
);
그리고 내부 함수도 계속:
repository.update(
data,
correlationId,
);
이 방식은 금방 번거로워진다.
그래서 Request Context 같은 개념을 사용할 수 있다.
RequestContext
requestId
correlationId
actorId
서비스는 필요할 때 Context를 읽는다.
Node.js에는 AsyncLocalStorage가 있다.
이를 이용하면 하나의 비동기 실행 흐름 안에서 Context를 유지할 수 있다.
개념:
{
requestId: 'req_123',
correlationId: 'cor_123',
actorId: 'admin_3'
}
이후 Service에서 굳이 모든 함수에 인자로 넘기지 않아도 Logger가 Context를 읽을 수 있다.
HTTP 요청이 Queue에 Job을 등록하면 실행 흐름이 끊긴다.
HTTP Request
↓
Queue Insert
----------------
다른 Worker Process
↓
Job 실행
AsyncLocalStorage는 프로세스를 넘어 전달되지 않는다.
그래서 Job Payload에 직접 넣어야 한다.
{
"jobId": "job_192",
"correlationId": "cor_9182",
"orderId": "order_299"
}
Worker가 시작할 때 이 값을 Context로 복원한다.
예:
{
eventId: 'evt_123',
type: 'ORDER_STATUS_CHANGED',
aggregateId: 'order_921',
correlationId: 'cor_882',
causationId: 'req_991'
}
여기서 Causation ID라는 개념도 활용할 수 있다.
Correlation은 같은 흐름이라는 뜻이다.
Causation은
무엇 때문에 이 이벤트가 발생했는가?
를 나타낸다.
예:
Request
req_1
↓ 발생
OrderStatusChanged
evt_1
↓ 발생
NotificationQueued
evt_2
각각:
evt_1.causationId = req_1
evt_2.causationId = evt_1
그리고 모두:
correlationId = cor_1
이다.
이 구조를 가지면 이벤트 흐름을 거의 트리처럼 복원할 수 있다.
관리자가 주문 상태를 변경한다.
correlationId
cor_20260921_001
로그:
order.status.change.requested
↓
order.status.changed
↓
notification.queued
↓
notification.send.started
↓
notification.send.succeeded
모든 이벤트에 동일한 correlationId가 있다.
운영자는 한 번의 검색으로 전체 과정을 볼 수 있다.
외부 Provider가 Webhook을 보낸다.
Webhook HTTP 요청 자체는 새 Request이므로:
requestId = req_webhook_883
를 가진다.
하지만 처음 발송한 메시지와 연결하기 위해:
providerMessageId
를 조회한다.
DB에서 NotificationJob을 찾고,
correlationId = cor_20260921_001
를 다시 가져온다.
결과적으로:
원본 요청
→ 알림톡 발송
→ 외부 Provider
→ Webhook
→ 상태 반영
전체가 하나의 Correlation으로 연결된다.
둘을 하나로 생각하면 안 된다.
Application Log는:
시스템이 어떻게 동작했는가?
를 본다.
Audit Log는:
누가 무엇을 변경했는가?
를 본다.
예:
Application Log
Notification API Timeout
Retry scheduled
반면:
Audit Log
관리자 admin_3
주문 order_921
상태 WAITING → COMPLETED
둘은 함께 연결되어야 하지만 목적은 다르다.
{
actorId: 'admin_3',
action: 'ORDER_STATUS_CHANGE',
targetId: 'order_921',
before: 'WAITING',
after: 'COMPLETED',
correlationId: 'cor_123'
}
그럼 관리자 행동 이후 어떤 자동화들이 발생했는지 추적할 수 있다.
예:
HTTP 요청 수
HTTP Error Rate
평균 응답 시간
Queue Pending Count
Worker 처리량
Retry 횟수
UNKNOWN Job 수
DLQ 수
Webhook 실패 건수
Notification 실패율
이런 값은 개별 로그보다 시스템 전체 상태를 보는 데 유용하다.
Metrics는 대표적으로 이런 형태가 있다.
계속 증가하는 값.
notification_sent_total
notification_failed_total
http_requests_total
현재 값.
queue_pending_jobs
worker_active_count
unknown_jobs
값의 분포.
http_request_duration
notification_send_duration
export_duration
성공률만 보는 것보다 실패율이 중요하다.
Error Rate
=
Failed Requests
/
Total Requests
예:
오늘 알림톡
발송 시도 1,000
실패 15
Error Rate = 1.5%
평소 실패율이 0.1%였는데 갑자기 5%가 되면 장애 가능성이 높다.
Worker 자동화에서는 Queue 크기만 보면 부족하다.
예:
Pending Job = 300
이라도 Worker가 매우 빠르게 처리하고 있다면 문제가 아닐 수 있다.
중요한 것은 가장 오래 기다린 Job이다.
Oldest Job Age
예:
Pending = 300
Oldest Age = 2 seconds
이면 정상일 수 있다.
하지만
Pending = 20
Oldest Age = 40 minutes
이면 Worker가 멈췄을 가능성이 있다.
실패율은 낮지만 Retry가 급증할 수도 있다.
Success 99.9%
여도 내부적으로
평균 Retry 4회
를 거치고 있다면 이미 Provider나 Network에 문제가 생기고 있는 것이다.
따라서:
retry_total
retry_success_total
retry_exhausted_total
등을 보면 좋다.
0917에서 만든 Reconciliation에도 지표가 필요하다.
reconciliation_total
reconciliation_recovered_total
reconciliation_failed_total
manual_required_total
unknown_jobs
특히
UNKNOWN 증가
는 시스템이 외부 상태를 확실하게 확인하지 못하고 있다는 신호다.
보통 다음 정도를 사용한다.
DEBUG
INFO
WARN
ERROR
개발 중 상세 정보.
정상적인 주요 흐름.
notification.send.started
notification.send.succeeded
시스템은 동작하지만 이상 징후.
retry scheduled
stale job detected
provider response slow
실제 처리 실패.
notification send failed
database transaction failed
운영 환경에서 DEBUG 로그를 과도하게 남기면 비용과 노이즈가 크게 늘어난다.
예를 들어 페이지 조회마다:
GET /products success
GET /products success
GET /products success
를 저장하면 로그가 너무 많아진다.
반면 중요 업무는 성공 로그도 가치가 있다.
주문 생성
상태 변경
알림톡 발송
Excel Export
Webhook 처리
AI Run
배포
즉 비즈니스 중요도에 따라 다르게 적용한다.
예:
TypeError: Cannot read property...
만으로는 부족하다.
최소한:
correlationId
requestId
jobId
event
errorCode
stack
attempt
service
정도는 같이 보는 것이 좋다.
문자열 메시지만 사용하면 관리하기 어렵다.
예:
NOTIFICATION_PROVIDER_TIMEOUT
NOTIFICATION_TEMPLATE_INVALID
EXPORT_S3_UPLOAD_FAILED
WEBHOOK_SIGNATURE_INVALID
AI_TOOL_TIMEOUT
AI_POLICY_DENIED
이런 식으로 Error Code를 정의하면 통계도 낼 수 있다.
이번 달 가장 많이 발생한 오류
1. PROVIDER_TIMEOUT 93회
2. RATE_LIMIT 31회
3. INVALID_TEMPLATE 7회
errorCode
= 프로그램이 판단하기 위한 값
errorMessage
= 사람이 읽는 설명
예:
{
"errorCode": "NOTIFICATION_PROVIDER_TIMEOUT",
"errorMessage": "알림톡 Provider 응답 시간이 초과되었습니다."
}
Error Message 문구가 바뀌어도 Error Code는 유지한다.
외부 API 문제는 특히 추적이 어렵다.
최소한 다음 정도를 남긴다.
provider
endpoint 이름
requestId
externalRequestId
statusCode
duration
result
단 전체 Request Body를 그대로 로그에 남기면 안 된다.
개인정보와 Secret이 포함될 수 있기 때문이다.
{
"event": "external_api.completed",
"provider": "kakao",
"operation": "send_alimtalk",
"correlationId": "cor_123",
"jobId": "job_391",
"externalRequestId": "kakao_8221",
"statusCode": 200,
"durationMs": 382
}
실제 전화번호나 메시지 전체 내용은 로그에서 제외한다.
Error가 아니더라도 지나치게 느린 요청은 문제다.
예:
관리자 주문 검색
평균 120ms
특정 요청
4.8s
이런 요청은 별도 WARN으로 남길 수 있다.
{
"event": "http.slow_request",
"path": "/admin/orders",
"durationMs": 4821,
"correlationId": "cor_123"
}
이후 DB Slow Query와 연결한다.
개발환경에서는 Query 로그가 편하다.
하지만 운영 환경에서 모든 SQL과 parameter를 남기면:
로그 폭증
개인정보 노출
비용 증가
문제가 생긴다.
운영에서는 주로:
Slow Query
Query Type
Model
Duration
중심으로 관찰하는 것이 낫다.
AI 자동화가 들어가면 일반 API보다 훨씬 추적이 중요하다.
한 Run이:
Task 생성
Prompt 구성
Context 조회
Model 호출
Tool 실행
File 수정
Test
Review
Commit
Report
등 여러 단계를 거치기 때문이다.
예:
taskId
runId
attemptId
stepId
correlationId
model
promptVersion
policyVersion
toolName
startedAt
completedAt
duration
status
단 Prompt 전체를 무조건 저장하지 않는 것이 좋다.
회사 코드나 개인정보가 포함될 수 있기 때문이다.
Prompt 자체 대신 Hash를 저장하는 방법도 있다.
promptHash
=
sha256(prompt)
같은 Prompt인지 비교할 수 있으면서 전체 내용은 로그에 노출하지 않는다.
필요한 경우 별도 보안 저장소에 원본을 저장한다.
AI가 Tool을 실행할 때:
tool.git.diff
tool.test.run
tool.file.write
tool.github.create_pr
처럼 이벤트를 남길 수 있다.
예:
{
"event": "ai.tool.completed",
"runId": "run_123",
"tool": "test.run",
"durationMs": 18392,
"status": "SUCCESS"
}
LLM API를 사용한다면:
inputTokens
outputTokens
totalTokens
estimatedCost
를 Run 단위로 기록하는 것이 좋다.
그러면 나중에:
AI Report 자동화 월 비용
평균 Run 비용
가장 비용 많이 쓰는 Workflow
등을 확인할 수 있다.
로컬 LLM이라면 비용 대신:
executionTime
modelLoadTime
GPU / RAM 사용량
등을 볼 수 있다.
AI 자동화 실패를 모두
AI_ERROR
로 처리하면 의미가 없다.
예:
MODEL_TIMEOUT
TOOL_ERROR
TEST_FAILED
POLICY_DENIED
CONTEXT_TOO_LARGE
INVALID_OUTPUT
GIT_CONFLICT
APPROVAL_REQUIRED
등으로 나누는 것이 낫다.
이 분류는 Retry 정책과도 직접 연결된다.
현재 규모에서는 Grafana 대시보드 수십 개를 만들 필요가 없다.
우선 핵심만 보면 된다.
오늘 API Error Rate
평균 API Response Time
Queue Pending
Oldest Job Age
Notification 실패
Webhook 실패
Retry
UNKNOWN
DLQ
AI Run 실패
이 정도만 봐도 상당한 운영 정보를 얻을 수 있다.
별도 모니터링 툴뿐 아니라 기존 관리자에 내부 운영 페이지를 만들 수 있다.
예:
운영 현황
API
정상
Queue
Pending 8
알림톡
오늘 성공 821
실패 3
Webhook
실패 0
DLQ
1
AI Automation
실행 중 1
실패 0
1인 개발 환경에서는 오히려 이런 화면이 실용적일 수 있다.
문제가 발생했을 때 Correlation ID 하나를 기준으로 이런 화면을 보여줄 수 있다.
Correlation
cor_20260921_a921
Timeline
09:31:02
관리자 주문 상태 변경
09:31:02
ORDER_STATUS_CHANGED
09:31:02
NotificationJob 생성
09:31:03
Worker 처리 시작
09:31:08
Provider Timeout
09:31:08
상태 UNKNOWN
09:32:01
Reconciliation 시작
09:32:02
Provider 결과 DELIVERED 확인
09:32:02
NotificationJob SENT 복구
이 정도면 장애 분석 속도가 크게 빨라진다.
Audit Log와 Automation Log를 Timeline으로 묶으면 운영성이 좋아진다.
[09:31:02] ADMIN
주문 상태 변경
[09:31:02] SYSTEM
Notification Job 생성
[09:31:03] WORKER
발송 요청
[09:31:08] SYSTEM
Provider Timeout
[09:32:01] RECONCILER
외부 상태 조회
[09:32:02] SYSTEM
발송 완료 확인
고객 문의 대응에도 꽤 유용하다.
에러 한 건마다 Slack이나 알림을 보내면 금방 무시하게 된다.
이를 Alert Fatigue라고 한다.
예:
알림톡 1건 실패
→ 즉시 긴급 알림
보다는:
5분간 실패율 10% 이상
또는
UNKNOWN 20건 이상
또는
Oldest Queue Job > 10분
같은 기준이 낫다.
심각도를 나눠두는 것도 좋다.
INFO
WARNING
CRITICAL
예:
INFO
Retry 발생
WARNING
알림톡 실패율 증가
CRITICAL
Worker 중단
전체 주문 API 장애
Production Health Check 실패
특히 나중에 워홀이나 원격 근무를 생각한다면 운영 자동화가 늘어날 가능성이 높다.
그때 모든 오류가 긴급 알림으로 오면 운영이 불가능하다.
Critical 기준은:
즉시 대응하지 않으면
고객 영향이 커지거나
데이터 손실 위험이 있거나
매출에 직접 영향이 있는가?
정도로 좁히는 것이 좋다.
운영이 더 성숙하면 SLI/SLO 개념을 사용할 수 있다.
SLI:
실제 측정 값
예:
주문 API 성공률
99.7%
SLO:
목표 수준
예:
주문 API 성공률
99.9% 이상
현재 규모에서 SLA 계약까지 갈 필요는 없지만 내부 운영 기준을 만드는 데 도움이 된다.
기술 Metric만 정상이라고 서비스가 정상인 것은 아니다.
예:
CPU 정상
Memory 정상
HTTP 200 정상
인데 주문 생성 수가 갑자기 0건일 수도 있다.
따라서:
시간당 주문 수
신청 완료율
알림톡 발송 건수
상담 등록 건수
결제 성공 건수
같은 Business Metric도 중요하다.
가장 위험한 장애 중 하나다.
시스템은 Error를 발생시키지 않지만 실제 기능은 동작하지 않는다.
예:
Webhook HTTP 200
하지만 DB Update 없음
또는:
Worker 정상 실행
하지만 NotificationJob 생성 자체가 안 됨
이 경우 Error Log만 보고 있으면 발견하기 어렵다.
그래서 Expected Metric이 필요하다.
주문 완료 100건
그런데 완료 알림톡 생성 0건
→ 이상
비즈니스 규칙 자체를 모니터링할 수도 있다.
예:
COMPLETED 주문인데
완료 NotificationJob이 없음
ExportJob SUCCESS인데
파일 URL이 없음
Webhook PROCESSED인데
관련 상태 변경 기록이 없음
이런 조건을 주기적으로 검사할 수 있다.
이는 Reconciliation과도 연결된다.
0921 핵심 연결점이다.
Observability는 문제를 발견한다.
UNKNOWN 증가
Queue Lag 증가
Webhook Error 증가
Reconciliation은 이를 복구한다.
실제 상태 조회
Drift 확인
안전한 Repair
즉:
Observe
↓
Detect
↓
Diagnose
↓
Reconcile
↓
Recover
흐름으로 연결된다.
0916에서 다룬 Idempotency 역시 중요하다.
로그를 통해:
같은 idempotencyKey가
몇 번 실행됐는지
확인할 수 있어야 한다.
예:
idempotencyKey
notification:order:921:completed
Attempt 1
Timeout
Attempt 2
Existing Result Found
Final
SUCCESS
그러면 중복 실행 방지 여부까지 추적할 수 있다.
AI를 바로 Production 복구 권한에 연결하기보다는 먼저 분석 용도로 쓰는 것이 좋다.
예:
지난 1시간 ERROR 로그 요약
반복 Error Code 묶기
Correlation Timeline 생성
가장 빈번한 실패 원인 분석
관련 Runbook 추천
이런 Read-Only 분석은 위험이 낮다.
현재 사용 중인 로컬 LLM 자동화에 운영 로그 요약을 연결할 수도 있다.
예:
오늘 Git 작업
+
배포 기록
+
Error Summary
+
Incident
+
Resolved Issue
를 모아서 일일 보고서에:
오늘 운영 이슈
- 알림톡 Timeout 3건
- 자동 Retry 3건
- 최종 실패 0건
- Webhook 오류 없음
같이 넣는 방식이다.
로그에는 다음 정보가 섞일 수 있다.
고객정보
Secret
JWT
Authorization Header
API Key
Cookie
Request Body
따라서 LLM에 전달하기 전 Redaction이 필요하다.
예:
Authorization: [REDACTED]
phone: [REDACTED]
email: [REDACTED]
서비스마다 제각각 로그를 남기지 않도록 공통 Logger를 사용하는 것이 좋다.
예:
logger.info({
event: 'order.status.changed',
orderId,
});
Logger가 자동으로:
timestamp
service
environment
requestId
correlationId
actorId
를 붙인다.
예:
interface LogContext {
requestId?: string;
correlationId?: string;
actorId?: string;
jobId?: string;
eventId?: string;
orderId?: string;
consultId?: string;
runId?: string;
}
도메인 ID만 추가하고 개인정보는 제외한다.
src/
├─ common/
│ ├─ logging/
│ │ ├─ logger.service.ts
│ │ ├─ request-context.service.ts
│ │ └─ logging.interceptor.ts
│ │
│ ├─ tracing/
│ │ ├─ correlation-id.ts
│ │ └─ trace-context.ts
│ │
│ └─ errors/
│ └─ error-code.ts
│
├─ automation/
│ └─ ...
│
└─ audit/
└─ ...
export const ErrorCode = {
NOTIFICATION_PROVIDER_TIMEOUT:
'NOTIFICATION_PROVIDER_TIMEOUT',
NOTIFICATION_TEMPLATE_INVALID:
'NOTIFICATION_TEMPLATE_INVALID',
EXPORT_UPLOAD_FAILED:
'EXPORT_UPLOAD_FAILED',
WEBHOOK_SIGNATURE_INVALID:
'WEBHOOK_SIGNATURE_INVALID',
AI_TOOL_TIMEOUT:
'AI_TOOL_TIMEOUT',
} as const;
이런 형태로 중앙 관리할 수 있다.
automation.job.claimed
↓
automation.job.started
↓
external_api.started
↓
external_api.failed
↓
automation.retry.scheduled
↓
automation.job.released
모든 이벤트가 동일한:
jobId
correlationId
idempotencyKey
를 가진다.
ai.run.created
↓
ai.step.started
ANALYSE
↓
ai.step.completed
↓
ai.step.started
MODIFY
↓
ai.tool.started
file.write
↓
ai.tool.completed
↓
ai.step.started
TEST
↓
ai.test.failed
↓
ai.run.failed
여기서 runId 하나만 검색하면 실행 전체를 재구성할 수 있다.
로그는 무한 보관할 필요가 없다.
예를 들어:
DEBUG
짧게
Application Log
중간 기간
Audit Log
상대적으로 길게
처럼 목적에 따라 분리할 수 있다.
특히 개인정보가 포함될 가능성이 있는 로그는 최소화가 우선이다.
지금 바로 모든 Observability 기술을 적용할 필요는 없다.
Request ID
Correlation ID
Structured Logging
Error Code
Queue Job에 Correlation ID 전달
Webhook 연결
Audit Log 연결
Worker Retry 로그
기본 Metrics
Error Rate
Queue Lag
UNKNOWN
DLQ
Incident Timeline
AI Run Trace
Distributed Trace
Alert 자동화
이 순서가 현실적이다.
먼저 이 다섯 가지 정도만 적용해도 된다.
1.
모든 HTTP Request에 requestId
2.
주요 업무 흐름에 correlationId
3.
JSON Structured Log
4.
Error Code 표준화
5.
Queue / Worker / Webhook에 correlationId 전달
이 정도만 해도 장애 추적 난이도가 크게 낮아진다.
현재 NestJS + Prisma 기반 프로젝트에
운영 추적을 위한 기본 Observability 구조를 추가해줘.
과도한 APM이나 복잡한 Distributed Tracing 시스템을
처음부터 도입하지 말고,
현재 1인 개발 환경에서 유지할 수 있는 수준으로 구현한다.
목표는 HTTP Request부터 Event, Queue, Worker,
Webhook, 외부 API 호출까지 하나의 업무 흐름을
Correlation ID로 추적할 수 있도록 만드는 것이다.
요구사항:
1. 모든 HTTP Request에 requestId를 생성한다.
2. 요청 Header에 x-request-id가 있다면 사용할 수 있도록 하되,
없다면 서버에서 생성한다.
3. correlationId도 동일하게 관리한다.
- x-correlation-id가 있으면 유지
- 없으면 새로 생성
4. Request Context 구조를 만든다.
최소 필드:
- requestId
- correlationId
- actorId
5. Node AsyncLocalStorage 또는 적절한 방식으로
같은 HTTP 요청 내에서 Context를 접근할 수 있게 한다.
6. 공통 Logger를 만든다.
Logger는 자동으로 다음 데이터를 붙인다.
- timestamp
- level
- environment
- requestId
- correlationId
- actorId
7. 문자열 로그보다 JSON Structured Logging을 기본으로 한다.
8. event 필드를 사용해 로그 이름을 표준화한다.
예:
- order.status.changed
- notification.queued
- notification.send.started
- notification.send.succeeded
- notification.send.failed
- webhook.received
- webhook.failed
9. Error Code를 공통 정의한다.
예:
- NOTIFICATION_PROVIDER_TIMEOUT
- EXPORT_UPLOAD_FAILED
- WEBHOOK_SIGNATURE_INVALID
10. Queue Job Payload에 correlationId를 저장한다.
11. Worker 실행 시 Job의 correlationId를
Worker Context로 복원한다.
12. Domain Event에도 다음 필드를 지원한다.
- eventId
- correlationId
- causationId
13. Webhook 처리 시 providerMessageId 등으로
원래 Job을 찾을 수 있다면
원본 correlationId를 이어서 사용한다.
14. Audit Log에도 correlationId를 저장한다.
15. 외부 API 호출 로그에서는
다음만 기록한다.
- provider
- operation
- statusCode
- durationMs
- externalRequestId
Request Body, Authorization Header,
전화번호, 이메일 등 민감 정보는 로그에 기록하지 않는다.
16. Worker 실행에서 다음 이벤트를 기록한다.
- job.claimed
- job.started
- job.completed
- job.failed
- retry.scheduled
17. 동일 correlationId로
HTTP → Event → Queue → Worker 흐름을 검색할 수 있도록 한다.
18. 기존 비즈니스 로직을 크게 변경하지 않는다.
19. 테스트를 작성한다.
- requestId 자동 생성
- 기존 requestId 유지
- correlationId 자동 생성
- Queue Job correlationId 전달
- Worker Context 복원
- 민감 정보 로그 미포함
자동화가 적을 때는
로그 확인
→ 오류 발견
정도로 충분하다.
하지만 시스템이
API
→ Event
→ Queue
→ Worker
→ 외부 API
→ Webhook
→ Reconciliation
→ AI Automation
처럼 연결되기 시작하면 단순 로그만으로는 원인을 추적하기 어렵다.
그래서 중요한 것이 Observability다.
핵심 세 가지는:
Logs
Metrics
Traces
그리고 현재 프로젝트에서 가장 먼저 도입할 것은 복잡한 Distributed Tracing이 아니라:
Request ID
+
Correlation ID
+
Structured Logging
이다.
Request ID는 하나의 HTTP 요청을 추적하고,
Correlation ID는:
HTTP
→ Event
→ Queue
→ Worker
→ Webhook
처럼 여러 실행에 걸친 하나의 업무 흐름을 연결한다.
또한 로그에는:
고객 이름
전화번호
이메일
Authorization Header
API Key
같은 민감 정보를 남기지 않고,
orderId
jobId
eventId
runId
correlationId
같은 내부 ID를 중심으로 추적해야 한다.
운영 자동화가 성숙하면 흐름은 결국 다음처럼 연결된다.
Observe
↓
Detect
↓
Trace
↓
Diagnose
↓
Reconcile
↓
Recover
특히 AI 자동화까지 붙게 되면
Task
→ Run
→ Step
→ Tool
마다 ID와 상태를 남겨야 실패 지점을 정확히 찾고 안전하게 Resume할 수 있다.
결국 좋은 Observability의 목표는
로그를 많이 남기는 것이 아니라, 문제가 생겼을 때 하나의 ID만으로 처음부터 끝까지 무슨 일이 있었는지 재구성할 수 있게 만드는 것
이다.