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).
This commit is contained in:
Claude
2026-09-18 09:32:34 +00:00
parent 320733265f
commit 03bdf7ee87
28 changed files with 1027 additions and 2 deletions
@@ -0,0 +1,16 @@
package com.ankurm.observability;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Companion app for docs/00-migrating-to-opentelemetry-starter.md. Run with `scripts/run.sh` --
* Boot's Docker Compose support (compose.yaml, grafana/otel-lgtm) auto-wires the OTLP metrics,
* traces and logs exporters with zero management.otlp.* properties in application.yml.
*/
@SpringBootApplication
public class ObservabilityApplication {
public static void main(String[] args) {
SpringApplication.run(ObservabilityApplication.class, args);
}
}
@@ -0,0 +1,22 @@
package com.ankurm.observability;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.aop.ObservedAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Without this bean, @Observed is inert: Spring Boot's autoconfiguration wires up an
* ObservationRegistry, but it does NOT register an ObservedAspect for you, even with
* spring-boot-starter-aspectj (AspectJ weaver) on the classpath and Micrometer Tracing active.
* See docs/output/03-observed-needs-explicit-bean.txt for the trace showing only the HTTP server
* span before this bean existed, and docs/02-observation-api.md for the correction.
*/
@Configuration(proxyBeanMethods = false)
class ObservationConfig {
@Bean
ObservedAspect observedAspect(ObservationRegistry registry) {
return new ObservedAspect(registry);
}
}
@@ -0,0 +1,39 @@
package com.ankurm.observability;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.observation.annotation.Observed;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
/**
* Real traffic source for docs/output/01-metrics-and-traces-in-lgtm.txt. Every call increments
* a Micrometer counter (exported as an OTLP metric) and, via @Observed, opens a trace span --
* one recording, two signals, exactly as described in
* https://ankurm.com/git.app/asmhatre/spring-boot-demo/src/branch/main/observability/docs/02-observation-api.md
*/
@RestController
class OrderController {
private final Counter ordersPlaced;
OrderController(MeterRegistry registry) {
this.ordersPlaced = Counter.builder("orders.placed")
.description("Number of orders placed")
.tag("channel", "web")
.register(registry);
}
@Observed(name = "place-order", contextualName = "order-controller#placeOrder")
@PostMapping("/orders/{id}")
Map<String, Object> placeOrder(@PathVariable String id) throws InterruptedException {
ordersPlaced.increment();
// Simulate a bit of real work so the trace span has non-zero duration.
Thread.sleep(ThreadLocalRandom.current().nextInt(5, 40));
return Map.of("id", id, "status", "placed");
}
}
@@ -0,0 +1,31 @@
server:
port: 8080
spring:
application:
name: order-service
management:
endpoints:
web:
exposure:
include: health
metrics:
tags:
application: ${spring.application.name}
# No management.otlp.* export properties here on purpose -- see docs/03-otel-env-vars.md.
# In development, Boot's Docker Compose support (see compose.yaml) auto-configures the OTLP
# export endpoints for metrics, traces and logs the moment it detects the grafana/otel-lgtm
# image, without a single management.otlp.* property in this file.
# management.tracing.sampling.probability defaults to 0.10 -- see docs/output/02-low-sampling-demo.txt
# for what that does to a short burst of local traffic, and the "fulltrace" profile below for the fix.
---
spring:
config:
activate:
on-profile: fulltrace
management:
tracing:
sampling:
probability: 1.0
@@ -0,0 +1,20 @@
package com.ankurm.observability;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
/**
* Docker Compose support is off by default in tests (Boot skips it under `mvn test`), so this
* just confirms the application context -- including the OpenTelemetry starter's autoconfiguration
* and the explicit ObservedAspect bean in ObservationConfig -- wires up cleanly without a running
* collector. The real, docker-backed proof (metrics and traces actually landing in a live Grafana
* LGTM stack) is in scripts/run-all.sh and docs/output/, which need Docker and take real wall time
* -- not something to run on every `mvn test`.
*/
@SpringBootTest
class ObservabilityApplicationTests {
@Test
void contextLoads() {
}
}