Skip to content

Commit

Permalink
Write TraceResponse HTTP observability header
Browse files Browse the repository at this point in the history
Prior to this commit, we added support for the "X-Trace-Id" HTTP
response header in spring-projectsgh-40857. This wrote the traceId information for MVC
applications, if the `management.observations.http.server.requests.write-trace-header`
was set.

After receiving feedback from the community, we are revisiting this
feature with the following changes:

* the header is now "traceresponse" and implements the W3C draft
  standard, see https://w3c.github.io/trace-context/#trace-context-http-response-headers-format
* the property is now
  "management.observations.http.server.requests.write-traceresponse"
* both MVC and WebFlux are now supported

Closes spring-projectsgh-44431
  • Loading branch information
bclozel committed Feb 27, 2025
1 parent 96d91aa commit 1446392
Show file tree
Hide file tree
Showing 15 changed files with 430 additions and 114 deletions.
3 changes: 2 additions & 1 deletion .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ public static class ServerRequests {
private String name = "http.server.requests";

/**
* Whether to write the "X-Trace-Id" HTTP response header.
* Whether to write the "traceresponse" HTTP response header.
*/
private boolean writeTraceHeader = false;
private boolean writeTraceresponse = false;

public String getName() {
return this.name;
Expand All @@ -140,12 +140,12 @@ public void setName(String name) {
this.name = name;
}

public boolean isWriteTraceHeader() {
return this.writeTraceHeader;
public boolean isWriteTraceresponse() {
return this.writeTraceresponse;
}

public void setWriteTraceHeader(boolean writeTraceHeader) {
this.writeTraceHeader = writeTraceHeader;
public void setWriteTraceresponse(boolean writeTraceresponse) {
this.writeTraceresponse = writeTraceresponse;
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -20,20 +20,24 @@
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.tracing.Tracer;

import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
import org.springframework.boot.actuate.web.tracing.reactive.TraceResponseObservationWebFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention;
Expand Down Expand Up @@ -61,6 +65,15 @@ public class WebFluxObservationAutoConfiguration {
this.observationProperties = observationProperties;
}

@Bean
@ConditionalOnBooleanProperty("management.observations.http.server.requests.write-traceresponse")
@ConditionalOnBean(Tracer.class)
@ConditionalOnMissingBean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public TraceResponseObservationWebFilter traceResponseObservationWebFilter() {
return new TraceResponseObservationWebFilter();
}

@Bean
@Order(0)
MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
import org.springframework.boot.actuate.web.tracing.servlet.TraceResponseHeaderObservationFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Expand Down Expand Up @@ -54,16 +55,18 @@ static <T extends ServerHttpObservationFilter> FilterRegistrationBean<T> filterR
static class TracingHeaderObservation {

@Bean
@ConditionalOnBooleanProperty("management.observations.http.server.requests.write-trace-header")
@ConditionalOnBooleanProperty("management.observations.http.server.requests.write-traceresponse")
@ConditionalOnBean(Tracer.class)
@ConditionalOnMissingFilterBean({ ServerHttpObservationFilter.class, TraceHeaderObservationFilter.class })
FilterRegistrationBean<TraceHeaderObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
Tracer tracer, ObjectProvider<ServerRequestObservationConvention> customConvention,
@ConditionalOnMissingFilterBean({ ServerHttpObservationFilter.class,
TraceResponseHeaderObservationFilter.class })
FilterRegistrationBean<TraceResponseHeaderObservationFilter> webMvcObservationFilter(
ObservationRegistry registry, 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);
TraceResponseHeaderObservationFilter filter = new TraceResponseHeaderObservationFilter(registry,
convention);
return filterRegistration(filter);
}

Expand All @@ -73,7 +76,8 @@ FilterRegistrationBean<TraceHeaderObservationFilter> webMvcObservationFilter(Obs
static class DefaultObservation {

@Bean
@ConditionalOnMissingFilterBean({ ServerHttpObservationFilter.class, TraceHeaderObservationFilter.class })
@ConditionalOnMissingFilterBean({ ServerHttpObservationFilter.class,
TraceResponseHeaderObservationFilter.class })
FilterRegistrationBean<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
ObjectProvider<ServerRequestObservationConvention> customConvention,
ObservationProperties observationProperties) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -20,13 +20,16 @@
import java.time.temporal.ChronoUnit;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.test.simple.SimpleTracer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
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.web.tracing.reactive.TraceResponseObservationWebFilter;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
Expand Down Expand Up @@ -117,6 +120,25 @@ void shouldBackOffOnCustomServerRequestObservationConvention() {
});
}

@Test
void shouldNotConfigureTraceResponseObservationWebFilterByDefault() {
this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(TraceResponseObservationWebFilter.class));
}

@Test
void shouldNotConfigureTraceResponseObservationWebFilterIfTracerMissing() {
this.contextRunner.withPropertyValues("management.observations.http.server.requests.write-traceresponse=true")
.run((context) -> assertThat(context).doesNotHaveBean(TraceResponseObservationWebFilter.class));
}

@Test
void shouldConfigureTraceResponseObservationWebFilter() {
this.contextRunner.withPropertyValues("management.observations.http.server.requests.write-traceresponse=true")
.withBean(Tracer.class, SimpleTracer::new)
.run((context) -> assertThat(context).hasSingleBean(TraceResponseObservationWebFilter.class));
}

private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) {
return getInitializedMeterRegistry(context, "http.server.requests");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

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;
Expand All @@ -31,6 +30,7 @@
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.actuate.web.tracing.servlet.TraceResponseHeaderObservationFilter;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
Expand Down Expand Up @@ -78,7 +78,7 @@ void definesFilterWhenRegistryIsPresent() {
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
assertThat(context.getBean(FilterRegistrationBean.class).getFilter())
.isInstanceOf(ServerHttpObservationFilter.class)
.isNotInstanceOf(TraceHeaderObservationFilter.class);
.isNotInstanceOf(TraceResponseHeaderObservationFilter.class);
});
}

Expand Down Expand Up @@ -132,18 +132,18 @@ void filterRegistrationDoesNotBackOffWithOtherFilter() {
@Test
void usesTracingFilterWhenTracingIsPresentAndEnabled() {
this.contextRunner.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class))
.withPropertyValues("management.observations.http.server.requests.write-trace-header=true")
.withPropertyValues("management.observations.http.server.requests.write-traceresponse=true")
.run((context) -> {
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
assertThat(context.getBean(FilterRegistrationBean.class).getFilter())
.isInstanceOf(TraceHeaderObservationFilter.class);
.isInstanceOf(TraceResponseHeaderObservationFilter.class);
});
}

