# 1. Migrating to spring-boot-starter-opentelemetry [README](../README.md) | Next: [2. The Observation API and @Observed](02-observation-api.md) ## 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: ```xml org.springframework.boot spring-boot-starter-opentelemetry ``` 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](https://ankurm.com/micrometer-opentelemetry-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`](../compose.yaml) names one image: ```yaml 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`](https://github.com/grafana/docker-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`](../src/main/resources/application.yml) has no `management.otlp.*` properties in it at all. Running `./scripts/run.sh` (which is `mvn spring-boot:run`) produces: ```console 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](output/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: ```console $ 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](output/01-metrics-and-traces-in-lgtm.txt), source: [`OrderController.java`](../src/main/java/com/ankurm/observability/OrderController.java)) Next: [2. The Observation API and @Observed](02-observation-api.md) -- where the same request that produced this metric turns out, on its own, to produce only half the trace you'd expect.