Files
spring-boot-demo/observability/docs/01-migrating-to-opentelemetry-starter.md
T
Claude 03bdf7ee87 Add observability: real OTLP metrics/traces to grafana/otel-lgtm, Docker Compose auto-wiring, and a dual-version (Boot 4.0 vs 4.1) proof of the new OTEL_* env var support
Companion module for the rewritten ankurm.com Prometheus/Grafana monitoring post. Verified
against a real running grafana/otel-lgtm container (not mocked): 8 real requests produce a real
orders_placed_total metric queried back from the bundled Prometheus-compatible API with zero
management.otlp.* properties, auto-wired entirely by Boot's Docker Compose service-connection
detection. Two real findings surfaced along the way and documented rather than smoothed over:
@Observed silently produces no span without an explicit ObservedAspect bean (AspectJ weaving
alone is not sufficient, despite Micrometer Tracing being active), and OTEL_EXPORTER_OTLP_ENDPOINT
already worked on Boot 4.0 via Micrometer's own OtlpConfig fallback -- what's actually new in 4.1
is the rest of the standard OTEL_* surface (verified with OTEL_METRIC_EXPORT_INTERVAL against
identical source compiled on both Boot 4.0.8 and 4.1.1).

Also fixes the root README's module table, which was missing a row for
resilience4j-circuit-breaker (added in a previous commit but never indexed here).
2026-09-18 09:32:34 +00:00

4.7 KiB

1. Migrating to spring-boot-starter-opentelemetry

README | Next: 2. The Observation API and @Observed

What the old post 4700 used, and what replaces it

The original version of this article's post built a Prometheus + Grafana stack by hand: add micrometer-registry-prometheus, expose /actuator/prometheus, run a Prometheus container with a hand-written prometheus.yml scrape config pointed at host.docker.internal:8080, run a Grafana container, wire up a data source, import a community dashboard by ID. That is still a completely valid way to run Prometheus and Grafana, and nothing about it stopped working on Spring Boot 4.1.

What's new is a fourth option, built by the Spring team and shipped as its own starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>

One dependency replaces micrometer-registry-prometheus (or any other vendor-specific registry) plus a tracing bridge. It pulls in micrometer-registry-otlp and micrometer-tracing-bridge-otel, and switches the whole export model from pull (something scrapes /actuator/prometheus on a timer) to push (the app itself POSTs metrics and traces, in one vendor-neutral wire format, OTLP, to wherever you point it). This repo's module, observability/, uses this starter exclusively -- the deep, beginner-to-advanced treatment of Micrometer versus OpenTelemetry, the Observation API, cardinality, and context propagation already exists on this blog: Micrometer to OpenTelemetry: The Spring Boot 4 Observability Guide. This module exists to verify one specific, narrower claim that guide states in passing but does not itself demonstrate end to end: that Boot 4.1 will auto-wire the OTLP export path against a real local collector with zero management.otlp.* properties, using nothing but Docker Compose.

Docker Compose does the wiring, verified

compose.yaml names one image:

services:
  lgtm:
    image: grafana/otel-lgtm:latest
    ports:
      - "3000:3000"   # Grafana UI
      - "4317:4317"   # OTLP gRPC ingest
      - "4318:4318"   # OTLP HTTP ingest
      - "9090:9090"   # Prometheus-compatible query API
      - "3200:3200"   # Tempo query API (traces)

grafana/otel-lgtm is a single image bundling Loki, Grafana, Tempo and (Mimir-backed) Prometheus, plus an OTLP collector endpoint -- the exact image the Spring team's own OpenTelemetry starter documentation uses as its local-development example. application.yml has no management.otlp.* properties in it at all. Running ./scripts/run.sh (which is mvn spring-boot:run) produces:

DockerComposeLifecycleManager : Using Docker Compose file .../compose.yaml
DockerCli :  Container obs-module-lgtm-1 Starting
DockerCli :  Container obs-module-lgtm-1 Started
DockerCli :  Container obs-module-lgtm-1 Healthy
PushMeterRegistry : Publishing metrics for OtlpMeterRegistry every 1m to http://127.0.0.1:4318/v1/metrics with resource attributes {service.name=order-service}

(00-docker-compose-auto-wiring.txt) The important detail is 127.0.0.1, not localhost. Micrometer's own OtlpConfig defaults already point at localhost:4318 with nothing configured at all, so an app that happens to reuse the default OTLP port could look auto-wired when it is actually just coincidental. 127.0.0.1 is what Boot's Docker Compose service-connection support specifically resolves the container to; if you see localhost instead, the auto-wiring did not actually happen and you are looking at the bare Micrometer default.

Real traffic, real metrics, real backend

8 real HTTP requests to a real running server, queried back out of the real Prometheus-compatible API bundled inside the container -- not the app's own /actuator/prometheus, and no scraping involved at all, since this is push, not pull:

$ curl -s "http://localhost:9090/api/v1/query?query=orders_placed_total"
{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"orders_placed_total", ...},"value":[..., "8"]}]}}

(01-metrics-and-traces-in-lgtm.txt, source: OrderController.java)

Next: 2. The Observation API and @Observed -- where the same request that produced this metric turns out, on its own, to produce only half the trace you'd expect.