← 목록으로 돌아가기

OpenTelemetry로 운영 관측성 설계하기: Trace·Metric·Log와 SLO 번레이트 알림까지

DevOps

OpenTelemetry Production Observability and SLO Tracing

장애는 로그 한 줄로 설명되지 않는다

운영 장애를 처음 겪는 팀은 대부분 로그부터 찾습니다. 에러 로그가 있으면 원인이 보일 것이라고 기대합니다. 하지만 실제 장애는 그렇게 친절하지 않습니다. 결제 API의 p95 지연 시간이 300ms에서 3초로 튀었는데 애플리케이션 로그에는 에러가 없습니다. 사용자 일부만 로그인에 실패하는데 인증 서비스는 200 응답을 반환합니다. 특정 지역에서만 이미지 업로드가 느린데 서버 CPU는 정상입니다. 이런 상황에서 로그만 뒤지는 방식은 금방 한계에 부딪힙니다.

관측성(observability)은 "무슨 일이 일어났는가"를 외부 출력만으로 추론할 수 있는 능력입니다. 단순히 로그를 많이 남기는 것이 아닙니다. 요청 하나가 어떤 서비스들을 거쳤는지 보는 trace, 시스템이 시간에 따라 어떤 상태였는지 보는 metric, 특정 사건의 세부 맥락을 담는 log가 함께 있어야 합니다. OpenTelemetry는 이 세 가지 신호를 수집하고 연결하기 위한 표준 계층입니다. 벤더 제품이 아니라 계측 모델, SDK, collector, exporter를 포함한 생태계에 가깝습니다.

이 글에서는 OpenTelemetry를 단순히 "붙이는 방법"이 아니라 운영 설계 관점에서 다룹니다. 무엇을 span으로 쪼갤지, 어떤 metric을 SLO와 연결할지, 로그와 trace를 어떻게 상호 참조할지, collector pipeline을 어디에 둘지, burn rate 알림은 어떻게 잡을지까지 실무 기준으로 정리합니다.


1. Trace, Metric, Log의 역할을 섞지 않는다

세 신호는 서로 대체재가 아닙니다. Trace는 요청 경로를 보여줍니다. 사용자의 checkout 요청이 API gateway, order service, payment service, fraud service, database를 거치는 동안 어디에서 시간이 쓰였는지 waterfall 형태로 확인할 수 있습니다. 마이크로서비스 환경에서 "어느 서비스가 느린가"를 찾는 데 가장 강력합니다.

Metric은 집계된 시간 흐름을 보여줍니다. 요청 수, 에러율, 지연 시간 분위수, 큐 적체량, CPU, 메모리, connection pool 사용률처럼 시스템 상태를 숫자로 추적합니다. 알림은 대부분 metric에서 시작해야 합니다. Trace는 모든 요청을 다 저장하기 어렵고, log는 구조가 제각각이라 안정적인 임계값을 만들기 어렵습니다.

Log는 사건의 세부 문맥을 남깁니다. 특정 주문 ID, 외부 PG 응답 코드, 검증 실패 이유, 재시도 횟수처럼 사람이 읽고 판단해야 할 정보가 여기에 들어갑니다. 하지만 log에 latency histogram 역할을 맡기거나, metric에 디버깅용 상세 메시지를 넣으면 둘 다 망가집니다. 좋은 관측성 설계는 세 신호의 역할을 분명히 나눕니다.

가장 중요한 연결고리는 trace id입니다. 모든 구조화 로그에 trace id와 span id를 넣으면, 대시보드에서 느린 요청 하나를 클릭해 관련 로그로 이동할 수 있습니다. 반대로 에러 로그에서 trace id를 타고 전체 요청 경로를 볼 수도 있습니다. 이 연결이 없으면 trace와 log는 각각 훌륭해 보여도 장애 대응 중에는 따로 놀게 됩니다.


2. Span 설계: 모든 함수를 span으로 만들지 않는다

OpenTelemetry를 처음 도입하면 모든 함수에 span을 넣고 싶어집니다. 하지만 span은 공짜가 아닙니다. 생성, 전파, export, 저장 비용이 있고, 너무 잘게 쪼개면 trace가 읽기 어려워집니다. Span은 "운영 판단에 의미 있는 경계"에 만들어야 합니다.

