장애가 발생했다고 무조건 로그 검색창부터 열 필요는 없다. 서비스 전체에서 오류가 얼마나 퍼졌는지 모른다면 메트릭이 먼저다. 특정 요청이 여러 서비스를 지나며 실패했다면 트레이스가 더 빠르다. 예외가 발생할 때 애플리케이션이 어떤 상태였는지 확인하려면 로그가 필요하고 CPU나 메모리를 실제로 소비한 함수가 무엇인지 찾으려면 프로파일이 답에 가깝다.
따라서 첫 번째로 볼 Signal은 “우리 도구가 무엇을 잘 보여주는가”가 아니라 “현재 증상이 어떤 형태의 실패인가”로 정해야 한다.
이 글은 무엇을 사용자 실패로 측정할지 다시 정의하지 않는다. 그 문제는 사용자 여정과 SLI 설계에서 먼저 결정해야 한다. 여기서는 실패가 감지된 다음, 로그·메트릭·트레이스·프로파일 중 어디에서 조사를 시작하고 어느 조건에서 다음 Signal로 넘어갈지를 다룬다.
네 Signal이 현재 같은 성숙도에 있는 것은 아니다
OpenTelemetry의 2026년 8월 현재 상태를 보면 Traces는 API·SDK·OTLP가 모두 Stable이다. Metrics는 Signal과 API·데이터 모델·OTLP는 Stable이지만 전체 SDK와 언어별 구현 상태가 섞여 있다. Logs도 Signal·Bridge API·SDK·OTLP는 Stable이지만 언어별 Appender와 구현 범위는 다르다.
Profiles는 다르다. 2026년 3월 Public Alpha에 들어갔고 Collector 및 eBPF Profiling Agent와의 연계가 진전됐지만 Concepts 문서는 여전히 Alpha로 표시하고 OTLP Profiles는 Development 상태다. OpenTelemetry의 Alpha 경고에 따르면 Critical Production Workload의 필수 의존성으로 전제해서는 안 된다. 프로파일링 기법 자체가 새롭다는 뜻은 아니며 OpenTelemetry 규격과 생태계의 상호운용 지원이 아직 성숙 과정에 있다는 뜻이다.
이 차이는 Signal 선택에 영향을 준다. 프로파일이 가장 잘 답하는 질문이 있더라도 현재 Runtime과 Backend가 필요한 Profile Type과 Correlation을 지원하지 않는다면, 기존 언어 Profiler나 다른 진단 수단을 함께 고려해야 한다.
각 Signal이 잘 답하는 질문과 잘 답하지 못하는 질문
| Signal | 잘 답하는 질문 | 잘 답하지 못하는 질문 |
|---|---|---|
| Metrics | 언제부터 이상했는가? 얼마나 많은 요청·인스턴스·사용자가 영향을 받는가? 오류율·지연 분포·Queue Depth·CPU·메모리가 어떻게 변했는가? | 특정 요청이 왜 실패했는가? 어느 함수가 CPU를 소비했는가? 실패 당시 정확한 업무 상태는 무엇이었는가? |
| Logs | 어떤 사건이 발생했는가? 어떤 예외·입력 분류·상태 전이·재시도 결과가 기록됐는가? | 문제가 전체의 몇 퍼센트에 해당하는가? 여러 서비스의 인과 경로는 무엇인가? 기록되지 않은 사건이 실제로 일어나지 않았다고 증명할 수 있는가? |
| Traces | 한 요청이나 작업이 어떤 서비스·DB·외부 API를 거쳤는가? 어느 Span에서 실패하거나 시간이 늘어났는가? 비동기 작업과 원 요청은 어떻게 연결되는가? | 어떤 함수가 CPU·Heap을 소비했는가? 문제가 전체 트래픽에서 얼마나 흔한가? 계측되지 않은 내부 상태에서 무슨 일이 있었는가? |
| Profiles | 어떤 함수와 호출 스택이 CPU, Heap, Allocation, Lock·I/O 대기를 소비했는가? | 사용자 영향이 얼마나 넓은가? 요청이 어떤 외부 서비스를 지나갔는가? 업무 결과가 정확했는가? 현재 환경에서 필요한 Profile Type을 실제로 지원하는가? |
OpenTelemetry도 네 Signal의 질문을 비슷하게 구분한다. Metrics는 시스템 수준의 상태, Logs는 개별 사건, Traces는 분산 요청 경로, Profiles는 자원을 소비한 코드를 보여준다. Metrics는 많은 측정값을 시간 구간별 통계로 집계하는 반면, Traces는 개별 요청의 생명주기와 Context를 연결한다.
Failure Type → First Signal → Next Signal Matrix
아래 표의 First Signal은 모든 환경에서 고정된 정답이 아니다. 현재 알고 있는 증상만으로 조사 범위를 가장 빨리 줄일 수 있는 기본 출발점이다.
| Failure Type | 현재 보이는 증상 | First Signal | 먼저 답할 질문 | Next Signal | 전환 조건과 주의점 |
|---|---|---|---|---|---|
| User-visible Error | 특정 요청·사용자 여정이 실패 | Trace | 어느 구간에서 실패 상태가 만들어졌는가? | Logs → Metrics | 요청 ID가 없고 오류율 급등만 보이면 Metrics로 범위부터 잡는다. |
| Latency | 서비스 전반 또는 일부 요청이 느림 | Metrics | 언제, 어느 Route·Service·구간에서 지연 분포가 변했는가? | Traces → Profiles 또는 Logs | 이미 느린 요청의 Trace ID가 있다면 Trace부터 시작한다. 한 Process 내부 시간이 길면 Profile로 이동한다. |
| Resource Saturation | CPU·메모리·Thread·Connection·Queue가 상한에 접근 | Metrics | 어떤 자원이 언제부터 포화됐고 어느 Instance에 집중됐는가? | Profiles → Logs 또는 Traces | 자원 소비 코드가 필요하면 Profile, 포화의 결과로 실패한 요청은 Trace·Logs로 확인한다. |
| Intermittent Failure | 일부 요청만 불규칙하게 실패 | Trace 또는 구조화된 Logs | 실패한 건과 정상 건의 경로·조건은 무엇이 다른가? | Logs ↔ Traces → Metrics | 실패 요청이 Trace에 남지 않았다면 Correlation ID가 있는 Logs에서 시작한다. 이후 세그먼트별 발생률을 Metrics로 확인한다. |
| External Dependency | 외부 API·DB·Queue·DNS 등 원격 의존성 의심 | Trace | 어느 외부 호출에서 지연·오류·재시도가 발생했는가? | Dependency Metrics → Logs | Trace가 원격 경계를 보여주지 못한다면 Client 계측과 Context 전파부터 점검한다. |
| Distributed Request Failure | 여러 서비스에 걸친 한 요청이 중간에서 실패 | Trace | 전체 경로 중 최초 실패와 연쇄 실패는 각각 어디인가? | 실패 Span의 Logs → Metrics | 마지막 오류를 낸 서비스와 최초 원인 서비스를 구분한다. |
| Code Hotspot | 특정 코드가 느리거나 CPU 비용이 크다고 의심 | Profile | 어느 함수·호출 스택이 자원을 가장 많이 소비하는가? | Trace → Metrics | 사용자가 겪는 경로와 연결하려면 Trace, 전체 영향 크기를 보려면 Metrics가 필요하다. |
| Memory / CPU anomaly | CPU 급등, Memory 증가, OOM, GC 압박 | Metrics | 어느 Process·Instance·배포 버전에서 이상이 시작됐는가? | Profiles → Logs → Traces | Profile Type 지원을 확인한다. OOM·Restart·GC 사건은 Logs, 영향 요청은 Traces로 연결한다. |
| Background Job | Queue 적체, 처리 지연, 중복·누락·실패 작업 | Metrics | 처리량·실패율·Queue Depth·Oldest Job Age가 어떻게 변했는가? | Traces / Span Links → Logs | 특정 Job ID가 주어졌다면 Logs나 Trace부터 시작할 수 있다. Producer와 Consumer Context를 잇지 않으면 원 요청을 찾기 어렵다. |
| Unknown symptom | “서비스가 이상하다” 외에 단서가 거의 없음 | Metrics | 변화가 시작된 시점, 영향 범위, 함께 변한 자원·오류·지연은 무엇인가? | Trace 또는 Logs → 필요 시 Profiles | Metrics는 원인을 확정하는 도구가 아니라 다음 질문을 고르는 지도다. |
이 Matrix를 Metrics → Logs → Traces → Profiles 같은 단일 계단으로 바꾸면 다시 문제가 생긴다. 특정 사용자 요청 한 건이 실패했다면 전체 Metrics부터 훑는 것보다 해당 Trace가 빠를 수 있다. 반대로 “오늘 아침부터 서비스가 대체로 느리다”는 제보만 있다면 임의의 Trace 한 건을 여는 것보다 지연 분포와 영향 Service를 먼저 보는 편이 낫다.
사용자 오류 한 건과 오류율 급등은 다른 출발점을 가진다
“결제 요청 ABC가 실패했다”처럼 구체적인 요청 식별자가 있다면 Trace에서 어느 Span이 최초 오류를 냈는지 보는 것이 빠르다. 실패 Span을 찾은 뒤 같은 trace_id와 span_id를 가진 로그로 이동하면, 당시 예외와 업무 상태를 확인할 수 있다.
반면 “결제 오류가 평소보다 많다”는 증상은 집계 문제다. 먼저 Metrics에서 오류율이 언제부터 상승했는지, 특정 Route·지역·배포 버전·Instance에 집중됐는지 범위를 좁혀야 한다. 이후 대표적인 실패 요청을 Trace로 고르고 해당 Span의 Logs로 이동한다.
같은 User-visible Error라도 하나의 사건인지, 집단적 변화인지에 따라 First Signal이 달라지는 이유다.
지연은 범위, 경로, 코드의 순서로 좁힌다
서비스가 전반적으로 느려졌다면 Metrics가 다음을 구분해 준다.
전체 요청이 느린가, 특정 Route만 느린가
평균만 변했는가, 상위 지연 구간이 두꺼워졌는가
특정 Service·Instance·배포 버전에 집중됐는가
CPU, 메모리, Queue, Connection 같은 자원 변화와 같은 시점인가
범위를 좁힌 뒤에는 Trace에서 느린 요청의 시간이 어느 Span에 머물렀는지 확인한다. 외부 API 호출이 길었다면 Dependency 문제로 전환하고 DB Span이 길다면 Query와 Connection 상태를 본다. 긴 시간이 한 Process 내부 Span에 남아 있다면 그때 Profile이 유용해진다. On-CPU Profile은 CPU를 소비한 함수, Off-CPU Profile은 Lock이나 I/O를 기다린 위치, Heap·Allocation Profile은 메모리를 점유하거나 반복 할당한 코드 경로를 찾는 데 쓰인다. 지원되는 Profile Type은 Runtime과 Profiler마다 다르다.
Trace는 “어느 구간이 느렸는가”를 보여주지만 그 구간 내부의 어떤 함수가 CPU를 썼는지까지 자동으로 답하지 않는다. Profile은 그 반대다. 함수 수준의 소비는 보여주지만 그 코드가 전체 사용자 요청에서 얼마나 큰 문제였는지는 Metrics와 Trace 없이 판단하기 어렵다.
자원 포화가 보였다고 자원이 원인이라고 단정하지 않는다
CPU 95%, Connection Pool 100%, Queue Depth 급등은 중요한 단서지만 그 자체가 최초 원인을 증명하지는 않는다. 느려진 외부 호출을 기다리는 동안 Worker와 Connection이 묶였을 수도 있고 재시도 폭증이 CPU와 Queue를 함께 밀어 올렸을 수도 있다.
따라서 Resource Saturation에서는 Metrics로 먼저 포화된 자원과 시점을 찾고 다음 Signal은 질문에 따라 갈린다.
누가 CPU·메모리를 소비했는가 → Profiles
어떤 요청이 자원을 오래 점유했는가 → Traces
OOM, Restart, GC, Connection Timeout이 실제로 발생했는가 → Logs
영향이 전체 요청 중 얼마나 되는가 → Metrics 유지
자원 차트 하나를 보고 인스턴스 증설부터 하는 것보다, 포화가 원인인지 결과인지 구분해야 같은 장애가 반복되지 않는다.
간헐 실패에서는 “실패한 한 건”을 보존해야 한다
간헐 장애는 전체 Metrics에서 희석되기 쉽다. 특정 사용자, 특정 Tenant, 특정 입력 조합, 일부 Instance에서만 실패한다면 정상 요청과 실패 요청을 비교할 식별자가 필요하다.
실패한 요청이 Trace로 남아 있다면 경로와 Span Attribute를 정상 요청과 비교한다. Trace가 없다면 구조화된 Logs에서 요청 ID, Job ID, 배포 버전, Instance, 결과 코드와 같은 안정된 Field를 이용해 사건을 찾는다. 여기서 구조화된 로그는 단순히 JSON으로 출력된 로그가 아니라, Field 이름·Type·의미가 일관된 로그를 뜻한다. OpenTelemetry 공식 문서도 JSON 여부보다 안정된 Schema가 구조화 로그의 기준이라고 설명한다.
한 건의 차이를 찾은 뒤에야 Metrics로 돌아가 같은 조건의 발생률을 계산할 수 있다. 원인을 찾았다고 생각했지만 실제로는 극소수 요청의 우연한 특성만 본 것인지 확인하기 위해서다.
외부 의존성과 분산 요청 실패는 Trace가 경계를 보여줘야 한다
분산 시스템에서 최종 오류를 기록한 서비스가 최초 원인인 것은 아니다. 상류 서비스가 하류 API 응답을 기다리다가 Timeout을 냈거나, 하류 서비스의 재시도가 Queue와 Connection을 고갈시킨 결과일 수 있다.
Trace는 요청이 서비스·DB·외부 API를 통과한 경로와 각 Span의 시간을 연결한다. OpenTelemetry는 W3C Trace Context의 traceparent를 기본 전파 방식으로 사용하며 Trace ID와 Parent Span 정보를 서비스 경계 너머로 전달한다. 비동기 작업은 Span Link나 Producer·Consumer Span을 이용해 원 요청과 후속 처리를 인과적으로 연결할 수 있다.
다만 외부 서비스에 내부 Context를 무조건 전달해서는 안 된다. 신뢰하지 않는 외부 경계에서는 들어오는 Trace Header를 검증하거나 정리해야 하고 나가는 Context가 내부 구조를 불필요하게 노출하지 않는지 검토해야 한다. Baggage에는 사용자 자격정보, API Key, 개인정보를 넣지 않는다.
Background Job은 요청 경로만으로 설명되지 않는다
백그라운드 작업은 HTTP 응답이 끝난 뒤 시작되거나, Producer가 Job을 만든 지 오래 지난 후 Consumer가 처리할 수 있다. 그래서 단일 요청의 Parent–Child 관계만으로는 충분하지 않을 수 있다.
Queue 전체가 밀리는 증상이라면 먼저 Metrics에서 다음을 본다.
Enqueue·Completion·Failure Rate
Queue Depth
Oldest Job Age 또는 처리 지연
Retry·Dead-letter 증가
Consumer 수와 처리량
특정 Job Type·Tenant·버전에 집중되는지
특정 Job 한 건을 추적할 때는 Producer Span과 Consumer Span, Span Link, Job ID가 필요하다. Logs에는 상태 전이와 재시도 사유를 남기되, Trace와 연결할 수 있는 Context가 있어야 한다. OpenTelemetry는 Producer Span을 비동기 Job 생성, Consumer Span을 이후의 실제 처리로 구분한다.
Signal을 모으는 것보다 Signal 사이를 이동할 수 있어야 한다
네 Signal이 모두 존재해도 서로 다른 이름과 식별자를 사용하면 장애 중에는 사실상 네 개의 고립된 데이터 저장소가 된다. 필요한 것은 하나의 화면보다 Correlation Contract다.
| 이동 경로 | 필요한 Context | 무엇을 가능하게 하는가 |
|---|---|---|
| Metrics → Traces | Exemplar, 공통 Resource, 시간 구간 | 지연·오류 Histogram의 대표 측정값에서 실제 요청 Trace로 이동 |
| Traces → Logs | trace_id, span_id, 공통 Timestamp | 실패 Span에서 같은 실행 Context의 예외·상태 로그로 이동 |
| Metrics / Logs / Traces 간 Resource 이동 | service.name, service.instance.id, 환경·버전·Region 등 | 같은 Service·Instance·Deployment가 만든 다른 Signal 조회 |
| Traces → Profiles | Trace·Span Context 또는 동일 Resource·시간 구간 | 느린 Span과 당시 실행된 코드 스택 연결 |
| Producer → Background Job | Span Link, Job·Message ID, Producer·Consumer Context | 원 요청과 나중에 실행된 비동기 작업 연결 |
OpenTelemetry의 모든 Signal은 Resource와 연결될 수 있으며 같은 Resource Identity는 동일 Service·Process·Instance에서 나온 Signal을 오가는 기준점이 된다. Metrics와 Traces는 Exemplar를 통해 집계값의 대표 측정과 Trace ID·Span ID를 연결할 수 있다. Logs는 활성 Trace Context를 Log Record에 주입할 수 있다. Profiles도 Request Context와 Resource Context 양쪽의 연결을 지향하지만 현재 Alpha이므로 실제 End-to-end 지원은 조합별로 확인해야 한다.
Metric Label에 매 요청의 Trace ID를 넣는 방식으로 Correlation을 구현해서는 안 된다. Metrics는 고유 Attribute 조합마다 별도 집계 상태를 유지하기 때문에 User ID, Raw URL, Trace ID처럼 값의 종류가 계속 늘어나는 Attribute는 Cardinality와 메모리 비용을 크게 키울 수 있다. 개별 Trace와 연결할 때는 Metric Dimension을 늘리는 대신 Exemplar 같은 연결 구조를 사용한다.
모든 Signal을 항상 수집해야 하는 것은 아니다
관측 가능성의 목표는 네 종류의 데이터를 빠짐없이 구매하거나 저장하는 것이 아니다. 반복해서 답해야 하는 운영 질문을 합리적인 비용과 위험으로 해결하는 것이다.
단일 Process에서 실행되고 외부 호출이 적은 단순한 작업은 기본 Metrics와 구조화된 Logs만으로 충분할 수 있다. 반대로 여러 서비스와 Queue를 통과하는 중요한 사용자 여정은 Traces가 없으면 원인 Service를 찾는 데 시간이 오래 걸릴 수 있다. CPU·메모리 비용이 핵심인 처리 서비스는 Profiles의 가치가 크지만 현재 환경의 지원 상태와 수집 Overhead, 데이터 보존, Symbolization, Backend 호환성을 먼저 검증해야 한다.
Signal 도입 여부는 다음 질문으로 정하는 편이 낫다.
어떤 장애 질문을 현재 데이터로 답하지 못하는가?
그 질문은 얼마나 자주 발생하며 사용자 피해가 얼마나 큰가?
새 Signal이 실제로 조사 범위를 줄이는가?
기존 Signal과 연결할 Context를 만들 수 있는가?
수집량·Cardinality·보존 비용·개인정보 위험을 통제할 수 있는가?
언어·Runtime·Agent·Collector·Backend가 필요한 기능을 실제로 지원하는가?
네 번째 Signal을 추가하는 것보다 기존 두 Signal 사이에 trace_id, Resource, Deployment Version을 일관되게 심는 일이 먼저일 수도 있다.
장애 중에는 다음 질문이 막힐 때 Signal을 바꾼다
실전에서는 모든 탭을 순서대로 열기보다, 현재 Signal이 다음 질문에 더 이상 답하지 못할 때 이동한다.
증상 형태를 분류한다. 한 요청인가, 집단적 변화인가, 자원 이상인가, 코드 비용인가.
First Signal에서 시간과 범위를 줄인다. Service, Route, Instance, Job Type, Dependency, Deployment Version까지 좁힌다.
구체적인 식별자를 확보한다. Trace ID, Span ID, Job ID, Resource Identity, 대표 Exemplar를 찾는다.
다음 질문에 맞는 Signal로 이동한다. 사건의 세부 상태는 Logs, 요청 경로는 Traces, 코드 자원 소비는 Profiles, 발생률과 영향 범위는 Metrics로 확인한다.
완화 행동에 필요한 증거를 남긴다. 원인 추정만 기록하지 말고 어떤 Signal과 식별자로 확인했는지, 영향을 받은 범위와 배포 버전은 무엇인지 남긴다.
장애 분석 속도를 결정하는 것은 Signal의 개수가 아니다. 현재 증상에 맞는 첫 질문을 고르고 그 답을 다음 Signal의 검색 조건으로 넘길 수 있는가가 더 중요하다.
메트릭은 범위를 보여주고 트레이스는 경로를 연결하며 로그는 사건을 설명하고 프로파일은 코드의 자원 소비를 드러낸다. 하지만 이 순서는 고정된 계단이 아니다. 첫 Signal은 언제나 지금 보고 있는 실패의 모양에서 시작해야 한다.