@Test
void tracingFilterRegistrationHasExpectedDispatcherTypesAndOrder() {
this.contextRunner.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class))
.withPropertyValues("management.observations.http.server.requests.write-trace-header=true")
.withPropertyValues("management.observations.http.server.requests.write-traceresponse=true")
.run((context) -> {
FilterRegistrationBean<?> registration = context.getBean(FilterRegistrationBean.class);
assertThat(registration).hasFieldOrPropertyWithValue("dispatcherTypes",
Expand All @@ -155,7 +155,7 @@ void tracingFilterRegistrationHasExpectedDispatcherTypesAndOrder() {
@Test
void filterRegistrationBacksOffWithAnotherTraceHeaderObservationFilterRegistration() {
this.contextRunner.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class))
.withPropertyValues("management.observations.http.server.requests.write-trace-header=true")
.withPropertyValues("management.observations.http.server.requests.write-traceresponse=true")
.withUserConfiguration(TestTraceHeaderObservationFilterRegistrationConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
Expand All @@ -167,10 +167,10 @@ void filterRegistrationBacksOffWithAnotherTraceHeaderObservationFilterRegistrati
@Test
void filterRegistrationBacksOffWithAnotherTraceHeaderObservationFilter() {
this.contextRunner.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class))
.withPropertyValues("management.observations.http.server.requests.write-trace-header=true")
.withPropertyValues("management.observations.http.server.requests.write-traceresponse=true")
.withUserConfiguration(TestTraceHeaderObservationFilterConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class)
.hasSingleBean(TraceHeaderObservationFilter.class));
.hasSingleBean(TraceResponseHeaderObservationFilter.class));
}

@Test
Expand Down Expand Up @@ -254,7 +254,7 @@ static class TestTraceHeaderObservationFilterRegistrationConfiguration {

@Bean
@SuppressWarnings("unchecked")
FilterRegistrationBean<TraceHeaderObservationFilter> testTraceHeaderObservationFilter() {
FilterRegistrationBean<TraceResponseHeaderObservationFilter> testTraceHeaderObservationFilter() {
return mock(FilterRegistrationBean.class);
}

Expand All @@ -264,8 +264,8 @@ FilterRegistrationBean<TraceHeaderObservationFilter> testTraceHeaderObservationF
static class TestTraceHeaderObservationFilterConfiguration {

@Bean
TraceHeaderObservationFilter testTraceHeaderObservationFilter() {
return new TraceHeaderObservationFilter(Tracer.NOOP, TestObservationRegistry.create());
TraceResponseHeaderObservationFilter testTraceHeaderObservationFilter() {
return new TraceResponseHeaderObservationFilter(TestObservationRegistry.create());
}

}
Expand Down
1 change: 1 addition & 0 deletions spring-boot-project/spring-boot-actuator/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ dependencies {
testImplementation("org.assertj:assertj-core")
testImplementation("com.jayway.jsonpath:json-path")
testImplementation("io.micrometer:micrometer-observation-test")
testImplementation("io.micrometer:micrometer-tracing-test")
testImplementation("io.projectreactor:reactor-test")
testImplementation("io.r2dbc:r2dbc-h2")
testImplementation("net.minidev:json-smart")
Expand Down
Loading

0 comments on commit 1446392

Please sign in to comment.