권장하는 span 경계는 외부 호출과 주요 도메인 단계입니다. HTTP inbound 요청, outbound HTTP 호출, DB 쿼리, 메시지 발행/소비, 캐시 조회, 파일 스토리지 접근은 기본 계측 대상입니다. 여기에 비즈니스적으로 중요한 단계가 있으면 custom span을 추가합니다. 예를 들어 checkout에서는 validate cart, reserve inventory, authorize payment, create order 정도가 의미 있는 경계입니다. 반면 DTO 변환 함수나 단순 유틸 함수까지 span으로 만들 필요는 없습니다.

Span 이름은 낮은 카디널리티를 유지해야 합니다. GET /orders/{id}는 좋지만 GET /orders/123456은 좋지 않습니다. 사용자 ID, 주문 ID, 이메일, 전화번호를 span 이름에 넣으면 저장소의 인덱스가 폭발하고 개인정보 위험도 커집니다. 이런 값은 필요할 때 attribute로 넣되, 샘플링과 마스킹 정책을 적용합니다.

Attribute 역시 절제해야 합니다. http.method, http.route, db.system, db.operation, messaging.system, error.type처럼 집계에 필요한 값은 유용합니다. 하지만 request body 전체, SQL 파라미터 전체, JWT 토큰, 카드 번호 일부처럼 민감하거나 고카디널리티인 값은 넣지 않습니다. 관측성 데이터도 운영 데이터입니다. 보안과 비용의 대상입니다.


3. Metric은 RED와 USE로 시작한다

애플리케이션 서비스에는 RED 지표가 기본입니다. Rate, Errors, Duration입니다. 초당 요청 수, 에러율, 요청 지연 시간 분포를 보면 사용자 경험에 가까운 상태를 알 수 있습니다. 특히 duration은 평균이 아니라 histogram 기반 분위수로 봐야 합니다. 평균 200ms인 API도 p99가 5초라면 일부 사용자는 장애를 겪고 있습니다.

인프라 리소스에는 USE 지표가 어울립니다. Utilization, Saturation, Errors입니다. CPU 사용률, 메모리 사용률, 디스크 I/O 대기, 네트워크 오류, connection pool 대기열, thread pool queue length 같은 값이 여기에 속합니다. RED가 "사용자가 느끼는 문제"를 보여준다면 USE는 "시스템 내부의 병목 후보"를 보여줍니다.

Metric 설계에서 가장 자주 터지는 문제는 label 카디널리티입니다. user_id, order_id, session_id를 label로 넣으면 time series 수가 폭발합니다. 저장 비용이 급증하고 쿼리가 느려지며, 최악의 경우 모니터링 시스템 자체가 장애를 일으킵니다. Label은 route, method, status_code, service, region, dependency처럼 가능한 값의 수가 제한된 차원에만 사용합니다.

비즈니스 지표도 포함해야 합니다. 결제 승인 성공률, 주문 생성 수, 재시도 큐 적체량, outbox pending age, 정산 배치 처리 건수는 CPU보다 운영 판단에 직접적입니다. 장애 대응 중 "서버는 정상인데 매출이 멈췄다"는 상황을 피하려면 기술 지표와 비즈니스 지표를 같은 대시보드에서 연결해서 봐야 합니다.


4. SLO와 Burn Rate: 알림은 증상이 아니라 사용자 피해에 맞춘다

알림을 CPU 80%나 에러 로그 10건으로 시작하면 금방 피로해집니다. CPU가 90%여도 사용자가 문제를 느끼지 않는 경우가 있고, 에러 로그가 많아도 retry로 모두 복구되는 경우가 있습니다. 반대로 CPU가 낮아도 외부 결제사의 지연 때문에 checkout이 망가질 수 있습니다. 알림은 가능하면 SLO에서 시작해야 합니다.

