
가장 시끄러운 알람이 틀렸을 때: SigNoz로 RootSpan 구축하기
요약
장애 발생 시 가장 시끄러운 증상이 아닌 실제 근본 원인을 찾기 위해 SigNoz를 활용한 RootSpan 시스템 구축 사례를 소개합니다. RootSpan은 텔레메트리 데이터를 비교 분석하여 첫 번째 로컬 발산 지점을 식별하는 읽기 전용 장애 상관관계 분석 시스템입니다.
핵심 포인트
- 단순한 에러 발생 지점이 아닌 실제 로컬 발산 지점 식별의 중요성
- SigNoz를 활용한 OpenTelemetry 트레이스, 로그, 메트릭 통합 관리
- 정상 및 실패 코호트 비교를 통한 장애 상관관계 분석 메커니즘
- SigNoz MCP 서버를 통한 데이터 접근 및 쿼리 자동화
알람이 완벽하게 정확하더라도, 조사를 시작해야 할 잘못된 지점을 가리킬 수 있습니다.
저의 해커톤 장애 상황에서, checkout은 에러를 반환했고 gateway는 느려졌지만, 두 서비스 모두 로컬에서 고장 난 상태는 아니었습니다. 두 서비스 모두 scoped timeout(범위 지정 타임아웃)이 시작된 inventory.reserve에서 대기 중이었습니다. 가장 시끄러운 증상과 첫 번째 고장 난 작업 사이의 그 간극 때문에, 저는 SigNoz 해커톤(Agents of SigNoz hackathon)을 위해 RootSpan을 구축했습니다.
RootSpan은 읽기 전용 장애 상관관계 분석 시스템(incident-correlation system)입니다. 이 시스템은 일치하는 정상 및 실패 텔레메트리 코호트(telemetry cohorts)를 비교하고, 첫 번째 로컬 발산(local divergence)의 순위를 매기며, 이를 뒷받침하거나 모순되는 증거를 보여준 뒤, 최종 결정은 사람에게 넘깁니다. RootSpan은 그 어떤 것도 배포, 롤백 또는 재시작하지 않습니다.
해결하고자 했던 장애 상황
저는 다음과 같은 요청 경로를 가진 3개 서비스 Python 실험실을 구축했습니다:
traffic -> gateway.checkout -> checkout.place_order -> inventory.reserve
-> inventory.db.select
정상 트래픽은 inventory-v1과 제어 플래그(control flag)를 사용했습니다. 실패한 코호트는 의도적으로 좁게 설정되었습니다: ap-south-1, inventory-v2, 그리고 async-reserve 플래그입니다. 재설정 가능한 결함 스위치(resettable fault switch)가 활성화되었을 때, inventory.reserve는 350ms 동안 대기한 후 타임아웃을 반환했습니다. 그 후 Gateway와 checkout도 실패했습니다.
이러한 형태가 중요합니다. 단순한 “첫 번째 빨간색 스팬(red span)” 또는 “가장 느린 서비스” 규칙은 단순히 다운스트림(downstream) 대기 시간을 포함하고 있을 뿐인 부모 서비스에 책임을 돌릴 수 있습니다. 대신 RootSpan은 다음과 같이 질문합니다: 실패한 트레이스(traces)에서 반복적으로 변했지만, 일치하는 정상 트레이스에서는 정상적으로 유지되었으며, 시간을 상속받은 것이 아니라 로컬에서 소비한 것은 무엇인가?
SigNoz는 마지막 스크린샷이 아니라 증거 평면(evidence plane)이었습니다
저는 커밋된 casting.yaml과 생성된 lock 파일을 사용하여 Foundry를 통해 셀프 호스팅된 SigNoz와 그 MCP 서버를 설치했습니다:
apiVersion: v1alpha1
kind: Installation
metadata
...
SigNoz는 RootSpan에서 여섯 가지의 뚜렷한 역할을 수행했습니다:
- 실험실의 OpenTelemetry 트레이스 (traces), 구조화된 로그 (structured logs), 그리고 커스텀 메트릭 (custom metrics)을 저장했습니다.
- 트레이스 기반의 체크아웃 에러율 (error-rate) 규칙이 고객에게 보이는 영향을 감지했습니다.
- Alertmanager 호환 웹훅 (webhook)이 멱등성 (idempotent)을 가진 RootSpan 인시던트 (incident)를 생성했습니다.
- SigNoz MCP 서버가 제한된 범위의 트레이스, 로그, 메트릭 및 쿼리 결과를 제공했습니다.
- Query Builder v5가 재현 가능한 지연 시간 (latency) 및 영향 범위 (blast-radius) 집계 (aggregations)를 생성했습니다.
- SigNoz는 상관관계 단계 (correlation stages) 및 Sentinel Mesh 스팬 (spans)을 포함하여 RootSpan 자체를 관찰했습니다.
이는 SigNoz가 로그, 메트릭, 트레이스를 연결된 OpenTelemetry 시그널 (signals)로 취급하는 동시에, Query Builder가 이러한 표면 전반에 걸쳐 필터링 (filtering), 집계 (aggregation), 백분위수 (percentiles), 그룹화 (grouping) 및 수식 (formulas)을 지원하기 때문에 가능합니다. OpenTelemetry 컨텍스트 전파 (context propagation)는 게이트웨이, 체크아웃, 인벤토리 HTTP 호출 전반에 걸쳐 트레이스 관계를 보존했으며, 트레이스 및 스팬 ID (trace and span IDs)를 통해 JSON 로그를 직접적으로 상관 분석할 수 있게 했습니다.
애플리케이션은 OTLP를 통해 세 가지 시그널을 모두 내보냈습니다. 두 개의 커스텀 카운터 (custom counters)는 의도적으로 단순하게 설계되었습니다: rootspan.lab.requests와 rootspan.lab.failures. 이들의 역할은 대시보드를 꾸미는 것이 아니라 의사결정에 답을 제공하는 것이었습니다.
데이터 경로에 LLM을 두지 않고 SigNoz 쿼리하기
가장 중요한 아키텍처 결정은 SigNoz MCP 서버를 일반적인 프로그래밍 방식의 클라이언트 (programmatic client)로 사용하는 것이었습니다. 어떤 증거가 존재하는지 결정하기 위해 모델 (model)이 반드시 필요하지는 않았습니다.
각 인시던트에 대해 RootSpan은 타입이 지정된 제한된 호출 (typed, bounded calls)을 사용했습니다:
signoz_search_traces -> 정상 및 실패 트레이스 ID
signoz_get_trace_details -> 완전한 제한된 트레이스 트리
signoz_search_logs -> 타임아웃 지문 (fingerprints) 및 트레이스 연결 예시
...
모든 호출은 도구 이름, 타입이 지정된 인자 (typed arguments), 시간 범위, 응답 해시 (response hash), 지속 시간 (duration), 상태 (status), 그리고 SigNoz 딥 링크 (deep link)를 저장했습니다. 상관관계의 핵심은 TelemetryGateway 계약 (contract)에 의존했으므로, 리플레이 픽스처 (replay fixtures)와 라이브 MCP 결과는 동일한 도메인 타입 (domain types)을 반환했습니다. 이를 통해 증거 (evidence)에 대한 두 번째 정의를 만들지 않고도, 라이브 스택이 오프라인 상태일 때 알고리즘을 테스트할 수 있었습니다.
런타임 자격 증명 (Runtime credentials)은 뷰어 전용 (Viewer-only)이었습니다. 부트스트랩 자격 증명 (Bootstrap credentials)은 알람, 대시보드, 웹훅 채널 및 서비스 계정을 생성할 수 있었지만, 실행 중인 조사자 (investigator)는 읽기만 가능했습니다. 이러한 분리는 인간의 승인 경계 (human-approval boundary)를 단순한 희망 사항이 아닌 강제 가능한 규칙으로 만들었습니다.
첫 번째 편차 순위 지정 (first-divergence ranking) 작동 방식
RootSpan은 정상적인 트레이스 (traces)와 실패한 트레이스 간의 동일한 작업을 정렬하고, 포함 지속 시간 (inclusive duration)과 제외 지속 시간 (exclusive duration) 또는 자체 지속 시간 (self duration)을 모두 계산합니다. 각 작업에 대해 다음 사항을 기록합니다:
- 정상 트레이스 대비 실패 트레이스에서의 유병률 (prevalence);
- 로컬 에러율 상승 (local error-rate lift);
- 포함 및 제외 지속 시간 비율 (inclusive and exclusive duration ratios);
- 로컬 작업에 기인하는 전체 변화의 비율;
- 독립적인 신호 지원 (independent signal support);
- 모순 및 코호트 커버리지 페널티 (contradiction and cohort-coverage penalties).
핵심 교훈은 포함 지연 시간 (inclusive latency)만으로는 오해의 소지가 있다는 것이었습니다. 결정적인 장애 상황에서 게이트웨이 (gateway)와 체크아웃 (checkout) 스팬 (spans)이 느려진 이유는 자식 호출 (child call)이 느렸기 때문입니다. 이들의 자체 지속 시간 (self-duration)은 베이스라인 (baseline) 근처를 유지했습니다. inventory.reserve는 큰 로컬 변화와 타임아웃을 보여주었으므로 첫 번째 순위로 지정되었습니다.
[
점수는 검사 가능한 상태로 유지되며, RootSpan은 결과를 증명된 인과 관계 (proven causality)가 아닌 순위가 매겨진 가설 (ranked hypothesis)이라고 부릅니다. 만약 어느 한 코호트 (cohort)가 누락되었거나, 사용 가능한 트레이스가 2개 미만이거나, 요청된 커버리지의 50% 미만으로 떨어지거나, 또는 어떤 작업도 증거 임계값 (evidence threshold)을 넘지 못하는 경우, 해당 장애는 INSUFFICIENT_EVIDENCE (증거 부족) 상태가 됩니다. 진단을 반환하지 않는 것은 하나의 기능 (feature)입니다.
Sentinel Mesh: 결정론적 권위를 가진 병렬 관찰
해커톤이 에이전트 (agents)에 집중되었기 때문에, 저는 단순히 "AI"라는 라벨이 붙은 단일 프로세스 이상의 것을 원했습니다. 각 라이브 인시던트 (live incident)는 논리적인 Sentinel Mesh를 생성합니다. 즉, 게이트웨이 (gateway), 체크아웃 (checkout), 인벤토리 (inventory), 그리고 데이터베이스 옵저버 (database observers)가 제한된 읽기 전용 작업을 병렬로 수행합니다.
하나의 리더 (leader)는 원자적 SQLite 임대 (atomic SQLite lease)를 통해 선출됩니다. 리더십은 모델이나 투표에 의해 결정되지 않습니다. 만약 리더가 실패하면, 임대 생성 (lease generation)이 진행되고 건강한 팔로워 (follower)가 역할을 이어받습니다. 실패한 팔로워는 성능 저하된 커버리지 (degraded coverage) 상태로 계속 표시되지만, 다른 팔로워들의 증거를 삭제할 수는 없습니다. 점수 산정 (scoring)과 기권 (abstention)에 대한 권한은 센티넬 (sentinels)이 아닌 결정론적 랭커 (deterministic ranker)가 가집니다.
SigNoz는 sentinel.leader.elect, sentinel.delegate, sentinel.observe, sentinel.leader.failover와 같은 스팬 (spans)뿐만 아니라 cohort.select, trace.align, divergence.rank, brief.compile을 통해 해당 워크플로우를 관찰 가능하게 (observable) 만들었습니다. 따라서 조사 시스템은 자신이 검사하는 시스템과 동일한 관찰 가능성 (observability) 표준을 충족해야 했습니다.
예상치 못했던 설정 문제
가장 어려웠던 통합 문제는 랭킹 (ranking)이 아니라 텔레메트리 수집 (telemetry ingestion)이었습니다. 저의 로컬 Foundry 캐스트 (cast)에서, OpAMP로 관리되는 인제스터 (ingester)는 nop 수신기 (receivers) 및 익스포터 (exporters)가 포함된 온보딩 전 설정을 수신할 수 있었습니다. 컨테이너들은 정상적으로 보였지만, OTLP 포트는 연결을 거부했습니다.
저는 이를 애플리케이션 소유의 작은 OpenTelemetry Collector 브리지(bridge)를 통해 해결했습니다. 이 브리지는 RootSpan과 실험실(lab)로부터 OTLP를 수신한 다음, 동일한 Foundry 텔레메트리 저장소를 대상으로 SigNoz의 네이티브 ClickHouse 익스포터(exporters)를 사용했습니다. 이는 배포용 스캐폴딩(scaffolding)이었으며, 두 번째 제품 데이터베이스가 아니었습니다. 더 중요한 것은, 완전한 3개 서비스 트레이스(traces), 커스텀 메트릭(custom metrics), 타임아웃 로그, 6개 상관관계 단계(correlation-stage)의 스팬(span) 이름 전체, 그리고 센티넬 관찰 스팬(sentinel observation spans)에 대해 make telemetry-check 어설션(assertions)을 추가했다는 점입니다. 이제 컨테이너가 초록색으로 표시되는 것만으로는 텔레메트리(telemetry)가 작동한다는 증거로 인정되지 않았습니다.
측정값이 실제로 말해주는 것
저는 실행 속도와 별개로 정확성을 평가했습니다. 결정론적 스위트(deterministic suite)는 4개 작업에서의 로컬 실패, 부분적 유병률(prevalence), 섞인 입력 순서, 누락되거나 규모가 작은 코호트(cohorts), 그리고 기권(abstention)을 유발해야 하는 정상적인 텔레메트리 등을 포함하여 14개의 라벨링된 시뮬레이션을 포함합니다.
make evaluate를 실행한 결과는 다음과 같습니다:
- 100% top-1 로컬라이제이션(localization) 및 1.0 평균 역순위(mean reciprocal rank);
- 100% 기권 재현율(abstention recall) 및 0% 오진(false diagnoses);
- 100% 인용 무결성(citation integrity), 딥링크 커버리지(deep-link coverage), 쿼리 출처(query provenance);
- 280회의 시드된 재정렬(seeded reorder) 시도 전반에 걸친 안정적인 순위;
- 이번 실행에서 50개의 정상 및 50개의 실패 트레이스에 대해 코어의 p95가 2.557 ms.
최종 배포된 make live-verify 게이트(gate)는 다른 경계를 측정했습니다: 5개의 완전한 MCP/SigNoz 조사. 이는 5/5회 실행에서 inventory.reserve를 1위로 선정했으며, p50 요청-준비 시간(request-to-ready)은 1002.8 ms, p95는 1283.3 ms였습니다. 이 수치들은 통제된 하나의 로컬 시나리오를 설명하는 것이지, 프로덕션의 근본 원인(root-cause) 정확도나 SLA 개선을 의미하는 것은 아닙니다.
Foundry 프로비저닝(provisioning) 후의 경로를 재현하려면:
make bootstrap-signoz
make app-up
make live-verify
...
과거의 나에게 해주고 싶은 말
영리한 스코어러(scorer)를 작성하기 전에 코호트(cohorts)를 맞추세요. 로컬 작업과 상속된 지연 시간(latency)을 분리하세요. 모순되는 사항은 뒷받침하는 증거와 함께 저장하세요. 성공만큼이나 기권(abstention)도 진지하게 테스트하세요. 그리고 조사 도구(investigator)를 조기에 계측(instrument)하세요. 그렇지 않으면 "에이전트가 느리다"라는 말이 트레이스(trace)조차 남지 않는 또 다른 장애(incident)가 될 것입니다.
가장 중요한 점은, 프로젝트가 에이전트적(agentic)이기 위해 LLM이 반드시 증거 파이프라인(evidence pipeline)을 소유할 필요는 없다는 것입니다. RootSpan의 유용한 자율성에는 경계가 있습니다. 관찰자(observers)를 조정하고, 검증된 텔레메트리(telemetry)를 수집하며, 다음 인간의 의사결정을 준비하는 것입니다. 결정론적 코드(Deterministic code)가 데이터가 무엇을 뒷받침하는지 결정합니다. 미래의 모델은 해당 패킷을 설명할 수는 있겠지만, 증거를 조작하거나 운영 권한(production authority)을 획득할 수는 없습니다.
RootSpan이 인벤토리 타임아웃(inventory timeout) 문제를 직접 해결한 것은 아닙니다. 대신 제가 더 신뢰할 수 있는 일을 해냈습니다. 상류(upstream)의 체크아웃 경고를 첫 번째 로컬 발산(local divergence)에 대한 인용된, 재현 가능한 설명으로 변환했고, 그 후 인간에게 인계(handoff)하며 멈췄습니다.
프로젝트 링크: source code, architecture and evaluation, 그리고 공식 SigNoz overview.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기