Browse Source
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-40857pull/44202/head
8 changed files with 323 additions and 27 deletions
@ -0,0 +1,89 @@
@@ -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 <T extends ServerHttpObservationFilter> FilterRegistrationBean<T> filterRegistration(T filter) { |
||||
FilterRegistrationBean<T> 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<TraceHeaderObservationFilter> webMvcObservationFilter(ObservationRegistry registry, |
||||
Tracer tracer, ObjectProvider<ServerRequestObservationConvention> 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<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry, |
||||
ObjectProvider<ServerRequestObservationConvention> 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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,77 @@
@@ -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()); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,66 @@
@@ -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"); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue