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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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