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
This commit is contained in:
parent
f5f888dbdd
commit
afcc780e61
@ -136,6 +136,7 @@ dependencies {
|
|||||||
testImplementation(project(":spring-boot-project:spring-boot-test"))
|
testImplementation(project(":spring-boot-project:spring-boot-test"))
|
||||||
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
|
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
|
||||||
testImplementation("io.micrometer:micrometer-observation-test")
|
testImplementation("io.micrometer:micrometer-observation-test")
|
||||||
|
testImplementation("io.micrometer:micrometer-tracing-test")
|
||||||
testImplementation("io.projectreactor:reactor-test")
|
testImplementation("io.projectreactor:reactor-test")
|
||||||
testImplementation("io.prometheus:prometheus-metrics-exposition-formats")
|
testImplementation("io.prometheus:prometheus-metrics-exposition-formats")
|
||||||
testImplementation("io.r2dbc:r2dbc-h2")
|
testImplementation("io.r2dbc:r2dbc-h2")
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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";
|
private String name = "http.server.requests";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to write the "X-Trace-Id" HTTP response header.
|
||||||
|
*/
|
||||||
|
private boolean writeTraceHeader = false;
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
@ -135,6 +140,14 @@ public class ObservationProperties {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isWriteTraceHeader() {
|
||||||
|
return this.writeTraceHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWriteTraceHeader(boolean writeTraceHeader) {
|
||||||
|
this.writeTraceHeader = writeTraceHeader;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.core.instrument.config.MeterFilter;
|
||||||
import io.micrometer.observation.Observation;
|
import io.micrometer.observation.Observation;
|
||||||
import io.micrometer.observation.ObservationRegistry;
|
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.CompositeMeterRegistryAutoConfiguration;
|
||||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
|
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
|
||||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
|
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.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
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.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.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;
|
import org.springframework.web.servlet.DispatcherServlet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,23 +55,10 @@ import org.springframework.web.servlet.DispatcherServlet;
|
|||||||
@ConditionalOnClass({ DispatcherServlet.class, Observation.class })
|
@ConditionalOnClass({ DispatcherServlet.class, Observation.class })
|
||||||
@ConditionalOnBean(ObservationRegistry.class)
|
@ConditionalOnBean(ObservationRegistry.class)
|
||||||
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
|
@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
|
||||||
|
@Import({ ObservationFilterConfigurations.TracingHeaderObservation.class,
|
||||||
|
ObservationFilterConfigurations.DefaultObservation.class })
|
||||||
public class WebMvcObservationAutoConfiguration {
|
public class WebMvcObservationAutoConfiguration {
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingFilterBean
|
|
||||||
public 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);
|
|
||||||
FilterRegistrationBean<ServerHttpObservationFilter> registration = new FilterRegistrationBean<>(filter);
|
|
||||||
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
|
|
||||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
|
||||||
return registration;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(MeterRegistry.class)
|
@ConditionalOnClass(MeterRegistry.class)
|
||||||
@ConditionalOnBean(MeterRegistry.class)
|
@ConditionalOnBean(MeterRegistry.class)
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.core.instrument.MeterRegistry;
|
||||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||||
|
import io.micrometer.tracing.Tracer;
|
||||||
import jakarta.servlet.DispatcherType;
|
import jakarta.servlet.DispatcherType;
|
||||||
import jakarta.servlet.Filter;
|
import jakarta.servlet.Filter;
|
||||||
import org.junit.jupiter.api.Test;
|
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.test.MetricsRun;
|
||||||
import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController;
|
import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController;
|
||||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
|
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.AutoConfigurations;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||||
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
|
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
|
||||||
@ -75,7 +77,8 @@ class WebMvcObservationAutoConfigurationTests {
|
|||||||
this.contextRunner.run((context) -> {
|
this.contextRunner.run((context) -> {
|
||||||
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
|
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
|
||||||
assertThat(context.getBean(FilterRegistrationBean.class).getFilter())
|
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"));
|
.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
|
@Test
|
||||||
void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
|
void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
|
||||||
this.contextRunner.withUserConfiguration(TestController.class)
|
this.contextRunner.withUserConfiguration(TestController.class)
|
||||||
@ -202,6 +249,27 @@ class WebMvcObservationAutoConfigurationTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class TestTraceHeaderObservationFilterRegistrationConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
FilterRegistrationBean<TraceHeaderObservationFilter> testTraceHeaderObservationFilter() {
|
||||||
|
return mock(FilterRegistrationBean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class TestTraceHeaderObservationFilterConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
TraceHeaderObservationFilter testTraceHeaderObservationFilter() {
|
||||||
|
return new TraceHeaderObservationFilter(Tracer.NOOP, TestObservationRegistry.create());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class TestFilterRegistrationConfiguration {
|
static class TestFilterRegistrationConfiguration {
|
||||||
|
|
||||||
|
@ -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 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[].
|
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.
|
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].
|
Applications can opt in and record exceptions by xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling[setting handled exceptions as request attributes].
|
||||||
|
Loading…
x
Reference in New Issue
Block a user