SLO(Service Level Objective)는 사용자가 기대하는 서비스 수준을 숫자로 정의한 것입니다. 예를 들어 "checkout 요청의 99.9%는 1초 안에 성공해야 한다"는 SLO가 될 수 있습니다. 여기서 실패는 5xx뿐 아니라 timeout, 명시적 결제 실패, 지연 시간 초과를 포함할 수 있습니다. 중요한 것은 기술 상태가 아니라 사용자 관점의 성공 여부입니다.

Error budget은 SLO에서 허용한 실패 여유분입니다. 99.9% SLO라면 0.1%는 실패할 수 있습니다. Burn rate는 이 예산을 얼마나 빠르게 태우는지를 나타냅니다. 짧은 윈도우와 긴 윈도우를 함께 보는 multi-window burn rate 알림을 쓰면, 순간적인 스파이크와 지속적인 장애를 구분할 수 있습니다.

예를 들어 5분 burn rate가 매우 높고 1시간 burn rate도 높다면 지금 당장 대응해야 하는 장애입니다. 5분만 높고 1시간은 정상이라면 일시적인 배포 스파이크일 수 있습니다. 반대로 5분은 낮지만 6시간 burn rate가 서서히 올라가면 느린 메모리 누수나 특정 dependency 성능 저하를 의심할 수 있습니다. 좋은 알림은 사람을 깨우는 횟수를 줄이면서도 실제 사용자 피해는 놓치지 않습니다.


5. Collector Pipeline: 애플리케이션과 벤더를 분리한다

OpenTelemetry Collector는 계측된 데이터를 받아 가공하고 내보내는 중간 계층입니다. 애플리케이션이 특정 벤더 exporter에 직접 묶이면 나중에 저장소를 바꾸기 어렵습니다. Collector를 두면 SDK는 OTLP로 데이터를 보내고, Collector가 sampling, filtering, batching, attribute masking, vendor export를 담당합니다.

배치와 재시도는 Collector에서 처리하는 편이 좋습니다. 애플리케이션 프로세스가 매 요청마다 telemetry backend로 직접 동기 전송하면, 관측성 시스템 장애가 본 서비스 장애로 전이될 수 있습니다. Telemetry는 중요하지만 사용자 요청 경로를 막아서는 안 됩니다. SDK는 비동기 전송과 bounded queue를 사용하고, queue가 가득 차면 애플리케이션을 멈추기보다 데이터를 버리는 정책이 대체로 안전합니다.

Sampling은 trace 비용을 결정합니다. 모든 요청을 100% 저장하면 비용이 빠르게 증가합니다. 하지만 에러 trace와 느린 trace는 최대한 보존해야 합니다. Head sampling은 요청 시작 시점에 샘플 여부를 결정하므로 비용 제어가 쉽지만, 나중에 에러가 난 요청을 놓칠 수 있습니다. Tail sampling은 요청이 끝난 뒤 전체 trace를 보고 결정하므로 에러와 느린 요청 보존에 유리하지만 Collector 메모리와 지연 비용이 듭니다. 운영 환경에서는 낮은 기본 샘플링에 에러, 고지연, 특정 중요 엔드포인트를 보존하는 정책을 조합하는 경우가 많습니다.

마스킹도 Collector에서 한 번 더 적용해야 합니다. SDK 레벨에서 민감 정보를 넣지 않는 것이 우선이지만, 실수는 발생합니다. Authorization 헤더, cookie, email, phone, card-like pattern은 Collector processor에서 제거하거나 해시 처리합니다. 관측성 데이터 저장소는 많은 사람이 접근하기 때문에 원본 서비스 DB만큼 엄격하게 다뤄야 합니다.


6. 장애 대응 흐름: 대시보드에서 Trace로, Trace에서 Log로

관측성 도구가 많아도 장애 대응 흐름이 정해져 있지 않으면 사람들은 각자 다른 화면을 봅니다. 권장 흐름은 SLO 대시보드에서 시작하는 것입니다. checkout SLO burn rate가 올라갔다면 먼저 어떤 차원이 문제인지 봅니다. region, route, dependency, status_code로 쪼개서 특정 범위에 집중합니다.

