diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index a07c2d7d41c..da516dcb0af 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -136,6 +136,7 @@ dependencies { testImplementation(project(":spring-boot-project:spring-boot-test")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("io.micrometer:micrometer-observation-test") + testImplementation("io.micrometer:micrometer-tracing-test") testImplementation("io.projectreactor:reactor-test") testImplementation("io.prometheus:prometheus-metrics-exposition-formats") testImplementation("io.r2dbc:r2dbc-h2") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java index 93d0e715246..e004eb04ccd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,6 +127,11 @@ public class ObservationProperties { */ private String name = "http.server.requests"; + /** + * Whether to write the "X-Trace-Id" HTTP response header. + */ + private boolean writeTraceHeader = false; + public String getName() { return this.name; } @@ -135,6 +140,14 @@ public class ObservationProperties { this.name = name; } + public boolean isWriteTraceHeader() { + return this.writeTraceHeader; + } + + public void setWriteTraceHeader(boolean writeTraceHeader) { + this.writeTraceHeader = writeTraceHeader; + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ObservationFilterConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ObservationFilterConfigurations.java new file mode 100644 index 00000000000..2bff98ee499 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ObservationFilterConfigurations.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.observation.web.servlet; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; +import jakarta.servlet.DispatcherType; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.observation.ServerRequestObservationConvention; +import org.springframework.web.filter.ServerHttpObservationFilter; + +/** + * Observation filter configurations imported by + * {@link WebMvcObservationAutoConfiguration}. + * + * @author Brian Clozel + */ +abstract class ObservationFilterConfigurations { + + static FilterRegistrationBean filterRegistration(T filter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); + registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC); + return registration; + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Tracer.class) + static class TracingHeaderObservation { + + @Bean + @ConditionalOnProperty(prefix = "management.observations.http.server.requests", name = "write-trace-header") + @ConditionalOnBean(Tracer.class) + @ConditionalOnMissingFilterBean({ ServerHttpObservationFilter.class, TraceHeaderObservationFilter.class }) + FilterRegistrationBean webMvcObservationFilter(ObservationRegistry registry, + Tracer tracer, ObjectProvider customConvention, + ObservationProperties observationProperties) { + String name = observationProperties.getHttp().getServer().getRequests().getName(); + ServerRequestObservationConvention convention = customConvention + .getIfAvailable(() -> new DefaultServerRequestObservationConvention(name)); + TraceHeaderObservationFilter filter = new TraceHeaderObservationFilter(tracer, registry, convention); + return filterRegistration(filter); + } + + } + + @Configuration(proxyBeanMethods = false) + static class DefaultObservation { + + @Bean + @ConditionalOnMissingFilterBean({ ServerHttpObservationFilter.class, TraceHeaderObservationFilter.class }) + FilterRegistrationBean webMvcObservationFilter(ObservationRegistry registry, + ObjectProvider customConvention, + ObservationProperties observationProperties) { + String name = observationProperties.getHttp().getServer().getRequests().getName(); + ServerRequestObservationConvention convention = customConvention + .getIfAvailable(() -> new DefaultServerRequestObservationConvention(name)); + ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention); + return filterRegistration(filter); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/TraceHeaderObservationFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/TraceHeaderObservationFilter.java new file mode 100644 index 00000000000..98f0c3b7dc2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/TraceHeaderObservationFilter.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.observation.web.servlet; + +import io.micrometer.observation.Observation.Scope; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.http.server.observation.ServerRequestObservationConvention; +import org.springframework.util.Assert; +import org.springframework.web.filter.ServerHttpObservationFilter; + +/** + * {@link ServerHttpObservationFilter} that writes the current {@link Span} in an HTTP + * response header. By default, the {@code "X-Trace-Id"} header is used. + * + * @author Brian Clozel + * @since 3.5.0 + */ +public class TraceHeaderObservationFilter extends ServerHttpObservationFilter { + + private static final String TRACE_ID_HEADER_NAME = "X-Trace-Id"; + + private final Tracer tracer; + + /** + * Create a {@link TraceHeaderObservationFilter} that will write the + * {@code "X-Trace-Id"} HTTP response header. + * @param tracer the current tracer + * @param observationRegistry the current observation registry + */ + public TraceHeaderObservationFilter(Tracer tracer, ObservationRegistry observationRegistry) { + super(observationRegistry); + Assert.notNull(tracer, "Tracer must not be null"); + this.tracer = tracer; + } + + /** + * Create a {@link TraceHeaderObservationFilter} that will write the + * {@code "X-Trace-Id"} HTTP response header. + * @param tracer the current tracer + * @param observationRegistry the current observation registry + * @param observationConvention the custom observation convention to use. + */ + public TraceHeaderObservationFilter(Tracer tracer, ObservationRegistry observationRegistry, + ServerRequestObservationConvention observationConvention) { + super(observationRegistry, observationConvention); + Assert.notNull(tracer, "Tracer must not be null"); + this.tracer = tracer; + } + + @Override + protected void onScopeOpened(Scope scope, HttpServletRequest request, HttpServletResponse response) { + Span currentSpan = this.tracer.currentSpan(); + if (currentSpan != null && !currentSpan.isNoop()) { + response.setHeader(TRACE_ID_HEADER_NAME, currentSpan.context().traceId()); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java index 2b4aa96c393..844dcb6ac32 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; -import jakarta.servlet.DispatcherType; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; @@ -35,16 +33,11 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.Order; -import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; -import org.springframework.http.server.observation.ServerRequestObservationConvention; -import org.springframework.web.filter.ServerHttpObservationFilter; import org.springframework.web.servlet.DispatcherServlet; /** @@ -62,23 +55,10 @@ import org.springframework.web.servlet.DispatcherServlet; @ConditionalOnClass({ DispatcherServlet.class, Observation.class }) @ConditionalOnBean(ObservationRegistry.class) @EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) +@Import({ ObservationFilterConfigurations.TracingHeaderObservation.class, + ObservationFilterConfigurations.DefaultObservation.class }) public class WebMvcObservationAutoConfiguration { - @Bean - @ConditionalOnMissingFilterBean - public FilterRegistrationBean webMvcObservationFilter(ObservationRegistry registry, - ObjectProvider customConvention, - ObservationProperties observationProperties) { - String name = observationProperties.getHttp().getServer().getRequests().getName(); - ServerRequestObservationConvention convention = customConvention - .getIfAvailable(() -> new DefaultServerRequestObservationConvention(name)); - ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention); - FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); - registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); - registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC); - return registration; - } - @Configuration(proxyBeanMethods = false) @ConditionalOnClass(MeterRegistry.class) @ConditionalOnBean(MeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/TraceHeaderObservationFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/TraceHeaderObservationFilterTests.java new file mode 100644 index 00000000000..7dffec7efd8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/TraceHeaderObservationFilterTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.observation.web.servlet; + +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.DefaultTracingObservationHandler; +import io.micrometer.tracing.test.simple.SimpleTracer; +import jakarta.servlet.FilterChain; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TraceHeaderObservationFilter}. + */ +class TraceHeaderObservationFilterTests { + + TestObservationRegistry observationRegistry = TestObservationRegistry.create(); + + @Test + void shouldWriteTraceHeaderWhenCurrentTrace() throws Exception { + TraceHeaderObservationFilter filter = createFilter(new SimpleTracer()); + + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(new MockHttpServletRequest(), response, getFilterChain()); + + assertThat(response.getHeader("X-Trace-Id")).isNotEmpty(); + } + + @Test + void shouldNotWriteTraceHeaderWhenNoCurrentTrace() throws Exception { + TraceHeaderObservationFilter filter = createFilter(Tracer.NOOP); + + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(new MockHttpServletRequest(), response, getFilterChain()); + assertThat(response.getHeaderNames()).doesNotContain("X-Trace-Id"); + } + + private TraceHeaderObservationFilter createFilter(Tracer tracer) { + this.observationRegistry.observationConfig().observationHandler(new DefaultTracingObservationHandler(tracer)); + return new TraceHeaderObservationFilter(tracer, this.observationRegistry); + } + + private static FilterChain getFilterChain() { + return (servletRequest, servletResponse) -> servletResponse.getWriter().print("Hello"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java index f9ba22dedae..2506ee28916 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.EnumSet; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.tracing.Tracer; import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import org.junit.jupiter.api.Test; @@ -29,6 +30,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfigu import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.NoopTracerAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; @@ -75,7 +77,8 @@ class WebMvcObservationAutoConfigurationTests { this.contextRunner.run((context) -> { assertThat(context).hasSingleBean(FilterRegistrationBean.class); assertThat(context.getBean(FilterRegistrationBean.class).getFilter()) - .isInstanceOf(ServerHttpObservationFilter.class); + .isInstanceOf(ServerHttpObservationFilter.class) + .isNotInstanceOf(TraceHeaderObservationFilter.class); }); } @@ -126,6 +129,50 @@ class WebMvcObservationAutoConfigurationTests { .run((context) -> assertThat(context).hasBean("testFilter").hasBean("webMvcObservationFilter")); } + @Test + void usesTracingFilterWhenTracingIsPresentAndEnabled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class)) + .withPropertyValues("management.observations.http.server.requests.write-trace-header=true") + .run((context) -> { + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + assertThat(context.getBean(FilterRegistrationBean.class).getFilter()) + .isInstanceOf(TraceHeaderObservationFilter.class); + }); + } + + @Test + void tracingFilterRegistrationHasExpectedDispatcherTypesAndOrder() { + this.contextRunner.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class)) + .withPropertyValues("management.observations.http.server.requests.write-trace-header=true") + .run((context) -> { + FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); + assertThat(registration).hasFieldOrPropertyWithValue("dispatcherTypes", + EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC)); + assertThat(registration.getOrder()).isEqualTo(Ordered.HIGHEST_PRECEDENCE + 1); + }); + } + + @Test + void filterRegistrationBacksOffWithAnotherTraceHeaderObservationFilterRegistration() { + this.contextRunner.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class)) + .withPropertyValues("management.observations.http.server.requests.write-trace-header=true") + .withUserConfiguration(TestTraceHeaderObservationFilterRegistrationConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + assertThat(context.getBean(FilterRegistrationBean.class)) + .isSameAs(context.getBean("testTraceHeaderObservationFilter")); + }); + } + + @Test + void filterRegistrationBacksOffWithAnothertestTraceHeaderObservationFilter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class)) + .withPropertyValues("management.observations.http.server.requests.write-trace-header=true") + .withUserConfiguration(TestTraceHeaderObservationFilterConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class) + .hasSingleBean(TraceHeaderObservationFilter.class)); + } + @Test void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) { this.contextRunner.withUserConfiguration(TestController.class) @@ -202,6 +249,27 @@ class WebMvcObservationAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class TestTraceHeaderObservationFilterRegistrationConfiguration { + + @Bean + @SuppressWarnings("unchecked") + FilterRegistrationBean testTraceHeaderObservationFilter() { + return mock(FilterRegistrationBean.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TestTraceHeaderObservationFilterConfiguration { + + @Bean + TraceHeaderObservationFilter testTraceHeaderObservationFilter() { + return new TraceHeaderObservationFilter(Tracer.NOOP, TestObservationRegistry.create()); + } + + } + @Configuration(proxyBeanMethods = false) static class TestFilterRegistrationConfiguration { diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc index f644b48d491..c1b6c412f8b 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/metrics.adoc @@ -775,6 +775,8 @@ See the {url-spring-framework-docs}/integration/observability.html#observability To add to the default tags, provide a javadoc:org.springframework.context.annotation.Bean[format=annotation] that extends javadoc:org.springframework.http.server.observation.DefaultServerRequestObservationConvention[] from the `org.springframework.http.server.observation` package. To replace the default tags, provide a javadoc:org.springframework.context.annotation.Bean[format=annotation] that implements javadoc:org.springframework.http.server.observation.ServerRequestObservationConvention[]. +If the application is using xref:actuator/tracing.adoc#actuator.micrometer-tracing[Tracing], you can configure the HTTP server observations to print an `X-Trace-Id` +HTTP response header containing the current trace Id. For that, you will need to enable the following configuration property: configprop:management.observations.http.server.requests.write-trace-header[]. TIP: In some cases, exceptions handled in web controllers are not recorded as request metrics tags. Applications can opt in and record exceptions by xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling[setting handled exceptions as request attributes].