Spring Boot 4 Keeps a Tracer With Tracing Off
Contents
Spring Boot 4.0's release notes cover it in one bullet, under Miscellaneous: "The property management.tracing.enabled has been renamed to management.tracing.export.enabled." The old key doesn't forward to the new one. It doesn't fail startup, and in the run I have it doesn't log a word. Set it to false and it binds to nothing.
I found it while chasing a red test on Boot 4.1.2, and the dead key wasn't what turned the test red. The test was wrong: with the new key, the Tracer bean stays too.
A gate test expected no Tracer#
The study behind this series started from a friend's question: can a Spring Boot service move to GraalVM native and keep its Grafana dashboards and alerts? Native is in the question, so every telemetry mode is its own build. Boot's native-image docs are blunt about runtime switches: "Properties that change if a bean is created are not supported (for example, @ConditionalOnProperty and .enabled properties)." A switch that silently does nothing would poison every number after it. So each build runs ModeGateTest, which asserts the registries and tracers that mode should have.
The metrics-only build, mo-m, turned tracing off with the key Boot 3 read: management.tracing.enabled: false. Its gate test expected no io.micrometer.tracing.Tracer bean and found one:
[ERROR] dev.tsvinc.pipeline.ModeGateTest.buildIsTheModeItClaims -- Time elapsed: 0.410 s <<< FAILURE!
org.opentest4j.AssertionFailedError: mo-m: expected no io.micrometer.tracing.Tracer bean ==> expected: <[]> but was: <[micrometerOtelTracer]>
Nothing in that run's log mentions the key: no warning, no deprecation notice. That covers one run at the log levels it happened to use. It doesn't show that Boot never says anything.
Deprecation level error: the key is not bound#
The release notes say renamed. Boot's configuration metadata says what kind of rename. This is the entry inside spring-boot-micrometer-tracing-4.1.2.jar:
{
"name": "management.tracing.enabled",
"deprecated": true,
"deprecation": {
"level": "error",
"replacement": "management.tracing.export.enabled",
"since": "4.0.0"
}
}
The 4.0.0 metadata already carries the same entry at level: error, so this has held since the first Boot 4 release. The field that matters is level. Boot's metadata format spec spells it out: at warning, the default, a property "should still be bound in the environment". But "when it has an error deprecation level, the property is no longer managed and is not bound."
So the key sits in the Environment with nobody reading it. The condition behind the new switch, OnEnabledTracingExportCondition, reads management.tracing.export.enabled and management.tracing.export.<name>.enabled. In Boot 3.5.16 the same job belonged to OnEnabledTracingCondition, with management.tracing.enabled as its global key. None of the 50 Boot 4.1.2 jars in my Maven cache has a class containing the old string.
Boot ships a tool for exactly this trap. The 4.0 migration guide points to spring-boot-properties-migrator, which "will not only analyze your application's environment and print diagnostics at startup, but also temporarily migrate properties at runtime for you." This app never had it on the classpath, and I never ran it, so which of its reports this key would land in stays open.
The replacement key keeps the Tracer too#
Swapping in management.tracing.export.enabled: false doesn't remove the bean either. This is the one from the failure, in OpenTelemetryTracingAutoConfiguration at v4.1.2:
@Bean
@ConditionalOnMissingBean(io.micrometer.tracing.Tracer.class)
OtelTracer micrometerOtelTracer(Tracer tracer, EventPublisher eventPublisher,
OtelCurrentTraceContext otelCurrentTraceContext) {
One condition: no Micrometer Tracer bean yet. Nothing about export, on the method or on the class. And when no other configuration supplies a Micrometer Tracer, NoopTracerAutoConfiguration registers Tracer.NOOP. With Micrometer Tracing on the classpath, Boot hands you a Tracer bean one way or the other.
The export switch gates something else. A byte scan of the same 50 jars finds four classes that use @ConditionalOnEnabledTracingExport. They are the OTLP exporter configuration, the Zipkin auto-configuration and two propagation configurations. The property's own description says it in words: "Whether auto-configuration of tracing is enabled to export and propagate traces." Export and propagation. Not the tracer.
Propagation is the half that's easy to miss. In OpenTelemetryPropagationConfigurations, both propagator beans carry the export condition, so with export off Boot falls back to TextMapPropagator.noop(), and the service would stop passing trace headers along. That's a source reading. I haven't tested it across two services.
And the old key, back when it worked? Boot 3.5.16 describes management.tracing.enabled with that exact sentence, and its micrometerOtelTracer carries the same single condition. If I read that source right, my test would have failed on Boot 3 too, with the key fully alive. The 4.0 name is the honest one.
Metrics only on Boot 4: export off, sampling zero#
The red run's log is stamped 00:12 on 24 October. The fix touched the config and the test, and the commit carrying both went in at 00:15:24. The config turns export off and samples nothing:
management:
# […]
tracing:
export:
enabled: false
sampling:
probability: 0.0
The test stopped asserting something Boot never promised. The old branch wanted a Tracer (MICROMETER_TRACER, the type name io.micrometer.tracing.Tracer) when traces were on and none when they were off. The new one wants a Tracer either way, then checks the two values:
assertSome(MICROMETER_TRACER);
// M: tracer present (Boot keeps it) but nothing sampled or exported; zero spans is checked at the
// collector in the smoke runs.
assertEquals(traces ? "true" : "false",
context.getEnvironment().getProperty("management.tracing.export.enabled"));
assertEquals(traces ? "1.0" : "0.0",
context.getEnvironment().getProperty("management.tracing.sampling.probability"));
Those assertEquals lines prove only that the config says what I meant: the test reads the same file it checks. Reading the code, they do mean the rewritten test now fails on a config that sets only the old key, because getProperty("management.tracing.export.enabled") comes back null. I haven't run that case.
Whether anything leaves the process is a question for the collector, and here the collector can't credit the fix. Boot 4.1.2 builds an OTLP span exporter only when management.opentelemetry.tracing.export.otlp.endpoint is set (in this app, which has no Docker Compose or Testcontainers connection to the collector), and the metrics-only config never set it, before the fix or after. A smoke run of that commit left the cell's traces.jsonl at 0 bytes, next to 469,581 bytes of metrics. That shows no spans reached the collector. It would have shown the same with the dead key. The traced build in the same pass wrote 65,721,154 bytes of traces, so the collector's trace path worked. What the fix changes in this app is propagation, now off, and sampling, now zero.
What a Tracer that samples nothing costs on its own, I haven't measured. Part 4 measures each mode as a whole, so Boot's metrics-only cells (mo-m) carry this Tracer, sampling nothing and with no span exporter behind it, inside their numbers.
Boot 4.1.2 telemetry defaults worth pinning#
With the metadata open, I wrote down the defaults that matter for the two Boot-starter builds, and what each build sets. Values from the 4.1.2 jars:
| Property | Boot 4.1.2 default | These builds |
|---|---|---|
management.tracing.sampling.probability | 0.1 | 0.0 metrics only, 1.0 traced |
management.otlp.metrics.export.step | 1m | 10s |
management.opentelemetry.tracing.export.max-queue-size | 2048 | 32768 traced |
management.opentelemetry.tracing.export.max-batch-size | 512 | 2048 traced |
management.opentelemetry.tracing.export.schedule-delay | 5s | 1s traced |
management.opentelemetry.tracing.export.timeout | 30s | default |
management.logging.export.otlp.enabled | true | false |
The span batch defaults are the OpenTelemetry SDK's own: BatchSpanProcessorBuilder in opentelemetry-java 1.62.0 sets queue 2048, batch 512, delay 5000 ms and timeout 30 000 ms. The sampler is where Boot departs from the SDK: 10 % of traces, where the SDK's otel.traces.sampler defaults to parentbased_always_on. The traced build raises the queue because the study's design estimated about 6k spans a second at the top load step, which would overflow 2048. And the OTLP log export switch defaults to true; both builds set it to false.
Read the level field#
Boot's release notes call it a rename, and the metadata entry looks like one: a replacement, a version, a deprecation flag. The level is the part that matters. At warning, the old key keeps working while you move. At error, it's a deletion that kept its name.
Filed under: java, spring-boot, opentelemetry, observability, migration
Read next#
- Part 3: the config file the native image never had — this gate test was green while the native build ran on library defaults.
- Part 1: Counts match, dashboards don't — what breaks in Grafana when a service swaps the OTel agent for a starter to go native.
- Part 4: Measuring OpenTelemetry Overhead Without Fooling Yourself — why every mode is a separate build, what each one costs, and which cells had to be thrown out.
- Five Things Spring Boot 4.1 Doesn't Tell You Until It's Too Late — five more Boot 4 migration traps from the memory benchmark, several of them silent.
Sources#
- Spring Boot 4.0 Release Notes — the "renamed" bullet, under Miscellaneous.
- Spring Boot 4.0 Migration Guide — points to
spring-boot-properties-migrator. - Configuration Metadata Format — what deprecation levels
warninganderrormean for binding. - Introducing GraalVM Native Images —
.enabledproperties can't switch beans in a native image. additional-spring-configuration-metadata.json, tracing module, v4.1.2 — the error-level entry and the replacement's description.OnEnabledTracingExportCondition, v4.1.2 — the two keys the export switch reads.OpenTelemetryTracingAutoConfiguration, v4.1.2 —micrometerOtelTracerand its single condition.OtlpTracingConfigurations, v4.1.2 — without a Docker Compose or Testcontainers connection, the OTLP span exporter exists only whenmanagement.opentelemetry.tracing.export.otlp.endpointis set.OpenTelemetryPropagationConfigurations, v4.1.2 — propagators behind the export condition,TextMapPropagator.noop()fallback.NoopTracerAutoConfiguration, v4.1.2 — theTracer.NOOPfallback bean.OnEnabledTracingCondition, v3.5.16 — Boot 3's reader ofmanagement.tracing.enabled.OpenTelemetryTracingAutoConfiguration, v3.5.16 — the same single condition onmicrometerOtelTracerin Boot 3.additional-spring-configuration-metadata.json, actuator-autoconfigure, v3.5.16 — Boot 3's description ofmanagement.tracing.enabled.BatchSpanProcessorBuilder, opentelemetry-java v1.62.0 — the SDK's batch span processor defaults.- OpenTelemetry Java: configure the SDK —
otel.traces.samplerdefaults toparentbased_always_on.