다음은 trace입니다. 문제가 되는 route의 느린 trace 샘플을 열어 waterfall을 봅니다. API gateway에서 시간이 쓰였는지, 특정 내부 서비스 호출이 느린지, DB 쿼리가 느린지, 외부 PG 응답이 느린지 확인합니다. 여기서 dependency latency hotspot이 보이면 해당 서비스의 RED/USE 대시보드로 이동합니다.

마지막은 log입니다. Trace에서 문제가 된 span의 trace id로 관련 로그를 좁힙니다. 결제 실패라면 외부 응답 코드, retry decision, idempotency key, circuit breaker 상태를 확인합니다. 이렇게 좁혀 들어가면 "로그 검색창에 에러 키워드부터 넣는" 방식보다 훨씬 빠르게 원인 후보를 줄일 수 있습니다.

사후 분석에서는 대시보드 링크, 대표 trace, 관련 로그, SLO 영향 시간, error budget 소모량을 함께 남깁니다. 장애 문서가 감정적인 회고가 아니라 다음 대응을 빠르게 하는 운영 자산이 되려면, 관측성 데이터가 재현 가능한 형태로 연결되어 있어야 합니다.


7. 도입 순서: 한 번에 모든 것을 계측하지 않는다

처음부터 전 서비스에 완벽한 계측을 넣으려 하면 실패합니다. 먼저 사용자 영향이 큰 핵심 여정 하나를 고릅니다. 예를 들어 회원가입보다 결제가 중요하다면 checkout flow부터 시작합니다. API gateway, order, payment, database, external PG 호출에 trace를 연결하고, checkout SLO metric과 burn rate 알림을 만듭니다. 그다음 로그에 trace id를 주입합니다.

두 번째 단계는 공통 라이브러리화입니다. HTTP client, DB client, message producer/consumer, logger에서 기본 계측을 자동으로 넣습니다. 개발자가 매번 span을 직접 만들게 하면 누락과 불일치가 생깁니다. Custom span은 중요한 도메인 단계에만 남겨둡니다.

세 번째 단계는 운영 기준 정리입니다. 어떤 attribute를 허용하는지, 어떤 label이 금지인지, sampling 정책은 무엇인지, 알림은 어떤 SLO에만 붙일 수 있는지 문서화합니다. 관측성은 기술 도입이 아니라 운영 언어를 맞추는 일입니다. "이 요청이 느리다"는 말이 어느 trace와 어느 metric을 의미하는지 팀 전체가 같은 방식으로 이해해야 합니다.


실무 체크리스트

  • 모든 inbound 요청 로그에 trace id와 span id가 들어가는가
  • HTTP, DB, 메시지 브로커, 캐시 호출이 자동 계측되는가
  • Span 이름과 metric label에 사용자 ID, 주문 ID 같은 고카디널리티 값이 없는가
  • RED 지표와 핵심 비즈니스 지표가 같은 대시보드에서 보이는가
  • SLO가 사용자 관점의 성공률과 지연 시간으로 정의되어 있는가
  • Burn rate 알림이 짧은 윈도우와 긴 윈도우를 함께 보는가
  • Collector에서 batch, retry, sampling, masking을 처리하는가
  • 에러 trace와 느린 trace를 보존하는 샘플링 정책이 있는가
  • 장애 문서에 대시보드, trace, log, SLO 영향이 함께 남는가

결론: 관측성은 도구가 아니라 운영 계약이다

OpenTelemetry를 설치했다고 관측성이 생기지는 않습니다. Trace가 있어도 span 경계가 엉망이면 읽을 수 없고, metric이 많아도 SLO와 연결되지 않으면 알림 피로만 늘어납니다. Log가 상세해도 trace id가 없으면 장애 중에는 바다에서 바늘 찾기가 됩니다.

좋은 관측성 설계는 팀이 장애를 같은 언어로 설명하게 만듭니다. "checkout SLO burn rate가 14배로 올라갔고, us-east 경로의 payment dependency span p99가 4초까지 증가했으며, 관련 trace의 로그에서 외부 PG timeout retry가 확인됐다"는 문장은 곧바로 행동으로 이어집니다. 이것이 운영에서 필요한 관측성입니다. 데이터는 많을수록 좋은 것이 아니라, 의사결정까지 이어질 때 가치가 있습니다.