Handling Custom Header Traces in Spring Boot 3.4
Imagine you have a Spring Boot 3.4 web service seamlessly working with two clients. The first client uses Spring Boot 3+, making trace propagation a breeze. Without any extra effort, you get beautiful end-to-end trace continuity đȘ. Logs appear clean and connected, as if by magic.
However, things take a turn when client two comes into play. Instead of standard tracing headers, they send custom headers like `ot-custom-traceid` and `ot-custom-spanid`. While these custom headers contain valid trace information, Spring Boot fails to propagate these traces. The result? You lose the ability to connect client traces with server-side logs.
This creates an observability gap. For client one, you see the full path of a request across services. For client two, you only see server-side logs, missing the critical client trace. It's like seeing half a puzzleâyou know somethingâs missing but canât put the pieces together. đ
In this article, weâll explore how to solve this problem without relying on Spring Cloud Sleuth, staying true to the Spring Boot 3.4 ecosystem. By the end, youâll know how to propagate and continue traces from custom headers, ensuring seamless observability across your system.
Command | Example of use |
---|---|
MDC.put | This command adds key-value pairs to the Mapped Diagnostic Context (MDC), allowing custom trace IDs to be included in logs. For example, MDC.put("traceId", "12345"). |
MDC.clear | Clears all entries from the MDC after a request is processed to avoid trace contamination between requests. For example, MDC.clear(). |
OncePerRequestFilter | A Spring Boot filter that ensures the filter logic is executed only once per HTTP request, ideal for tracing headers. Example: public class CustomTraceFilter extends OncePerRequestFilter. |
filterChain.doFilter | Proceeds to the next filter in the chain, ensuring the request continues through other filters. For example, filterChain.doFilter(request, response). |
RestTemplate.getInterceptors() | Retrieves the list of interceptors for a RestTemplate instance, allowing custom interceptors to be added. Example: restTemplate.getInterceptors().add(new CustomInterceptor()). |
ClientHttpRequestInterceptor | An interface for intercepting outgoing HTTP requests and adding custom headers. For example, implementing ClientHttpRequestInterceptor to insert trace IDs. |
HttpServletRequest.getHeader | Extracts the value of a specific HTTP header from the incoming request. Example: request.getHeader("ot-custom-traceid"). |
FilterRegistrationBean | Registers custom filters in the Spring Boot application. For example: registrationBean.setFilter(new CustomTraceFilter()). |
MockMvc.perform | Simulates HTTP requests in unit tests for Spring Boot applications. Example: mockMvc.perform(get("/test-endpoint").header("ot-custom-traceid", "12345")). |
ClientHttpRequestExecution.execute | Executes the intercepted HTTP request with the provided request body and headers. Example: execution.execute(request, body). |
Custom Header Trace Propagation in Spring Boot
One of the key components in solving this issue is the CustomTraceFilter. This filter extends the OncePerRequestFilter class, ensuring the trace header logic runs only once for each HTTP request. Filters in Spring Boot are incredibly useful when modifying requests or responses globally. For example, if the client sends tracing information like ot-custom-traceid or ot-custom-spanid in custom headers, this filter intercepts the request, extracts these headers, and propagates them into the Mapped Diagnostic Context (MDC). By adding the trace IDs to the MDC, we ensure these identifiers are visible in the logs generated during request processing.
The MDC is a critical part of logging frameworks like SLF4J and Logback. It allows us to store contextual information for the current thread, such as custom trace IDs. Using commands like MDC.put and MDC.clear, we ensure that the logging system includes the trace details and avoids contamination between concurrent requests. For example, if Client Two sends `ot-custom-traceid` as `8f7ebd8a73f9a8f50e6a00a87a20952a`, this ID is stored in MDC and included in all downstream logs, creating a consistent trace path.
On the other hand, for outgoing HTTP requests, the RestTemplate interceptor plays an essential role. By implementing ClientHttpRequestInterceptor, we can attach the same trace headers (`ot-custom-traceid` and `ot-custom-spanid`) to outgoing requests. This ensures that the trace continuity is maintained when the application calls other microservices. For instance, when the server processes a request with trace ID `8f7ebd8a73f9a8f50e6a00a87a20952a`, it attaches this ID to the outgoing headers, so downstream services can recognize and propagate the trace seamlessly.
Finally, the unit tests written with MockMvc validate the entire setup by simulating HTTP requests and verifying header propagation. In real-world applications, testing is crucial to ensure the trace headers are correctly handled. For example, by sending a GET request with custom headers and inspecting the response or logs, we can confirm that the filter and interceptor work as expected. This comprehensive approach solves the challenge without relying on legacy dependencies like Spring Cloud Sleuth. Ultimately, the combination of filters, interceptors, and MDC ensures trace continuity even when clients use custom headers, making the system robust and fully observable. đ
Propagating Custom Tracing Headers in Spring Boot 3.4
Using Java with Spring Boot 3.4 and Micrometer for Backend Processing
// Solution 1: Extract and Propagate Custom Trace Headers Manually
// Import necessary Spring Boot and Micrometer libraries
import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomTraceFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException {
String traceId = request.getHeader("ot-custom-traceid");
String spanId = request.getHeader("ot-custom-spanid");
try {
if (traceId != null) {
MDC.put("traceId", traceId); // Add traceId to Mapped Diagnostic Context
}
if (spanId != null) {
MDC.put("spanId", spanId);
}
filterChain.doFilter(request, response); // Continue request processing
} finally {
MDC.clear(); // Ensure MDC is cleared after processing
}
}
}
// Register the filter in your configuration class
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<CustomTraceFilter> traceFilter() {
FilterRegistrationBean<CustomTraceFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CustomTraceFilter());
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
Unit Test for Custom Trace Header Propagation
Testing with JUnit and MockMvc to Validate Trace Header Propagation
// Import necessary libraries
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest
public class CustomTraceFilterTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testCustomTraceHeaders() throws Exception {
mockMvc.perform(get("/test-endpoint")
.header("ot-custom-traceid", "12345")
.header("ot-custom-spanid", "67890"))
.andExpect(status().isOk());
}
}
Propagating Custom Headers in HTTP Requests Using RestTemplate
Using RestTemplate Interceptors to Add Custom Headers in Outgoing Requests
// Import necessary libraries
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
public class CustomHeaderInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
headers.add("ot-custom-traceid", "12345");
headers.add("ot-custom-spanid", "67890");
return execution.execute(request, body);
}
}
// Register the interceptor with RestTemplate
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new CustomHeaderInterceptor());
return restTemplate;
}
}
Handling Custom Header Traces with OpenTelemetry in Spring Boot 3.4
When working with Spring Boot 3.4, another powerful approach to propagate traces from custom headers is by integrating OpenTelemetry. OpenTelemetry, an open-source observability framework, helps instrument, collect, and export traces seamlessly. It provides mechanisms to extract and inject trace context, including custom headers like ot-custom-traceid and ot-custom-spanid, into your application. By leveraging OpenTelemetryâs TextMapPropagator, you can bridge the gap between non-standard clients and your observability system.
To use OpenTelemetry in Spring Boot 3.4, a custom propagator can be implemented to extract tracing information from the custom headers and attach it to the current trace context. For example, when your server receives an incoming request from Client Two, OpenTelemetry can parse custom headers and reconstruct the original trace context. This ensures that downstream services see the same trace IDs, allowing end-to-end visibility. Unlike older solutions like Spring Cloud Sleuth, OpenTelemetry is lightweight and aligns with modern observability standards.
By combining OpenTelemetryâs propagator with Micrometer, you can enrich your metrics and logging with trace information. Imagine seeing traces for requests coming from both Client One and Client Two seamlessly in your observability tool. OpenTelemetry automatically supports integrations with Prometheus, Zipkin, or Jaeger, enabling you to centralize trace visualization. This approach ensures that even when custom headers are involved, no trace data is lost, and debugging becomes significantly easier. đ
Frequently Asked Questions about Propagating Custom Traces in Spring Boot
- How do I manually extract custom trace headers in Spring Boot?
- You can use request.getHeader("custom-header") to manually fetch a specific header and add it to the MDC using MDC.put("traceId", value).
- What is the benefit of using OpenTelemetry for custom trace propagation?
- OpenTelemetry provides a modern, vendor-neutral approach to propagating traces, including custom headers, across microservices.
- Can I propagate custom headers with RestTemplate in Spring Boot?
- Yes, by implementing a ClientHttpRequestInterceptor, you can attach custom headers like traceid and spanid to outgoing requests.
- How do I register a filter to capture headers globally?
- You can create a filter that extends OncePerRequestFilter and register it using FilterRegistrationBean to capture headers for all endpoints.
- What tools can I use to visualize traces from Spring Boot?
- Tools like Zipkin, Jaeger, and Prometheus can integrate with Spring Boot and OpenTelemetry to visualize end-to-end traces.
Ensuring Seamless Trace Continuity
In modern systems, handling custom trace headers is critical for reliable observability. By using filters and interceptors, you can capture client-provided tracing information and propagate it correctly across your services. This avoids fragmented logs and missing traces. đ
Spring Boot 3.4, combined with Micrometer or OpenTelemetry, allows robust solutions without relying on older tools like Spring Cloud Sleuth. Whether you're dealing with Client Oneâs standard headers or Client Twoâs custom headers, implementing these techniques bridges the trace gaps efficiently. đ
Sources and References
- Spring Boot Official Documentation: Propagation of Tracing Contexts. Spring Boot Documentation
- OpenTelemetry for Java Developers: Guide to Trace Propagation. OpenTelemetry Java
- Micrometer Observability Documentation: Integrating Custom Trace Headers. Micrometer Observability
- SLF4J Logging API: Mapped Diagnostic Context (MDC) Use Cases. SLF4J Manual