모놀리식 Spring Boot 애플리케이션에 OpenTelemetry와 Grafana Tempo를 적용해, 단일 프로세스 안에서 요청이 계층(Controller → Service → Inventory → Payment → Repository → DB)을 따라 흐르는 모습을 trace / metrics / logs로 관측할 수 있는지 검증하는 실험 프로젝트다.
MSA가 아니다. 목적은 "모놀리식 내부 요청 흐름을 어떻게 관측하는가"를 보여주는 것이다.
이 저장소는 교재처럼 읽고 실습할 수 있게 구성되어 있다. 처음이라면 study-guide부터.
| 문서 | 내용 |
|---|---|
| docs/study-guide.md | 읽기 순서 · 실습 체크포인트 9개 · 연습문제 6개 · FAQ |
| docs/architecture.md | 구성도 — 토폴로지, 계층, 요청 시퀀스, 설정 파일 지도 |
| docs/observability-deep-dive.md | 동작 원리 — @Observed가 Tempo의 span이 되기까지 |
| .workspace/ | 설계 결정 기록(ADR 8종) · 스펙 · 검증 결과 — "왜 이렇게 만들었나" |
flowchart LR
Client[Client] --> App[Monolith Spring Boot App]
App --> DB[(PostgreSQL)]
App -- OTLP --> Collector[OpenTelemetry Collector]
Collector --> Tempo[Grafana Tempo]
Prometheus[Prometheus] -- scrape /actuator/prometheus --> App
Grafana[Grafana] --> Tempo
Grafana --> Prometheus
| 영역 | 선택 |
|---|---|
| Language / Framework | Java 21, Spring Boot 3 (Spring MVC) |
| Build | Gradle (Groovy) + Wrapper |
| Persistence | Spring Data JPA (Hibernate) |
| Database | PostgreSQL |
| Tracing | Micrometer Observation + Micrometer Tracing (OpenTelemetry bridge) → OTLP |
| Trace backend | Grafana Tempo |
| Metrics | Micrometer + Spring Boot Actuator (Prometheus registry) |
| Dashboard | Grafana (datasource + 대시보드 자동 provisioning) |
| Alerting | Grafana managed alerting (span metrics 기반 실험 rule) |
| Logging | Logback structured JSON (trace_id / span_id 포함) |
- Trace: 각 계층 메서드를
@Observed로 감싸 span을 만든다. Micrometer Observation이 span을 만들고, Micrometer Tracing(OTel bridge)이 OTLP(HTTP,:4318/v1/traces)로 Collector에 보낸다. Collector는 Tempo로 trace를 전달한다. HTTP 요청 자체는 Spring MVC가 자동으로 root span(http.server.requests)을 만든다. - Metrics: 앱 자체 metric은 Actuator
/actuator/prometheus로 노출하고 Prometheus가 직접 scrape한다. 추가로 Tempo metrics-generator가 trace span에서traces_spanmetrics_*RED metrics를 생성해 Prometheus에 remote write한다. 즉, metric은 "앱 계측 기반"과 "trace 파생 기반" 두 경로를 함께 볼 수 있다. - Logs: Logback이 JSON으로 출력하며, Micrometer Tracing이 MDC에 넣은
traceId/spanId를trace_id/span_id필드로 노출해 로그와 trace를 연결한다.
make up # docker compose up --build (app, postgres, collector, tempo, prometheus, grafana)기동 후:
curl localhost:10080/healthz # {"status":"ok"}Docker 런타임이 필요하다. Docker Desktop을 실행하거나 colima를 사용한다면
colima start후 진행한다. Makefile이docker compose(플러그인)와docker-compose(단독 바이너리)를 자동 감지한다. 다른 런타임은make up COMPOSE="nerdctl compose"처럼 override할 수 있다. App의 기본 host port는10080이다. 컨테이너 내부 포트는 계속8080이며, 필요하면make up APP_PORT=19090처럼 host port만 바꿀 수 있다.
make load # 정상 주문 20건 + 실패 주문(fail_payment=true) 1건http://localhost:3000 (admin / admin)
- Explore → Tempo: 최근 trace를 검색해
POST /orders요청을 연다. - Dashboards → monolith-otel-lab: 요청 수/지연, 주문 생성/실패 카운트, span metrics 기반 RED 지표.
- Alerting → Alert rules: 결제 실패 span이 들어오면
Payment authorization span errorsrule이 Firing 되는지 확인.
정상 주문 trace 구조 (Tempo에 표시되는 실제 span 이름):
http post /orders
└─ order-controller.create-order
└─ order-service.create-order
├─ inventory-service.reserve
├─ payment-client.authorize
└─ order-repository.insert
⚠️ span 이름은 코드의OrderService.createOrder가 아니라 kebab-case로 표시된다 — Micrometer가 이름을 정규화하기 때문이다. Tempo에서 검색할 때 주의. (원리 설명)
실패 주문(POST /orders?fail_payment=true) trace에서는 payment-client.authorize span이 error 상태이고,
order-repository.insert는 실행되지 않는다.
JSON 로그에 trace_id/span_id가 포함되어 Tempo의 trace와 연결할 수 있다.
이 프로젝트는 Grafana managed alert rule을 provisioning한다. 외부 Slack/Email/Webhook 전송은 붙이지 않고, Grafana UI/API에서 alert 상태 전이를 확인하는 실험용 구성이다.
make load이 명령은 마지막에 POST /orders?fail_payment=true를 1회 보내므로,
Tempo span metrics의 payment-client.authorize error count가 증가한다. Grafana에서:
Alerting → Alert rules → Payment authorization span errors
를 열면 10~30초 안에 Firing 상태를 확인할 수 있다. API로도 확인 가능하다.
curl -fsS -u admin:admin http://localhost:3000/api/alertmanager/grafana/api/v2/alerts \
| jq '.[] | select(.labels.alertname=="Payment authorization span errors") | {labels,status}'rule은 최근 2분의 결제 실패 span 증가분을 보기 때문에, 새 실패 요청이 없으면 시간이 지나며 다시 Normal로 돌아간다.
Docker Compose와 같은 관측성 스택을 로컬 Kubernetes에서도 실험할 수 있다.
Kubernetes 예제는 kind 기준이며, Grafana provisioning은 compose에서 쓰는 dashboard/datasource/alert 파일을 재사용한다.
Compose stack과 같은 host port(
10080,3000,9090)를 사용한다. 이미 compose를 실행 중이면 먼저make down으로 정리한다.
namespace는 목적별로 분리한다.
| Namespace | Purpose | Resources |
|---|---|---|
monolith-otel-app |
애플리케이션 런타임 | Spring Boot app Service/Deployment |
monolith-otel-data |
데이터 저장소 | PostgreSQL Service/Deployment |
monolith-otel-observability |
관측성 플랫폼 | OpenTelemetry Collector, Tempo, Prometheus, Grafana |
namespace가 다르기 때문에 app은 postgres.monolith-otel-data.svc.cluster.local,
otel-collector.monolith-otel-observability.svc.cluster.local처럼 namespace-qualified DNS로 접근한다.
make k8s-up
curl -fsS http://localhost:10080/healthz
make k8s-load
make k8s-status확인 URL:
App: http://localhost:10080
Grafana: http://localhost:3000 (admin/admin)
Prometheus: http://localhost:9090
정리:
make k8s-down애플리케이션 코드 변경을 빠르게 확인할 때 사용한다.
make test기대 결과:
BUILD SUCCESSFUL
테스트 범위는 단위 테스트, MockMvc web slice, JPA(H2) slice, Spring context-load다. 실제 PostgreSQL/Tempo/Grafana 연동은 아래 통합 테스트로 확인한다.
로컬 기본 실습 경로다.
make up
curl -fsS http://localhost:10080/healthz
make load기대 결과:
{"status":"ok"}
sent 20 successful orders and 1 failing payment request
Prometheus에서 앱 metric과 span metrics를 확인한다.
curl -fsS -G 'http://localhost:9090/api/v1/query' \
--data-urlencode 'query=order_created_count_total'
curl -fsS -G 'http://localhost:9090/api/v1/query' \
--data-urlencode 'query=sum by (span_name, status_code) (traces_spanmetrics_calls_total)'Grafana provisioning도 API로 확인할 수 있다.
curl -fsS -u admin:admin http://localhost:3000/api/datasources
curl -fsS -u admin:admin http://localhost:3000/api/v1/provisioning/alert-rules브라우저에서는 다음을 확인한다.
Grafana → Explore → Tempo → http post /orders trace
Grafana → Dashboards → monolith-otel-lab
Grafana → Alerting → Payment authorization span errors
정리:
make downkind 기반 로컬 Kubernetes 경로다. Compose와 같은 host port를 쓰므로, 먼저 Compose stack을 내려둔다.
make down
make k8s-dry-run
make k8s-up
make k8s-status
curl -fsS http://localhost:10080/healthz
make k8s-load기대 결과:
6 pods Running/Ready across 3 namespaces
{"status":"ok"}
sent 20 successful orders and 1 failing payment request
Kubernetes 환경에서도 같은 URL에서 확인한다.
App: http://localhost:10080
Grafana: http://localhost:3000 (admin/admin)
Prometheus: http://localhost:9090
Tempo API를 직접 보고 싶으면 짧게 port-forward를 연다.
kubectl -n monolith-otel-observability port-forward svc/tempo 3200:3200
curl -fsS -G 'http://localhost:3200/api/search' \
--data-urlencode 'q={name="http post /orders"}' \
--data-urlencode 'limit=5'정리:
make k8s-down| Method | Path | 설명 |
|---|---|---|
| GET | /healthz |
{"status":"ok"} |
| POST | /orders |
주문 생성 (201) |
| POST | /orders?fail_payment=true |
결제 실패 시뮬레이션 (402) |
| GET | /orders/{order_id} |
주문 조회 (200 / 404) |
요청/응답 예시:
curl -X POST localhost:10080/orders -H 'Content-Type: application/json' \
-d '{"user_id":"user-1","items":[{"sku":"item-1","quantity":2}]}'
# -> {"order_id":"...","status":"created"}make test # ./gradlew test (단위 + web 슬라이스 + JPA(H2) 슬라이스 + context-load)
make build # ./gradlew bootJar테스트는 빠른 피드백을 위해 H2를 사용한다. 실제 PostgreSQL 동작은
make up통합 스택에서 검증한다.
app: http://localhost:10080
grafana: http://localhost:3000 (admin/admin)
prometheus: http://localhost:9090
postgres: localhost:5432
otel-collector: localhost:4317 (gRPC), 4318 (HTTP)
tempo: internal only
Prometheus는 Docker 네트워크 내부에서 app:8080을 scrape하므로 host port 변경의 영향을 받지 않는다.
Tempo metrics-generator는 span metrics를 Prometheus의 /api/v1/write로 remote write한다.
src/main/java/com/sangjinsu/monolithotellab
├─ web REST controller, exception handler, request log filter
├─ order service, JPA entity, repository, dto
├─ inventory fake inventory service
├─ payment fake payment client
└─ platform/observability @Observed 활성화(ObservedAspect)
deploy/ docker compose/k8s 관측성 설정, otel-collector, tempo, prometheus, grafana provisioning + dashboard
docs/ 학습 문서 — 구성도 · 동작 원리 · 학습 가이드 (+ Grafana 스크린샷)
.workspace/ LLM 작업 위키 — 스펙, ADR 8종, 계획, 검증 기록 (읽는 법: .workspace/README.md)
- 실험 프로젝트이므로 인증/인가, 실제 결제 연동, 운영 수준 구성은 포함하지 않는다.
- 설계 결정과 변경 이력은
.workspace/에 기록되어 있다 (AGENTS.md가 최상위 스펙).