옵저버빌리티 스택 도입 TDD
Background
PRD 참조. 4개 서비스를 OpenTelemetry로 계측해 단일 trace로 연결하고, 동일 텔레메트리를 SigNoz·Grafana 두 스택에 fan-out해 PoC 비교 후 ADR로 운영 스택을 확정한다.
Overview
- 모든 서비스(
backend·aggregator·ml·frontend)를 OTLP로 계측 — Spring 2종은 OTel Java agent 자동계측, ml은 opentelemetry-instrumentation, frontend는@vercel/otel. - 단일 OpenTelemetry Collector(게이트웨이) 가 OTLP를 수신해 SigNoz와 Grafana(Tempo/Prometheus/Loki)로 fan-out.
- 인프라 3종(MySQL·Kafka·Redis) 컨테이너 도입 + exporter(mysqld/kafka/redis) → Prometheus scrape 및 Collector 수집.
- W3C Trace Context 전파로 서비스 간 trace 연결. Kafka는 Collector·agent가 메시지 헤더에 traceparent를 실어 produce→consume span 연결.
- compose는 앱/Grafana/SigNoz/Collector를 별도 파일로 분리해 PoC 동안 두 스택을 독립 기동.
- 임계치 알림은 Grafana contact point·SigNoz alert channel 양쪽에서 Discord webhook으로 검증.
Terminology
| 용어 | 정의 |
|---|---|
| OpenTelemetry(OTel) | 벤더 중립 텔레메트리 표준. trace·metric·log를 OTLP 프로토콜로 내보냄 |
| OTLP | OpenTelemetry Protocol. 서비스→Collector→백엔드 전송 규격 |
| Collector | OTLP를 수신·처리·fan-out하는 게이트웨이 |
| trace / span | 요청 한 건의 전체 경로(trace) / 그 안의 단위 작업(span) |
| W3C Trace Context | traceparent 헤더로 서비스 간 trace를 잇는 전파 표준 |
| LGTM | Loki(로그)·Grafana(UI)·Tempo(trace)·Mimir/Prometheus(metric) 조합 |
| SigNoz | OTel-native 단일 앱 옵저버빌리티 플랫폼(ClickHouse 백엔드) |
| exporter | 인프라(MySQL/Kafka/Redis) 메트릭을 Prometheus 포맷으로 노출하는 사이드카 |
| fan-out | 한 입력을 둘 이상의 백엔드로 동시 전송 |
Define Problem
AS-IS
[browser] → frontend(Next.js) → aggregator(:8090) → backend(:8080) → ml(:8000)
└→ MySQL(:3308)
- 계측 없음: 메트릭 0, trace 0
- 인프라: MySQL 컨테이너만 존재 (Kafka·Redis 미도입)
- 가시성: 로그 grep + 추측에 의존
TO-BE
서비스(4종) ──OTLP──▶ OTel Collector ──┬──▶ SigNoz (ClickHouse) ── SigNoz UI
└──▶ Tempo/Prometheus/Loki ── Grafana UI
▲
infra exporter(mysqld/kafka/redis) ──scrape──┘ (Prometheus + Collector)
- 단일 trace: frontend→aggregator→backend→ml + Kafka consume span 연결
- 앱 메트릭(OTLP push) + 인프라 메트릭(exporter scrape) 통합
- 동일 텔레메트리가 두 스택에 동시 유입 → PoC 공정 비교 → ADR 확정
Possible Solutions
과거 결정 참조
- 로컬 DB 환경 및 포트 선택 — docker-compose MySQL 8.0, 호스트 포트 3308, 신규 포트는 충돌 회피 필요
- 프로덕션 배포 전략 선택 — Docker Compose + SSH 배포 (compose 일관성 유지)
- ML 응답 캐시 저장소 선택 — Redis가 한 번 채택됐다 신호 영속화 저장소 선택에서 제거됨 → 현재 Redis 미사용 상태 확인
- 민감 정보 주입 방식 선택 — Discord webhook URL은 ~/.zshrc 환경변수로만 주입
| 방안 | 설명 | 채택 여부 |
|---|---|---|
| OTel + 중앙 Collector fan-out | 서비스는 OTLP만 알고 Collector가 두 백엔드로 분기. 두 스택이 동일 입력을 받아 공정 비교 가능 | 채택 — 벤더 중립 + PoC 공정성 동시 충족 (ADR-003) |
| 서비스별 dual-export | 각 서비스가 SigNoz·Grafana로 직접 2중 전송 | 미채택 — 서비스가 백엔드를 알게 됨, 설정 중복, 스택 교체 비용 큼 |
| 벤더 네이티브 에이전트(Datadog 등) | 단일 벤더 SDK로 계측 | 미채택 — 벤더 종속, OSS 비교 PoC 불가, 로컬 비용 (ADR-002) |
| Micrometer Tracing + OTLP | Spring에 라이브러리 직접 추가 | 미채택(Spring) — 코드 변경·의존성 추가 필요, Java agent가 무코드 자동계측 우위 (ADR-004) |
Detail Design
Component Diagram
flowchart LR subgraph Services["서비스(계측)"] FE["frontend\n@vercel/otel"] AGG["aggregator\nOTel Java agent"] BE["backend\nOTel Java agent"] ML["ml\nopentelemetry"] end subgraph Infra["인프라 + exporter"] MYSQL["MySQL + mysqld-exporter"] KAFKA["Kafka + kafka-exporter"] REDIS["Redis + redis-exporter"] end subgraph Pipeline["수집"] COL["OTel Collector\n(게이트웨이)"] PROM["Prometheus"] end subgraph Backends["백엔드 스택(PoC)"] SIGNOZ["SigNoz"] GRAF["Grafana\nTempo/Loki"] end FE --> COL AGG --> COL BE --> COL ML --> COL Infra --> PROM PROM --> GRAF COL --> SIGNOZ COL --> GRAF COL --> PROM
Sequence Diagram — 단일 trace 전파 (정상 흐름)
sequenceDiagram participant FE as frontend participant AGG as aggregator participant BE as backend participant ML as ml participant COL as Collector FE->>AGG: HTTP (traceparent 주입) AGG->>BE: HTTP (traceparent 전파) AGG->>ML: HTTP (traceparent 전파) BE-->>AGG: response + span export ML-->>AGG: response + span export AGG-->>FE: response FE->>COL: OTLP span AGG->>COL: OTLP span BE->>COL: OTLP span ML->>COL: OTLP span COL->>COL: 동일 trace_id로 span 병합 후 fan-out
Sequence Diagram — Kafka consume trace (샘플 검증 경로)
sequenceDiagram participant P as Producer(backend) participant K as Kafka participant C as Consumer(backend) participant COL as Collector P->>K: produce(traceparent 헤더 주입) K->>C: deliver(헤더 보존) C->>C: consume span을 부모 trace에 연결 P->>COL: producer span (OTLP) C->>COL: consumer span (OTLP)
ERD
해당 없음 — 본 과제는 텔레메트리 수집·시각화 인프라이며 신규 비즈니스 테이블이 없다. (Kafka·Redis는 메트릭 대상 인프라로만 도입)
Testing Plan
- 단일 trace 연결: frontend→aggregator→backend→ml 요청 1건이 동일 trace_id로 양 스택 UI에 표시되는지 검증.
- Kafka consume span: 샘플 produce→consume 1건이 동일 trace에 producer·consumer span으로 연결되는지 검증.
- 앱 메트릭: Spring(JVM·HTTP·HikariCP), ml(요청 처리량·지연)이 양 스택에 노출되는지 검증.
- 인프라 메트릭: mysqld/kafka/redis exporter 메트릭이 Prometheus·Collector 경유로 수집되는지 검증.
- fan-out 동등성: 같은 부하에서 SigNoz·Grafana의 trace/메트릭 건수가 일치(±오차)하는지 검증.
- 알림: 임계치 위반 시 Discord webhook 수신 검증(양 스택).
- 예외: Collector 다운 시 서비스가 graceful degrade(앱 동작 무영향)하는지 검증.
Release Scenario
- 인프라 확장 — Kafka·Redis + exporter 컨테이너 기동(STK-OBS-01).
- 백엔드 스택 기동 — Grafana LGTM(STK-OBS-02)·SigNoz(STK-OBS-03) 각각 별도 compose로 기동.
- Collector 기동 — fan-out 라우팅 적용(STK-OBS-04).
- 서비스 계측 적용 — backend·aggregator·ml·frontend(STK-OBS-05~08).
- 검증·알림 — Kafka 샘플 trace(STK-OBS-09)·Discord 알림(STK-OBS-10).
- PoC 비교·ADR 확정(STK-OBS-11) 후 운영 스택 1개로 정리.
- 롤백: 옵저버빌리티 compose 파일만
docker compose down. 앱 계측은 OTel env 비활성(OTEL_SDK_DISABLED=true)으로 즉시 OFF, 코드 변경 불요.
Project Information
- 저장소:
stock-application/(루트 compose + 각 서비스 디렉토리) - 포트: Grafana 3000 / SigNoz 3301 / Collector OTLP 4317·4318 / Prometheus 9090 / exporters 9104·9308·9121 (기존 3306·3307·3308 회피)
- 티켓 prefix: STK-OBS-NN