Skip to content

Commit afcc780

Browse files
committed
Write TraceId in HTTP Response headers
Prior to this commit, the Micrometer instrumentation support would auto-configure a `ServerHttpObservationFilter` for creating observations in Spring MVC applications. As of Spring Framework 6.2, applications can extend this filter class to get notified of the observation scope being opened. This commit contributes a new `TraceHeaderObservationFilter` implementation that writes the current Trace Id (if present) to the `X-Trace-Id` HTTP response header. This feature is disabled by default, applications will need to enable `management.observations.http.server.requests.write-trace-header`. ` Closes gh-40857
1 parent f5f888d commit afcc780

File tree

8 files changed

+323
-27
lines changed

8 files changed

+323
-27
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ dependencies {
136136
testImplementation(project(":spring-boot-project:spring-boot-test"))
137137
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
138138
testImplementation("io.micrometer:micrometer-observation-test")
139+
testImplementation("io.micrometer:micrometer-tracing-test")
139140
testImplementation("io.projectreactor:reactor-test")
140141
testImplementation("io.prometheus:prometheus-metrics-exposition-formats")
141142
testImplementation("io.r2dbc:r2dbc-h2")

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -127,6 +127,11 @@ public static class ServerRequests {
127127
*/
128128
private String name = "http.server.requests";
129129

130+
/**
131+
* Whether to write the "X-Trace-Id" HTTP response header.
132+
*/
133+
private boolean writeTraceHeader = false;
134+
130135
public String getName() {
131136
return this.name;
132137
}
@@ -135,6 +140,14 @@ public void setName(String name) {
135140
this.name = name;
136141
}
137142

143+
public boolean isWriteTraceHeader() {
144+
return this.writeTraceHeader;
145+
}
146+
147+
public void setWriteTraceHeader(boolean writeTraceHeader) {
148+
this.writeTraceHeader = writeTraceHeader;
149+
}
150+
138151
}
139152

140153
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.observation.web.servlet;
18+
19+
import io.micrometer.observation.ObservationRegistry;
20+
import io.micrometer.tracing.Tracer;
21+
import jakarta.servlet.DispatcherType;
22+
23+
import org.springframework.beans.factory.ObjectProvider;
24+
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
25+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
26+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
27+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
28+
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
29+
import org.springframework.boot.web.servlet.FilterRegistrationBean;
30+
import org.springframework.context.annotation.Bean;
31+
import org.springframework.context.annotation.Configuration;
32+
import org.springframework.core.Ordered;
33+
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
34+
import org.springframework.http.server.observation.ServerRequestObservationConvention;
35+
import org.springframework.web.filter.ServerHttpObservationFilter;
36+
37+
/**
38+
* Observation filter configurations imported by
39+
* {@link WebMvcObservationAutoConfiguration}.
40+
*
41+
* @author Brian Clozel
42+
*/
43+
abstract class ObservationFilterConfigurations {
44+
45+
static <T extends ServerHttpObservationFilter> FilterRegistrationBean<T> filterRegistration(T filter) {
46+
FilterRegistrationBean<T> registration = new FilterRegistrationBean<>(filter);
47+
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
48+
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
49+
return registration;
50+
}
51+
52+
@Configuration(proxyBeanMethods = false)
53+
@ConditionalOnClass(Tracer.class)
54+
static class TracingHeaderObservation {
55+
56+
@Bean
57+
@ConditionalOnProperty(prefix = "management.observations.http.server.requests", name = "write-trace-header")
58+
@ConditionalOnBean(Tracer.class)
59+
@ConditionalOnMissingFilterBean({ ServerHttpObservationFilter.class, TraceHeaderObservationFilter.class })
60+
FilterRegistrationBean<TraceHeaderObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
61+
Tracer tracer, ObjectProvider<ServerRequestObservationConvention> customConvention,
62+
ObservationProperties observationProperties) {
63+
String name = observationProperties.getHttp().getServer().getRequests().getName();
64+
ServerRequestObservationConvention convention = customConvention
65+
.getIfAvailable(() -> new DefaultServerRequestObservationConvention(name));
66+
TraceHeaderObservationFilter filter = new TraceHeaderObservationFilter(tracer, registry, convention);
67+
return filterRegistration(filter);
68+
}
69+
70+
}
71+
72+
@Configuration(proxyBeanMethods = false)
73+
static class DefaultObservation {
74+
75+
@Bean
76+
@ConditionalOnMissingFilterBean({ ServerHttpObservationFilter.class, TraceHeaderObservationFilter.class })
77+
FilterRegistrationBean<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
78+
ObjectProvider<ServerRequestObservationConvention> customConvention,
79+
ObservationProperties observationProperties) {
80+
String name = observationProperties.getHttp().getServer().getRequests().getName();
81+
ServerRequestObservationConvention convention = customConvention
82+
.getIfAvailable(() -> new DefaultServerRequestObservationConvention(name));
83+
ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention);
84+
return filterRegistration(filter);
85+
}
86+
87+
}
88+
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.observation.web.servlet;
18+
19+
import io.micrometer.observation.Observation.Scope;
20+
import io.micrometer.observation.ObservationRegistry;
21+
import io.micrometer.tracing.Span;
22+
import io.micrometer.tracing.Tracer;
23+
import jakarta.servlet.http.HttpServletRequest;
24+
import jakarta.servlet.http.HttpServletResponse;
25+
26+
import org.springframework.http.server.observation.ServerRequestObservationConvention;
27+
import org.springframework.util.Assert;
28+
import org.springframework.web.filter.ServerHttpObservationFilter;
29+
30+
/**
31+
* {@link ServerHttpObservationFilter} that writes the current {@link Span} in an HTTP
32+
* response header. By default, the {@code "X-Trace-Id"} header is used.
33+
*
34+
* @author Brian Clozel
35+
* @since 3.5.0
36+
*/
37+
public class TraceHeaderObservationFilter extends ServerHttpObservationFilter {
38+
39+
private static final String TRACE_ID_HEADER_NAME = "X-Trace-Id";
40+
41+
private final Tracer tracer;
42+
43+
/**
44+
* Create a {@link TraceHeaderObservationFilter} that will write the
45+
* {@code "X-Trace-Id"} HTTP response header.
46+
* @param tracer the current tracer
47+
* @param observationRegistry the current observation registry
48+
*/
49+
public TraceHeaderObservationFilter(Tracer tracer, ObservationRegistry observationRegistry) {
50+
super(observationRegistry);
51+
Assert.notNull(tracer, "Tracer must not be null");
52+
this.tracer = tracer;
53+
}
54+
55+
/**
56+
* Create a {@link TraceHeaderObservationFilter} that will write the
57+
* {@code "X-Trace-Id"} HTTP response header.
58+
* @param tracer the current tracer
59+
* @param observationRegistry the current observation registry
60+
* @param observationConvention the custom observation convention to use.
61+
*/
62+
public TraceHeaderObservationFilter(Tracer tracer, ObservationRegistry observationRegistry,
63+
ServerRequestObservationConvention observationConvention) {
64+
super(observationRegistry, observationConvention);
65+
Assert.notNull(tracer, "Tracer must not be null");
66+
this.tracer = tracer;
67+
}
68+
69+
@Override
70+
protected void onScopeOpened(Scope scope, HttpServletRequest request, HttpServletResponse response) {
71+
Span currentSpan = this.tracer.currentSpan();
72+
if (currentSpan != null && !currentSpan.isNoop()) {
73+
response.setHeader(TRACE_ID_HEADER_NAME, currentSpan.context().traceId());
74+
}
75+
}
76+
77+
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java

+4-24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,9 +20,7 @@
2020
import io.micrometer.core.instrument.config.MeterFilter;
2121
import io.micrometer.observation.Observation;
2222
import io.micrometer.observation.ObservationRegistry;
23-
import jakarta.servlet.DispatcherType;
2423

25-
import org.springframework.beans.factory.ObjectProvider;
2624
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
2725
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
2826
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
@@ -35,16 +33,11 @@
3533
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3634
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3735
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
38-
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
3936
import org.springframework.boot.context.properties.EnableConfigurationProperties;
40-
import org.springframework.boot.web.servlet.FilterRegistrationBean;
4137
import org.springframework.context.annotation.Bean;
4238
import org.springframework.context.annotation.Configuration;
43-
import org.springframework.core.Ordered;
39+
import org.springframework.context.annotation.Import;
4440
import org.springframework.core.annotation.Order;
45-
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
46-
import org.springframework.http.server.observation.ServerRequestObservationConvention;
47-
import org.springframework.web.filter.ServerHttpObservationFilter;
4841
import org.springframework.web.servlet.DispatcherServlet;
4942

5043
/**
@@ -62,23 +55,10 @@
6255
@ConditionalOnClass({ DispatcherServlet.class, Observation.class })
6356
@ConditionalOnBean(ObservationRegistry.class)
6457
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
58+
@Import({ ObservationFilterConfigurations.TracingHeaderObservation.class,
59+
ObservationFilterConfigurations.DefaultObservation.class })
6560
public class WebMvcObservationAutoConfiguration {
6661

67-
@Bean
68-
@ConditionalOnMissingFilterBean
69-
public FilterRegistrationBean<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
70-
ObjectProvider<ServerRequestObservationConvention> customConvention,
71-
ObservationProperties observationProperties) {
72-
String name = observationProperties.getHttp().getServer().getRequests().getName();
73-
ServerRequestObservationConvention convention = customConvention
74-
.getIfAvailable(() -> new DefaultServerRequestObservationConvention(name));
75-
ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention);
76-
FilterRegistrationBean<ServerHttpObservationFilter> registration = new FilterRegistrationBean<>(filter);
77-
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
78-
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
79-
return registration;
80-
}
81-
8262
@Configuration(proxyBeanMethods = false)
8363
@ConditionalOnClass(MeterRegistry.class)
8464
@ConditionalOnBean(MeterRegistry.class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.observation.web.servlet;
18+
19+
import io.micrometer.observation.tck.TestObservationRegistry;
20+
import io.micrometer.tracing.Tracer;
21+
import io.micrometer.tracing.handler.DefaultTracingObservationHandler;
22+
import io.micrometer.tracing.test.simple.SimpleTracer;
23+
import jakarta.servlet.FilterChain;
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.mock.web.MockHttpServletRequest;
27+
import org.springframework.mock.web.MockHttpServletResponse;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
/**
32+
* Tests for {@link TraceHeaderObservationFilter}.
33+
*/
34+
class TraceHeaderObservationFilterTests {
35+
36+
TestObservationRegistry observationRegistry = TestObservationRegistry.create();
37+
38+
@Test
39+
void shouldWriteTraceHeaderWhenCurrentTrace() throws Exception {
40+
TraceHeaderObservationFilter filter = createFilter(new SimpleTracer());
41+
42+
MockHttpServletResponse response = new MockHttpServletResponse();
43+
filter.doFilter(new MockHttpServletRequest(), response, getFilterChain());
44+
45+
assertThat(response.getHeader("X-Trace-Id")).isNotEmpty();
46+
}
47+
48+
@Test
49+
void shouldNotWriteTraceHeaderWhenNoCurrentTrace() throws Exception {
50+
TraceHeaderObservationFilter filter = createFilter(Tracer.NOOP);
51+
52+
MockHttpServletResponse response = new MockHttpServletResponse();
53+
filter.doFilter(new MockHttpServletRequest(), response, getFilterChain());
54+
assertThat(response.getHeaderNames()).doesNotContain("X-Trace-Id");
55+
}
56+
57+
private TraceHeaderObservationFilter createFilter(Tracer tracer) {
58+
this.observationRegistry.observationConfig().observationHandler(new DefaultTracingObservationHandler(tracer));
59+
return new TraceHeaderObservationFilter(tracer, this.observationRegistry);
60+
}
61+
62+
private static FilterChain getFilterChain() {
63+
return (servletRequest, servletResponse) -> servletResponse.getWriter().print("Hello");
64+
}
65+
66+
}

0 commit comments

Comments
 (0)