옵저버빌리티 스택 도입 TDD
Background
근거 PRD: /Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/옵저버빌리티 스택 도입/PRD.md (검수 PASS)
선행 컨텍스트: ../도메인 경계 재설계/TDD.md (5계층 컨텍스트 맵·의존 규칙)
sports-application backend는 계측이 부분적으로 존재한다 — 실측:
backend/build.gradle.kts:37io.micrometer:micrometer-registry-datadog(유료 SaaS registry). Prometheus registry 의존성은 부재.backend/build.gradle.kts:41spring-boot-starter-actuator존재.application.yml:156management.endpoints.web.exposure.include: health,info,prometheus,metrics—prometheus엔드포인트가 노출 목록에 있으나micrometer-registry-prometheus가 없어/actuator/prometheus는 실제로 404다(Micrometer가 PrometheusMeterRegistry 빈을 만들지 못함).application.yml:160-167management.datadog.metrics.export(DATADOG_ENABLED:false기본 비활성).application.yml:168-172management.metrics.tags:application,env: ${APP_ENV:local},service: mcp—env태그 체계는 이미 존재.- 분산 추적(OpenTelemetry)은 전무.
NotificationEventWorker(Kafka Consumer,KafkaListenerContainerFactory#kafkaListenerContainerFactory)·KafkaProducerConfig가 존재하나 trace 전파 계측 없음. - 루트
docker-compose.yml은minio·mock-pg·mock-servers·mailhog만 — MySQL/Redis/Kafka/Mongo·LGTM·Collector·Kafka UI 전무. 앱은 이들을localhost기본값(application.yml:35,39,44,61)으로 참조. - 비동기 스레드풀
mcpAuditExecutor(AsyncConfig.kt:12, core 4 / max 16 / queue 200) 존재 — Micrometer 자동 바인딩 대상 아님(커스텀 executor).
즉 실제 공백은 ① Prometheus registry 부재로 노출 채널이 죽어 있음 ② 분산 추적 전무 ③ 인프라 자체가 compose로 묶여 있지 않아 수집 대상이 없음이다.
Overview
- 무엇을:
micrometer-registry-datadog을micrometer-registry-prometheus로 교체하고, Micrometer Tracing + OTel Bridge로 분산 추적을 신규 도입한다. OTel Collector 1개가 trace→Tempo·log→Loki로 라우팅하고, 메트릭은 Prometheus가 앱(/actuator/prometheus)과 exporter 3종(mysqld/redis/kafka)을 직접 scrape한다. Grafana 대시보드 4종·Kafka UI·exporter 3종을 관측 전용 compose로 편입한다. - 데이터스토어 소유 경계 (senior-pm #4 반영): MySQL/MongoDB/Redis/Kafka 데이터스토어 자체는 ⑧ base compose가 단독 소유한다(서비스명
mysql/mongodb/redis/kafka). ⑤는 데이터스토어를 정의하지 않고, ⑧이 띄운 서비스를 대상으로 exporter·Kafka UI를 공유 네트워크로 붙인다.mongodb서비스명은 ⑧ 규약을 따른다(구mongo폐기 — mongo 컨테이너 이중화 방지). - 왜: 죽어 있는 Prometheus 노출을 살리고(registry 교체), 추측에 의존하는 병목 식별을 trace 기반으로 전환하며, 인프라 메트릭 관측을 확보한다.
- 어떻게: 계측은 앱 코드 최소 변경(의존성 교체 + config)으로, 인프라는 기존
docker-compose.yml·⑧ base compose를 건드리지 않고 관측 전용 compose 파일 2개(docker-compose.observability.yml=LGTM+Collector+Grafana,docker-compose.observability-agents.yml=exporter 3종+Kafka UI)로 추가해 ⑦⑧과의 Single Writer 충돌을 없앤다.env태그는 신규 키 없이 재사용한다.
Terminology
| 용어 | 정의 |
|---|---|
| LGTM | Loki(로그)·Grafana(시각화)·Tempo(트레이스)·Mimir/Prometheus(메트릭) 스택 |
| OTLP | OpenTelemetry Protocol — 텔레메트리 전송 표준(gRPC 4317 / HTTP 4318) |
| Collector | OTLP를 수신해 백엔드별로 라우팅하는 게이트웨이 프로세스 |
| Micrometer Tracing | Spring 계측 추상화(Observation API), OTel Bridge로 OTLP 전송 |
| scrape | Prometheus가 대상의 /metrics를 주기적으로 pull하는 수집 방식 |
| exporter | 인프라(MySQL/Redis/Kafka) 상태를 Prometheus 포맷 /metrics로 노출하는 사이드카 |
| exemplar | 메트릭 샘플에 trace_id를 연결해 메트릭→트레이스 점프를 가능케 하는 메타데이터 |
| resource attribute | trace/log에 부여되는 OTel 차원(service.name·deployment.environment 등) |
Define Problem
AS-IS
build.gradle.kts:37: Datadog registry만 존재 →/actuator/prometheus는 registry 빈 부재로 404(노출 목록에는 있으나 동작 안 함).application.yml:160-167: Datadog export 경로(비활성). LGTM과 연결 불가.- 분산 추적 없음 —
Controller→UseCase→DomainService→Gateway, Kafka Producer→NotificationEventWorker구간이 하나의 trace로 엮이지 않음. - 인프라 메트릭 없음 — MySQL slow query·Redis memory·Kafka lag 관측 수단 부재.
docker-compose.yml에 데이터 인프라·관측 스택 부재 → 단일 기동 관측 불가.mcpAuditExecutor(AsyncConfig.kt:12) 스레드풀 active/max 미노출(FR-5 요구).
TO-BE
/actuator/prometheus가 실제 동작(Prometheus registry) → Prometheus가 직접 scrape.- Micrometer Tracing이 HTTP in/out·JDBC·Kafka 구간 span을 OTLP로 Collector→Tempo 전송, trace 연결율 99%+.
- Collector가 trace→Tempo·log→Loki 라우팅, 메트릭은 scrape 경로.
- exporter 3종 + Prometheus self-scrape로 인프라·수집기 자체 관측.
- Grafana 대시보드 4종(MySQL/Redis/Kafka/Spring)에
env태그 필터. docker compose -f ... -f docker-compose.infra.yml -f docker-compose.observability.yml로 앱+인프라+관측 단일 기동.
Architecture Benchmarking (의무)
| 제품/사례 | 해결 방식 | 참고할 패턴 | 미참고 사유 |
|---|---|---|---|
Grafana docker-otel-lgtm (grafana.com/blog/otel-lgtm, github.com/grafana/docker-otel-lgtm) | Collector + Prometheus + Loki + Tempo + Grafana를 단일 이미지로 묶어 로컬 관측 백엔드 제공. Collector가 metric→Prometheus, trace→Tempo, log→Loki 라우팅 | ① Collector를 단일 게이트웨이로 두고 백엔드별 라우팅하는 토폴로지 채택. ② OTLP 4317/4318 수신 규약 채택 | 올인원 단일 이미지는 컨테이너별 리소스·설정 제어가 불가 → 8GB 제약(NFR)·exporter 편입·⑧ dev/prod 분리를 위해 컨테이너를 개별 서비스로 분해해 채택 |
| Baeldung/frankel.ch — Spring Boot 3 Micrometer Tracing vs Java Agent (blog.frankel.ch, spring.io/blog 2025-11-18) | Micrometer Tracing + bridge-otel + exporter-otlp로 코드 계측 vs Java Agent 바이트코드 계측 비교 | Micrometer Tracing이 기존 Micrometer 메트릭 자산을 그대로 유지하며 trace를 OTLP로 보내는 경로 채택(ADR-001) | Java Agent는 라이브러리 버전 강결합·메트릭 병렬 파이프라인 유발 → 미채택(ADR-001) |
| 사람인 OpenTelemetry 도입 (saramin.github.io) | OTel trace로 서비스 간 호출 병목·장애 전파를 데이터로 식별, MTTR 50%+ 단축 | ”trace 연결이 병목 식별의 핵심”이라는 본 과제 핵심 가설의 실증 근거 — 100% 샘플링·전 구간 연결 목표의 정당성 | MSA 다중 서비스 전제 → 개인 모놀리스에는 전파 규약(HTTP/Kafka)만 참조, 서비스 메시 부분은 미참조 |
Possible Solutions
방안 비교 — OTel 계측 방식 (핵심 결정, ADR-001)
| 방안 | 설명 | 왜 채택 / 미채택 |
|---|---|---|
| A. Micrometer Tracing + OTel Bridge | micrometer-tracing-bridge-otel + opentelemetry-exporter-otlp 의존성 추가, management.tracing·management.otlp.tracing으로 config. Observation API로 HTTP in/out·RestClient·Kafka(관측 옵션) span 생성. 메트릭은 기존 Micrometer 유지 | 채택 — ① 메트릭은 /actuator/prometheus scrape로 유지(FR-1/FR-5, 신규 계측 코드 0). ② env 태그를 management.metrics.tags + OTel resource attribute에 config로 일관 부여(FR-8, 누락 0 용이). ③ Spring Boot 3.3.5 1급 지원, 라이브러리 버전 결합 없음 |
| B. OpenTelemetry Java Agent | -javaagent:opentelemetry-javaagent.jar 부착, 환경변수만으로 무코드 계측. 바이트코드로 JDBC/Kafka/HTTP 자동 계측 | 미채택 — ① Agent가 메트릭도 OTLP로 밀어 /actuator/prometheus scrape 경로와 병렬 이중 파이프라인 발생 → FR-1(registry 교체·엔드포인트 재사용)과 충돌. ② Agent가 라이브러리 버전에 강결합(v1→v2에서 Spring 애노테이션 추적 중단 전례) → 운영 리스크. ③ env 태그를 Micrometer와 별도 resource로 재정의해야 해 FR-8 이중 관리 |
| C. 수동 SDK 계측 | OTel SDK를 직접 코드에 심어 span 생성 | 미채택 — 전 구간 수동 계측은 노동·누락 위험 과다, 지금 규모에 과함(단순함 우선) |
방안 비교 — 메트릭 수집 경로 (ADR-002)
| 방안 | 설명 | 왜 채택 / 미채택 |
|---|---|---|
| P1. 앱·exporter는 Prometheus 직접 scrape, trace/log만 Collector | 앱 /actuator/prometheus·exporter /metrics를 Prometheus가 pull. Collector는 trace→Tempo·log→Loki 전담 | 채택 — FR-1 “엔드포인트 재사용”과 정확히 일치. 메트릭 경로와 trace 경로를 분리해 Collector 장애가 메트릭 수집에 전파되지 않음(장애 격리). exporter는 본래 scrape 모델 |
| P2. 앱 메트릭도 Collector로 OTLP push → remote_write→Prometheus | 모든 텔레메트리를 Collector 단일 경로로 | 미채택(부분 보조) — 단일 경로는 Collector가 메트릭까지 SPOF. 단 Collector가 OTLP로 받은 소량 메트릭(예: 향후 커스텀)을 remote_write로 넘기는 경로는 열어두되(FR-3 문구 충족), 주 경로는 scrape. exemplar(FR-11)만 이 경로 활용 |
방안 비교 — docker-compose 파일 소유 (ADR-004)
| 방안 | 설명 | 왜 채택 / 미채택 |
|---|---|---|
C1. 기존 docker-compose.yml에 전부 추가 | 단일 파일에 인프라+관측 편입 | 미채택 — ⑦(다중 인스턴스+LB)·⑧(dev/prod 분리)가 같은 파일을 동시 수정 → Single Writer 충돌 불가피 |
| C2. ⑤ 데이터스토어도 별도 파일로 소유 | ⑤가 docker-compose.infra.yml에 MySQL/Redis/Kafka/Mongo 정의 | 미채택 (senior-pm #4) — ⑧ base compose도 데이터스토어를 정의해 -f 병합 시 중복 서비스 발생(특히 mongo≠mongodb로 mongo 컨테이너 이중화). 데이터스토어 단일 소유자는 ⑧ |
| C3. ⑤는 관측 전용 compose 2개, 데이터스토어는 ⑧ 소유 | docker-compose.observability.yml(LGTM+Collector+Grafana) + docker-compose.observability-agents.yml(exporter 3종+Kafka UI). exporter는 ⑧ base 데이터스토어에 접속 | 채택 — 파일 소유가 과제별로 분리돼 ⑦⑧이 각자 override 파일(.scale.yml·.dev.yml)을 추가해도 충돌 0. 데이터스토어 중복 정의 0. Compose merge는 표준 기능 |
Detail Design
시스템 역할 경계 (의무) — 서버·컴포넌트 토폴로지
과제 특성상 신규 애플리케이션 서버(워커·스케줄러·소켓)를 추가하지 않는다 — 계측은 기존 API 서버 인프로세스(Micrometer Tracing)로 충분하고, 수집·시각화는 독립 인프라 컨테이너로 분리한다. 별도 서버를 만들지 않는 근거: trace는 요청-응답 인프로세스 계측, 메트릭은 pull(scrape)이라 앱은 노출만 하면 되며 별도 처리 서버가 불필요하다.
| 단위 | 종류 | 역할 | 소유/책임 | 노출 인터페이스 | 의존 |
|---|---|---|---|---|---|
| Spring Boot 앱(기존) | 프로세스 | 메트릭 노출 + trace/log OTLP 발신 | Micrometer registry·Tracing 계측 | GET /actuator/prometheus, OTLP 발신(4317) | Collector(trace/log), 없음(metric은 pull당함) |
| OTel Collector | 컨테이너 | trace/log 게이트웨이 라우팅 | receiver→exporter 파이프라인 | OTLP 수신 :4317/:4318, self-metric :8888 | Tempo, Loki, Prometheus(remote_write) |
| Prometheus | 컨테이너 | 메트릭 저장·scrape 오케스트레이션 | scrape 잡·remote_write 수신 | PromQL :9090 | 앱·exporter 3종·Collector·self |
| Tempo | 컨테이너 | trace 저장·조회 | trace 백엔드 | :3200(query), OTLP 수신 | (없음) |
| Loki | 컨테이너 | 로그 저장·조회 | log 백엔드 | :3100 | (없음) |
| Grafana | 컨테이너 | 시각화 | 데이터소스·대시보드 4종 프로비저닝 | UI :3000 | Prometheus, Tempo, Loki |
| mysqld_exporter | 컨테이너(⑤ 소유) | MySQL 메트릭 노출 | cpu/mem/slow query/connection | /metrics :9104 | ⑧ mysql |
| redis_exporter | 컨테이너(⑤ 소유) | Redis 메트릭 노출 | cpu/mem | /metrics :9121 | ⑧ redis |
| kafka_exporter | 컨테이너(⑤ 소유) | Kafka 메트릭 노출 | cpu/mem/lag/partition | /metrics :9308 | ⑧ kafka |
| Kafka UI | 컨테이너(⑤ 소유) | lag·partition 조회 UI | 토픽·컨슈머그룹 조회 | UI :8089 | ⑧ kafka |
| 데이터스토어(MySQL/MongoDB/Redis/Kafka) | 컨테이너(⑧ 소유) | 앱 런타임 저장소 | ⑧ base compose가 단독 정의 | 각 표준 포트 | (없음) — ⑤는 정의하지 않고 참조만 |
인터페이스·엔드포인트 계약 (구현자 간 해석 차이 제거 — Single Writer 병렬 authoring의 SSOT)
컨테이너 호스트명·포트를 계약으로 고정한다. 관측 compose(INFRA-02=backends, INFRA-01=agents)와 config(INFRA-03/04)가 이 표를 참조해 파일을 만지지 않고도 정합한다. 데이터스토어 서비스명은 ⑧ base compose 규약을 그대로 인용한다.
| 대상 | compose 서비스명 | 네트워크 호스트:포트 | scrape/접속 경로 | env 태그 원천 |
|---|---|---|---|---|
| 앱 메트릭 | backend(⑧ 컨테이너) | backend:8080 (비컨테이너 로컬은 host.docker.internal:8080) | /actuator/prometheus | management.metrics.tags.env |
| 앱 trace 발신 | — | otel-collector:4318 (/v1/traces) | OTLP HTTP (Spring Boot 3.3.5 제약 — gRPC 미지원, 4318 사용) | resource deployment.environment |
| Collector self | otel-collector | otel-collector:8888 | /metrics | — |
| MySQL exporter | mysqld-exporter | mysqld-exporter:9104 | /metrics | Prometheus relabel env |
| Redis exporter | redis-exporter | redis-exporter:9121 | /metrics | Prometheus relabel env |
| Kafka exporter | kafka-exporter | kafka-exporter:9308 | /metrics | Prometheus relabel env |
| Prometheus self | prometheus | prometheus:9090 | /-/healthy, /metrics | — |
| Grafana datasource | grafana | Prometheus http://prometheus:9090, Tempo http://tempo:3200, Loki http://loki:3100 | — | — |
| 데이터스토어 (⑧ 소유·참조만) | mysql/mongodb/redis/kafka | 표준 포트(3306/27017/6379/9092) | exporter가 접속 | — |
service.name 규약 (senior-pm #5 확정) — trace/log를 발신하는 서비스는 아래 service.name으로 자기를 식별한다. Grafana·Tempo 대시보드 라벨이 두 서비스를 구분한다.
| 서비스 | service.name | 발신 방식 | 소유 |
|---|---|---|---|
| Spring Boot BE | sports-application | Micrometer Tracing OTLP resource | 본 과제(BE-02) |
| web FE (Next.js 서버 런타임) | sports-web | @vercel/otel OTEL_SERVICE_NAME | design-fe-web(⑤ FE) |
두 서비스 모두 deployment.environment=${APP_ENV}를 공유하므로, Grafana에서 env 필터 + service.name(Tempo)·service_name(Loki) 라벨로 BE/FE를 분리 조회한다.
앱 계측 config 계약(BE-02가 확정, 후속 과제가 인용):
# application.yml (management) — Spring Boot 3.3.5
management.metrics.tags.env = ${APP_ENV:local} # 기존 키 재사용 (FR-8)
management.tracing.sampling.probability = 1.0 # 100% 샘플링 (NFR)
# OTLP tracing은 3.3.5에서 HTTP 전용 — Collector HTTP 포트(4318) + /v1/traces 경로. transport 프로퍼티는 3.4+ 전용이라 두지 않는다.
management.otlp.tracing.endpoint = ${MANAGEMENT_OTLP_TRACING_ENDPOINT:http://localhost:4318/v1/traces}
# env → deployment.environment 매핑 (service.name은 spring.application.name=sports-application이 자동 매핑)
management.opentelemetry.resource-attributes.deployment.environment = ${APP_ENV:local}
# 콘솔 로그 MDC 키는 Micrometer 기본값 traceId/spanId (logback-spring.xml)
클래스/파일 역할 정의 — 앱 계측 변경 (신규 계측 코드 최소)
| 파일 | 레이어 | 변경 | 근거 |
|---|---|---|---|
build.gradle.kts | build | micrometer-registry-datadog 제거, micrometer-registry-prometheus·micrometer-tracing-bridge-otel·opentelemetry-exporter-otlp·opentelemetry-logback-appender-1.0 추가 | FR-1/FR-2 |
application.yml | config | management.datadog.* 제거, management.tracing·management.otlp.tracing 추가, prometheus 노출·env 태그 유지 | FR-1/FR-2/FR-8 |
KafkaConsumerConfig.kt | infrastructure | factory.containerProperties.isObservationEnabled = true | Kafka Consumer trace 전파(연결율 99%) |
KafkaProducerConfig.kt | infrastructure | KafkaTemplate.setObservationEnabled(true) | Producer→Consumer trace 전파 |
AsyncConfig.kt(+메트릭 바인딩) | infrastructure | mcpAuditExecutor를 ExecutorServiceMetrics로 바인딩 | FR-5 Async threadpool active/max |
logback-spring.xml(신규) | config | OTLP log appender + trace_id/span_id MDC 패턴 | FR-3 log→Loki, FR-11 exemplar 기반 |
Tomcat/JVM/GC/Heap 메트릭은 Micrometer + actuator가 기본 제공 → Prometheus registry 교체만으로 노출(FR-5, 신규 코드 0). Async 커스텀 executor만 명시 바인딩 필요.
실패 경로·동시성·멱등 (해피 패스만 있는 설계는 미완성)
| 실패 시나리오 | 영향 | 설계 대응 | 감지 |
|---|---|---|---|
| Collector 다운 | trace/log 유실 | 메트릭은 scrape 경로라 무영향(장애 격리). 앱 BatchSpanProcessor 버퍼가 재시작까지 일부 완충, 초과분은 drop(추적은 best-effort, NFR 99%가 1% 완충 허용) | Prometheus self-scrape up{job="otel-collector"}==0 (⑥ 알림 소스) |
| exporter 다운 | 해당 인프라 메트릭 공백 | 나머지 대시보드 정상 | up{job="mysqld-exporter"}==0 |
| Prometheus 다운 | 신규 메트릭 수집 중단 | 앱 런타임 무영향(pull 대상일 뿐) | Grafana 데이터소스 헬스체크 |
| 앱 재시작 | trace 컨텍스트 리셋 | 정상(새 trace) | — |
| trace 연결 끊김(Kafka 경계) | Producer/Consumer span 분리 | isObservationEnabled=true로 헤더 전파, 미전파 시 연결율 저하 | 샘플 10건 trace 검사(Success Metric) |
- 동시성: 이 과제는 앱 비즈니스 상태를 바꾸지 않는다 — 락·경쟁 조건 신규 없음. Collector/Prometheus는 stateless 수신·pull이라 동시성 이슈 없음.
- 멱등: 신규 이벤트 소비 로직 없음 → 멱등 키 불필요. 메트릭 scrape는 본질적 멱등(idempotent read).
상태 전이 표
이 과제는 상태 머신을 도입하지 않는다(관측 스택). trace 샘플링 결정만 존재: sampling.probability=1.0 → 모든 요청 record. 별도 상태 전이 없음.
Component Diagram — 텔레메트리 파이프라인 (Mermaid flowchart LR)
flowchart LR subgraph App["Spring Boot (env 태그)"] MM["Micrometer Prometheus"] MT["Micrometer Tracing"] end MYSQLE["mysqld_exporter"] REDISE["redis_exporter"] KAFKAE["kafka_exporter"] COL["OTel Collector"] PROM["Prometheus"] TEMPO["Tempo"] LOKI["Loki"] GRAF["Grafana"] MM -->|scrape| PROM MYSQLE -->|scrape| PROM REDISE -->|scrape| PROM KAFKAE -->|scrape| PROM MT -->|OTLP trace/log| COL COL -->|trace| TEMPO COL -->|log| LOKI COL -->|remote_write| PROM PROM --> GRAF TEMPO --> GRAF LOKI --> GRAF
Sequence Diagram — 느린 요청 병목 식별 (User Scenario 1)
sequenceDiagram participant U as 운영자 participant G as Grafana participant P as Prometheus participant T as Tempo U->>G: Spring 대시보드 열람 (env=prod 필터) G->>P: PromQL P95 latency 조회 P-->>G: 느린 엔드포인트 식별 U->>G: 해당 trace 열람 (exemplar 점프) G->>T: trace_id 조회 T-->>G: span 트리 (DB/Gateway 구간) G-->>U: 병목 span 표시
ERD
해당 없음 — 이 과제는 DB 스키마·컬렉션을 변경하지 않는다(관측 스택 도입, 앱 데이터 모델 무변경). 선행 인프라 DB 작업 없음.
Testing Plan
앱 계측은 통합 검증 중심(비즈니스 로직 신규 없음). 인프라는 기동·엔드포인트·데이터 흐름 검증.
| 레벨 | 대상 | 범위 |
|---|---|---|
| build | build.gradle.kts | 의존성 해석·컴파일 성공, datadog 아티팩트 부재·prometheus/tracing-bridge-otel 존재 |
| infrastructure(통합) | actuator | /actuator/prometheus가 200 + jvm_memory_used_bytes·tomcat_threads_busy·executor_active(mcp-audit) 노출 |
| infrastructure(통합) | Kafka 관측 | Producer→Consumer 왕복에서 trace_id 전파(단일 trace) — Testcontainers Kafka + Tempo mock 또는 span 검증 |
| presentation(통합) | HTTP trace | 임의 API 호출 시 서버 로그 MDC에 trace_id/span_id 존재 |
| infra 검증(수동/스크립트) | compose | docker compose -f ... config 유효, 컨테이너 healthy, promtool check config 통과 |
| scenario(E2E) | 연결율 | 임의 10건 요청 → Tempo에서 trace 조회, 9건+ 연결(99% 기준) |
| scenario(E2E) | 대시보드 | Grafana 4종 대시보드 패널 렌더, Kafka UI lag 조회 |
핵심 실패 경로 시나리오:
- Collector 정지 상태에서 앱 요청 →
/actuator/prometheus는 여전히 200(장애 격리 검증). micrometer-registry-datadog잔존 → build 검증 실패(회귀 감지).- Kafka
isObservationEnabled=false로 되돌리면 Producer/Consumer trace 분리 → 연결율 저하 감지.
Release Scenario — 무중단 배포 (의무)
앱은 런타임 동작을 바꾸지 않는다(계측 추가·registry 교체). 인프라는 신규 컨테이너 추가라 기존 서비스 영향 없음. 리스크는 ① registry 교체 시 메트릭 노출 순간 단절 ② Datadog 잔재로 인한 부팅 실패다.
- 배포 순서(코드 먼저): 앱 계측 변경(BE wave) → 인프라 컨테이너 기동(INFRA). 앱이
/actuator/prometheus·OTLP 발신을 먼저 갖추고, 인프라는 아직 없어도 앱은 정상 부팅(OTLP export 실패는 로그 경고일 뿐 요청 처리 무영향). - 피처 플래그 등가 — OTLP 엔드포인트 미설정 시 no-op:
OTEL_EXPORTER_OTLP_ENDPOINT미주입이면 trace export만 비활성, 앱·메트릭 정상. Collector 준비 후 환경변수 주입으로 활성 → 사실상 플래그 ON/OFF. - registry 교체 무중단: Datadog는 기본 비활성(
DATADOG_ENABLED:false)이라 제거해도 활성 export 중단 없음. Prometheus registry는 추가 즉시/actuator/prometheus활성 — 순단 없음. - 단계별 전환 조건:
- BE wave 병합 → 앱 부팅 성공 +
/actuator/prometheus200 확인 → 다음. - INFRA wave 기동 → 컨테이너 healthy + Prometheus
up==1(앱·exporter) 확인 → 다음. - E2E 연결율 9/10 확인 → 완료.
- BE wave 병합 → 앱 부팅 성공 +
- 롤백 (단계별):
- trace 문제:
OTEL_EXPORTER_OTLP_ENDPOINT언셋 → export no-op(즉시, 앱 무영향). - registry 문제:
build.gradle.kts의존성 revert(datadog 복구) — 단일 PR revert. - 인프라 문제:
docker compose -f docker-compose.observability.yml down— 앱은 계속 동작(scrape 대상이 사라질 뿐). - 포트 충돌(Open Q1): 데이터 인프라 포트를
.env로 오버라이드하거나docker-compose.infra.yml만 down.
- trace 문제:
Observability (본 과제 자체가 관측 체계)
| 관측 지표 | 원천 | 대시보드 |
|---|---|---|
| MySQL cpu/mem/slow query/connection | mysqld_exporter | MySQL 대시보드 |
| Redis cpu/mem | redis_exporter | Redis 대시보드 |
| Kafka cpu/mem/lag/partition | kafka_exporter + Kafka UI | Kafka 대시보드 |
| Spring cpu/mem/gc/heap/tomcat·async threadpool | Micrometer /actuator/prometheus | Spring 대시보드 |
| 수집기 헬스 | Prometheus self-scrape up | (Spring/공용 패널) |
- 알람 자체는 [지능형 장애 알림 ⑥] 소관 — 본 과제는 데이터소스(Prometheus/Loki/Tempo) 제공까지.
env태그로 dev/prod 필터(모든 패널 변수화).
형제 과제 접점 (⑥⑦⑧)
| 과제 | 접점 | 경계 |
|---|---|---|
| ⑥ 지능형 장애 알림 | 본 과제의 Prometheus/Loki/Tempo를 알림·원인분석 데이터소스로 사용 | 본 과제=데이터소스 제공, ⑥=P95 임계 알림·Discord. 본 과제 완료가 ⑥ 선행 |
| ⑦ 다중 인스턴스 + LB | compose 공동 수정, 앱 인스턴스가 N개로 늘면 scrape 대상도 N개 | 본 과제=/actuator/prometheus 노출·prometheus.yml 잡 구조 제공(다중 타깃 확장 가능하게 설계). ⑦=docker-compose.scale.yml로 인스턴스·nginx 추가 |
| ⑧ dev/prod 환경 분리 | ① env(APP_ENV) 값 체계 정의 ② 데이터스토어(MySQL/MongoDB/Redis/Kafka) base compose 단독 소유(senior-pm #4) | 본 과제=env 태그 키 재사용(신규 키 0), 데이터스토어를 정의하지 않고 ⑧ 서비스(mysql/mongodb/redis/kafka)에 exporter를 붙임. ⑧=local/dev/prod 값 의미·base compose(데이터스토어)·.dev.yml/.prod.yml 소유. ⑤ exporter는 ⑧ base compose 기동을 런타임 선행 조건으로 요구 |
Open Questions
- (PRD Open Q1) 데이터스토어 포트 충돌 → 데이터스토어가 ⑧ 소유로 이관됐으므로 포트·오버라이드 정책도 ⑧ base compose 소관. ⑤ exporter는 ⑧이 정한 서비스명/포트에 붙기만 한다. INFRA-05 문서는 ⑧ base compose 기동 선행을 안내.
- (PRD Open Q2) dev 관측 스택 상시 vs 배포 직후만 → ⑧ 소관으로 위임(본 과제는 compose 파일만 제공, 기동 정책은 ⑧). 기본은 상시 기동 가능하게 설계.
- exemplar(FR-11 P2)는 Prometheus
--enable-feature=exemplar-storage+ Micrometermanagement.prometheus.metrics.export.properties로 활성 — INFRA-03/BE-02에서 옵션 제공하되 P2라 검증은 best-effort. - FR-3 로그→Loki 경로의 Spring Boot 3.3.5 제약 (구현 중 발견):
management.otlp.logging(앱 OTLP 로그 export 자동 구성)은 Spring Boot 3.4+에서만 제공된다. 3.3.5에서는 logbackOpenTelemetryAppender를 등록해도 SDKLoggerProvider/OTLP 로그 exporter가 자동 구성되지 않아 앱→Loki 직접 OTLP 로그 전송이 no-op이다. 현재 확보: 콘솔 로그에traceId/spanIdMDC 노출(trace 상관), trace→Tempo 정상. 후속(별도 티켓): ① Spring Boot 3.4+ 업그레이드로management.otlp.logging사용, 또는 ②SdkLoggerProvider+OtlpHttpLogRecordExporter빈 수동 구성 +OpenTelemetryAppender.install(), 또는 ③ Grafana Alloy/Promtail로 컨테이너 stdout→Loki 수집(앱 무변경, 버전 무관 — 권장). 로그 상관은 3방안 중 택1로 완성한다. trace 연결율 99%(FR-2, 핵심 NFR)는 본 과제에서 충족.
Document History
| 날짜 | 변경 내용 |
|---|---|
| 2026-07-03 | 최초 작성 — 실측 AS-IS(registry 부재로 /actuator/prometheus 404, env 태그 기존), OTel 계측=Micrometer Tracing 결정, scrape/OTLP 경로 분리, compose 파일 분리 전략, 무중단 배포, ⑥⑦⑧ 접점 |
| 2026-07-03 | senior-pm 정합 보완 #4 — 데이터스토어 단일 소유자=⑧ base compose로 확정, ⑤는 데이터스토어 정의 제거·관측 전용 compose 2개(observability.yml+observability-agents.yml)로 재편, 서비스명 mongodb 통일. #5 — service.name 규약 명시(BE=sports-application, web=sports-web), Grafana/Tempo 라벨 구분 |