Skip to content

Commit fee568d

Browse files
feature: add X-Correlation-Id to tracing filter (#129)
* feature: add X-Correlation-Id to tracing filter * feature: add X-Correlation-Id to tracing filter * feature: config logback.xml to put exception at the end * feature: config logback.xml to put exception at the end * feature: config logback.xml to put exception at the end --------- Co-authored-by: Duncan Crawford <duncan.crawford@hmcts.net>
1 parent d5e4fe7 commit fee568d

File tree

6 files changed

+100
-199
lines changed

6 files changed

+100
-199
lines changed
Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,49 @@
11
package uk.gov.hmcts.cp.filters.tracing;
22

3+
import jakarta.annotation.Nonnull;
34
import jakarta.servlet.FilterChain;
45
import jakarta.servlet.ServletException;
56
import jakarta.servlet.http.HttpServletRequest;
67
import jakarta.servlet.http.HttpServletResponse;
78
import lombok.extern.slf4j.Slf4j;
8-
import org.owasp.encoder.Encode;
99
import org.slf4j.MDC;
10-
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.core.Ordered;
11+
import org.springframework.core.annotation.Order;
1112
import org.springframework.stereotype.Component;
1213
import org.springframework.web.filter.OncePerRequestFilter;
1314

1415
import java.io.IOException;
16+
import java.util.UUID;
1517

1618
@Component
19+
@Order(Ordered.HIGHEST_PRECEDENCE)
1720
@Slf4j
1821
public class TracingFilter extends OncePerRequestFilter {
1922

20-
public static final String TRACE_ID = "traceId";
21-
public static final String SPAN_ID = "spanId";
22-
public static final String APPLICATION_NAME = "applicationName";
23+
public static final String CORRELATION_ID_KEY = "X-Correlation-Id";
2324

24-
private final String applicationName;
25-
26-
public TracingFilter(@Value("${spring.application.name}") final String applicationName) {
27-
this.applicationName = applicationName;
25+
@Override
26+
protected boolean shouldNotFilter(@Nonnull final HttpServletRequest request) {
27+
return false;
2828
}
2929

3030
@Override
31-
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
31+
protected void doFilterInternal(@Nonnull final HttpServletRequest request,
32+
@Nonnull final HttpServletResponse response,
33+
@Nonnull final FilterChain filterChain) throws ServletException, IOException {
3234
try {
33-
populateMDC(request, response, filterChain);
35+
final String correlationId = getCorrelationId(request);
36+
MDC.put(CORRELATION_ID_KEY, getCorrelationId(request));
37+
response.setHeader(CORRELATION_ID_KEY, correlationId);
38+
filterChain.doFilter(request, response);
3439
} finally {
35-
MDC.clear();
40+
MDC.remove(CORRELATION_ID_KEY);
3641
}
3742
}
3843

39-
public void populateMDC(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws IOException, ServletException {
40-
log.info("TracingFilter for uri:{}", Encode.forJava(request.getRequestURI()));
41-
MDC.put(APPLICATION_NAME, applicationName);
42-
if (request.getHeader(TRACE_ID) != null) {
43-
MDC.put(TRACE_ID, request.getHeader(TRACE_ID));
44-
response.setHeader(TRACE_ID, request.getHeader(TRACE_ID));
45-
}
46-
if (request.getHeader(SPAN_ID) != null) {
47-
MDC.put(SPAN_ID, request.getHeader(SPAN_ID));
48-
response.setHeader(SPAN_ID, request.getHeader(SPAN_ID));
49-
}
50-
filterChain.doFilter(request, response);
44+
private String getCorrelationId(final HttpServletRequest request) {
45+
return request.getHeader(CORRELATION_ID_KEY) == null
46+
? UUID.randomUUID().toString()
47+
: request.getHeader(CORRELATION_ID_KEY);
5148
}
5249
}

src/main/resources/logback.xml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
33
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
44
<providers>
5+
<mdc/>
56
<timestamp>
67
<fieldName>timestamp</fieldName>
78
<pattern>yyyy-MM-dd' 'HH:mm:ss.SSS</pattern>
89
</timestamp>
910
<!-- Lets keep the message to the left so its easier to find without scrolling -->
10-
<pattern>
11-
<pattern>{"message": "%message%n%xException{full}"}</pattern>
12-
</pattern>
13-
<mdc/>
11+
<message/>
1412
<loggerName/>
1513
<threadName/>
1614
<logLevel/>
15+
<pattern>
16+
<pattern>{"exception": "%xException{full}"}</pattern>
17+
</pattern>
1718
</providers>
1819
</encoder>
1920
</appender>
21+
2022
<root level="INFO">
2123
<appender-ref ref="JSON"/>
2224
</root>

src/test/java/uk/gov/hmcts/cp/filters/tracing/TracingFilterTest.java

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,63 @@
44
import jakarta.servlet.ServletException;
55
import jakarta.servlet.http.HttpServletRequest;
66
import jakarta.servlet.http.HttpServletResponse;
7+
import org.junit.jupiter.api.AfterEach;
78
import org.junit.jupiter.api.Test;
89
import org.junit.jupiter.api.extension.ExtendWith;
910
import org.mockito.Mock;
1011
import org.mockito.junit.jupiter.MockitoExtension;
1112
import org.slf4j.MDC;
1213

1314
import java.io.IOException;
15+
import java.util.UUID;
1416

1517
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.mockito.ArgumentMatchers.anyString;
19+
import static org.mockito.ArgumentMatchers.eq;
1620
import static org.mockito.Mockito.verify;
1721
import static org.mockito.Mockito.when;
18-
import static uk.gov.hmcts.cp.filters.tracing.TracingFilter.APPLICATION_NAME;
19-
import static uk.gov.hmcts.cp.filters.tracing.TracingFilter.SPAN_ID;
20-
import static uk.gov.hmcts.cp.filters.tracing.TracingFilter.TRACE_ID;
2122

2223
@ExtendWith(MockitoExtension.class)
2324
class TracingFilterTest {
25+
2426
@Mock
2527
private HttpServletRequest request;
2628
@Mock
2729
private HttpServletResponse response;
2830
@Mock
2931
private FilterChain filterChain;
3032

31-
private final TracingFilter tracingFilter = new TracingFilter("myAppName");
33+
private final TracingFilter filter = new TracingFilter();
34+
35+
@AfterEach
36+
void tearDown() {
37+
MDC.clear();
38+
}
39+
40+
@Test
41+
void shouldAlwaysFilter_all_paths() {
42+
assertThat(filter.shouldNotFilter(request)).isFalse();
43+
}
44+
45+
@Test
46+
void doFilterInternal_puts_correlationId_in_MDC_and_response_when_header_present() throws ServletException, IOException {
47+
final String correlationId = UUID.randomUUID().toString();
48+
when(request.getHeader(TracingFilter.CORRELATION_ID_KEY)).thenReturn(correlationId);
49+
50+
filter.doFilterInternal(request, response, filterChain);
51+
52+
verify(response).setHeader(TracingFilter.CORRELATION_ID_KEY, correlationId);
53+
verify(filterChain).doFilter(request, response);
54+
assertThat(MDC.get(TracingFilter.CORRELATION_ID_KEY)).isNull();
55+
}
3256

3357
@Test
34-
void filter_should_use_incoming_traceId() throws ServletException, IOException {
35-
when(request.getHeader(TRACE_ID)).thenReturn("incoming-traceId");
36-
when(request.getHeader(SPAN_ID)).thenReturn("incoming-spanId");
58+
void doFilterInternal_generates_correlationId_when_header_absent() throws ServletException, IOException {
59+
when(request.getHeader(TracingFilter.CORRELATION_ID_KEY)).thenReturn(null);
3760

38-
tracingFilter.populateMDC(request, response, filterChain);
61+
filter.doFilterInternal(request, response, filterChain);
3962

40-
assertThat(MDC.get(APPLICATION_NAME)).isEqualTo("myAppName");
41-
verify(response).setHeader(TRACE_ID, "incoming-traceId");
42-
assertThat(MDC.get(TRACE_ID)).isEqualTo("incoming-traceId");
43-
verify(response).setHeader(SPAN_ID, "incoming-spanId");
44-
assertThat(MDC.get(SPAN_ID)).isEqualTo("incoming-spanId");
63+
verify(response).setHeader(eq(TracingFilter.CORRELATION_ID_KEY), anyString());
64+
verify(filterChain).doFilter(request, response);
4565
}
46-
}
66+
}
Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,62 @@
11
package uk.gov.hmcts.cp.integration;
22

3-
import ch.qos.logback.classic.AsyncAppender;
43
import com.fasterxml.jackson.core.type.TypeReference;
54
import com.fasterxml.jackson.databind.ObjectMapper;
65
import lombok.extern.slf4j.Slf4j;
7-
import org.assertj.core.api.AssertionsForClassTypes;
86
import org.junit.jupiter.api.AfterEach;
97
import org.junit.jupiter.api.Test;
10-
import org.slf4j.LoggerFactory;
118
import org.slf4j.MDC;
129

1310
import java.io.ByteArrayOutputStream;
1411
import java.io.IOException;
1512
import java.io.PrintStream;
13+
import java.nio.charset.StandardCharsets;
1614
import java.util.Map;
15+
import java.util.UUID;
1716

1817
import static org.assertj.core.api.Assertions.assertThat;
18+
import static uk.gov.hmcts.cp.filters.tracing.TracingFilter.CORRELATION_ID_KEY;
1919

2020
@Slf4j
2121
class SpringLoggingIntegrationTest extends IntegrationTestBase {
22-
private PrintStream originalStdOut = System.out;
22+
23+
private final PrintStream originalStdOut = System.out;
2324

2425
@AfterEach
2526
void afterEach() {
2627
System.setOut(originalStdOut);
2728
}
2829

30+
UUID correlationId = UUID.randomUUID();
31+
2932
@Test
30-
void springboot_test_should_log_correct_fields_including_exception() throws IOException {
31-
MDC.put("any-mdc-field", "1234-1234");
32-
ByteArrayOutputStream capturedStdOut = captureStdOut();
33-
log.info("spring boot test message", new RuntimeException("MyException"));
34-
35-
String logMessage = capturedStdOut.toString();
36-
AssertionsForClassTypes.assertThat(logMessage).isNotEmpty();
37-
Map<String, Object> capturedFields = new ObjectMapper().readValue(logMessage, new TypeReference<>() {
38-
});
39-
assertThat(capturedFields.get("any-mdc-field")).isEqualTo("1234-1234");
33+
void springboot_test_should_log_correct_fields() throws IOException {
34+
MDC.put(CORRELATION_ID_KEY, correlationId.toString());
35+
final ByteArrayOutputStream capturedStdOut = captureStdOut();
36+
log.info("spring boot test message", new RuntimeException("TestException"));
37+
38+
final String logMessage = capturedStdOut.toString(StandardCharsets.UTF_8);
39+
assertThat(logMessage).isNotEmpty();
40+
41+
final Map<String, Object> capturedFields =
42+
new ObjectMapper().readValue(logMessage, new TypeReference<>() {
43+
});
44+
45+
assertThat(capturedFields.get(CORRELATION_ID_KEY)).isEqualTo(correlationId.toString());
4046
assertThat(capturedFields.get("timestamp")).isNotNull();
41-
assertThat(capturedFields.get("logger_name")).isEqualTo("uk.gov.hmcts.cp.integration.SpringLoggingIntegrationTest");
47+
assertThat(capturedFields.get("message").toString()).contains("spring boot test message");
48+
assertThat(capturedFields.get("exception").toString())
49+
.contains("java.lang.RuntimeException: TestException")
50+
.contains("cp.integration.SpringLoggingIntegrationTest");
51+
assertThat(capturedFields.get("logger_name"))
52+
.isEqualTo("uk.gov.hmcts.cp.integration.SpringLoggingIntegrationTest");
4253
assertThat(capturedFields.get("thread_name")).isEqualTo("Test worker");
4354
assertThat(capturedFields.get("level")).isEqualTo("INFO");
44-
assertThat(capturedFields.get("message").toString()).contains("spring boot test message\njava.lang.RuntimeException: MyException");
45-
assertThat(capturedFields.get("message").toString()).contains("at uk.gov.hmcts.cp.integration.SpringLoggingIntegrationTest");
4655
}
4756

4857
private ByteArrayOutputStream captureStdOut() {
4958
final ByteArrayOutputStream capturedStdOut = new ByteArrayOutputStream();
50-
System.setOut(new PrintStream(capturedStdOut));
59+
System.setOut(new PrintStream(capturedStdOut, true, StandardCharsets.UTF_8));
5160
return capturedStdOut;
5261
}
5362
}

0 commit comments

Comments
 (0)