옵저버빌리티 스택 도입 TDD

Background

근거 PRD: /Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/옵저버빌리티 스택 도입/PRD.md (검수 PASS) 선행 컨텍스트: ../도메인 경계 재설계/TDD.md (5계층 컨텍스트 맵·의존 규칙)

sports-application backend는 계측이 부분적으로 존재한다 — 실측:

  • backend/build.gradle.kts:37 io.micrometer:micrometer-registry-datadog (유료 SaaS registry). Prometheus registry 의존성은 부재.
  • backend/build.gradle.kts:41 spring-boot-starter-actuator 존재.
  • application.yml:156 management.endpoints.web.exposure.include: health,info,prometheus,metricsprometheus 엔드포인트가 노출 목록에 있으나 micrometer-registry-prometheus가 없어 /actuator/prometheus는 실제로 404다(Micrometer가 PrometheusMeterRegistry 빈을 만들지 못함).
  • application.yml:160-167 management.datadog.metrics.export(DATADOG_ENABLED:false 기본 비활성).
  • application.yml:168-172 management.metrics.tags: application, env: ${APP_ENV:local}, service: mcpenv 태그 체계는 이미 존재.
  • 분산 추적(OpenTelemetry)은 전무. NotificationEventWorker(Kafka Consumer, KafkaListenerContainerFactory#kafkaListenerContainerFactoryKafkaProducerConfig가 존재하나 trace 전파 계측 없음.
  • 루트 docker-compose.ymlminio·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-datadogmicrometer-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

용어정의
LGTMLoki(로그)·Grafana(시각화)·Tempo(트레이스)·Mimir/Prometheus(메트릭) 스택
OTLPOpenTelemetry Protocol — 텔레메트리 전송 표준(gRPC 4317 / HTTP 4318)
CollectorOTLP를 수신해 백엔드별로 라우팅하는 게이트웨이 프로세스
Micrometer TracingSpring 계측 추상화(Observation API), OTel Bridge로 OTLP 전송
scrapePrometheus가 대상의 /metrics를 주기적으로 pull하는 수집 방식
exporter인프라(MySQL/Redis/Kafka) 상태를 Prometheus 포맷 /metrics로 노출하는 사이드카
exemplar메트릭 샘플에 trace_id를 연결해 메트릭→트레이스 점프를 가능케 하는 메타데이터
resource attributetrace/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 Bridgemicrometer-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 병합 시 중복 서비스 발생(특히 mongomongodb로 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 :8888Tempo, 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 :3000Prometheus, Tempo, Loki
mysqld_exporter컨테이너(⑤ 소유)MySQL 메트릭 노출cpu/mem/slow query/connection/metrics :9104mysql
redis_exporter컨테이너(⑤ 소유)Redis 메트릭 노출cpu/mem/metrics :9121redis
kafka_exporter컨테이너(⑤ 소유)Kafka 메트릭 노출cpu/mem/lag/partition/metrics :9308kafka
Kafka UI컨테이너(⑤ 소유)lag·partition 조회 UI토픽·컨슈머그룹 조회UI :8089kafka
데이터스토어(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/prometheusmanagement.metrics.tags.env
앱 trace 발신otel-collector:4318 (/v1/traces)OTLP HTTP (Spring Boot 3.3.5 제약 — gRPC 미지원, 4318 사용)resource deployment.environment
Collector selfotel-collectorotel-collector:8888/metrics
MySQL exportermysqld-exportermysqld-exporter:9104/metricsPrometheus relabel env
Redis exporterredis-exporterredis-exporter:9121/metricsPrometheus relabel env
Kafka exporterkafka-exporterkafka-exporter:9308/metricsPrometheus relabel env
Prometheus selfprometheusprometheus:9090/-/healthy, /metrics
Grafana datasourcegrafanaPrometheus 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 BEsports-applicationMicrometer Tracing OTLP resource본 과제(BE-02)
web FE (Next.js 서버 런타임)sports-web@vercel/otel OTEL_SERVICE_NAMEdesign-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.ktsbuildmicrometer-registry-datadog 제거, micrometer-registry-prometheus·micrometer-tracing-bridge-otel·opentelemetry-exporter-otlp·opentelemetry-logback-appender-1.0 추가FR-1/FR-2
application.ymlconfigmanagement.datadog.* 제거, management.tracing·management.otlp.tracing 추가, prometheus 노출·env 태그 유지FR-1/FR-2/FR-8
KafkaConsumerConfig.ktinfrastructurefactory.containerProperties.isObservationEnabled = trueKafka Consumer trace 전파(연결율 99%)
KafkaProducerConfig.ktinfrastructureKafkaTemplate.setObservationEnabled(true)Producer→Consumer trace 전파
AsyncConfig.kt(+메트릭 바인딩)infrastructuremcpAuditExecutorExecutorServiceMetrics로 바인딩FR-5 Async threadpool active/max
logback-spring.xml(신규)configOTLP 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

앱 계측은 통합 검증 중심(비즈니스 로직 신규 없음). 인프라는 기동·엔드포인트·데이터 흐름 검증.

레벨대상범위
buildbuild.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 검증(수동/스크립트)composedocker 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 활성 — 순단 없음.
  • 단계별 전환 조건:
    1. BE wave 병합 → 앱 부팅 성공 + /actuator/prometheus 200 확인 → 다음.
    2. INFRA wave 기동 → 컨테이너 healthy + Prometheus up==1(앱·exporter) 확인 → 다음.
    3. E2E 연결율 9/10 확인 → 완료.
  • 롤백 (단계별):
    • 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.

Observability (본 과제 자체가 관측 체계)

관측 지표원천대시보드
MySQL cpu/mem/slow query/connectionmysqld_exporterMySQL 대시보드
Redis cpu/memredis_exporterRedis 대시보드
Kafka cpu/mem/lag/partitionkafka_exporter + Kafka UIKafka 대시보드
Spring cpu/mem/gc/heap/tomcat·async threadpoolMicrometer /actuator/prometheusSpring 대시보드
수집기 헬스Prometheus self-scrape up(Spring/공용 패널)
  • 알람 자체는 [지능형 장애 알림 ⑥] 소관 — 본 과제는 데이터소스(Prometheus/Loki/Tempo) 제공까지.
  • env 태그로 dev/prod 필터(모든 패널 변수화).

형제 과제 접점 (⑥⑦⑧)

과제접점경계
⑥ 지능형 장애 알림본 과제의 Prometheus/Loki/Tempo를 알림·원인분석 데이터소스로 사용본 과제=데이터소스 제공, ⑥=P95 임계 알림·Discord. 본 과제 완료가 ⑥ 선행
⑦ 다중 인스턴스 + LBcompose 공동 수정, 앱 인스턴스가 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 + Micrometer management.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에서는 logback OpenTelemetryAppender를 등록해도 SDK LoggerProvider/OTLP 로그 exporter가 자동 구성되지 않아 앱→Loki 직접 OTLP 로그 전송이 no-op이다. 현재 확보: 콘솔 로그에 traceId/spanId MDC 노출(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-03senior-pm 정합 보완 #4 — 데이터스토어 단일 소유자=⑧ base compose로 확정, ⑤는 데이터스토어 정의 제거·관측 전용 compose 2개(observability.yml+observability-agents.yml)로 재편, 서비스명 mongodb 통일. #5 — service.name 규약 명시(BE=sports-application, web=sports-web), Grafana/Tempo 라벨 구분