diff --git a/cf-java-logging-support-core/beats/app-logs/docs/fields.asciidoc b/cf-java-logging-support-core/beats/app-logs/docs/fields.asciidoc index 3d23bc29..9c43a1b0 100644 --- a/cf-java-logging-support-core/beats/app-logs/docs/fields.asciidoc +++ b/cf-java-logging-support-core/beats/app-logs/docs/fields.asciidoc @@ -91,6 +91,19 @@ required: False A unique identifier that can be used to correlate multiple messages to a request. +==== w3c_traceparent + +type: string + +example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 + +required: False + +The content of the W3C traceparent header as defined in +https://www.w3.org/TR/trace-context/#traceparent-header. +The traceparent allows correlation of logs to the request. + + ==== sap_passport type: string diff --git a/cf-java-logging-support-core/beats/app-logs/etc/app-logs.template.json b/cf-java-logging-support-core/beats/app-logs/etc/app-logs.template.json index 2f18f6b5..137dafde 100644 --- a/cf-java-logging-support-core/beats/app-logs/etc/app-logs.template.json +++ b/cf-java-logging-support-core/beats/app-logs/etc/app-logs.template.json @@ -198,6 +198,11 @@ "index": "not_analyzed", "type": "string" }, + "w3c_traceparent": { + "doc_values": true, + "index": "not_analyzed", + "type": "string" + }, "written_at": { "doc_values": true, "ignore_malformed": true, diff --git a/cf-java-logging-support-core/beats/app-logs/etc/fields.yml b/cf-java-logging-support-core/beats/app-logs/etc/fields.yml index 527071c9..d4a02ff3 100644 --- a/cf-java-logging-support-core/beats/app-logs/etc/fields.yml +++ b/cf-java-logging-support-core/beats/app-logs/etc/fields.yml @@ -76,6 +76,15 @@ ctx: description: | A unique identifier that can be used to correlate multiple messages to a request. + - name: "w3c_traceparent" + type: string + required: false + example: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + description: | + The content of the W3C traceparent header as defined in + https://www.w3.org/TR/trace-context/#traceparent-header. + The traceparent allows correlation of logs to the request. + - name: "sap_passport" type: string required: false diff --git a/cf-java-logging-support-core/beats/request-metrics/docs/fields.asciidoc b/cf-java-logging-support-core/beats/request-metrics/docs/fields.asciidoc index c0711593..64ba9b51 100644 --- a/cf-java-logging-support-core/beats/request-metrics/docs/fields.asciidoc +++ b/cf-java-logging-support-core/beats/request-metrics/docs/fields.asciidoc @@ -100,6 +100,19 @@ required: False A unique identifier that can be used to correlate multiple messages to a request. +==== w3c_traceparent + +type: string + +example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 + +required: False + +The content of the W3C traceparent header as defined in +https://www.w3.org/TR/trace-context/#traceparent-header. +The traceparent allows correlation of logs to the request. + + ==== sap_passport type: string diff --git a/cf-java-logging-support-core/beats/request-metrics/etc/fields.yml b/cf-java-logging-support-core/beats/request-metrics/etc/fields.yml index e343fd20..3c87629b 100644 --- a/cf-java-logging-support-core/beats/request-metrics/etc/fields.yml +++ b/cf-java-logging-support-core/beats/request-metrics/etc/fields.yml @@ -85,6 +85,15 @@ ctx: description: | A unique identifier that can be used to correlate multiple messages to a request. + - name: "w3c_traceparent" + type: string + required: false + example: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + description: | + The content of the W3C traceparent header as defined in + https://www.w3.org/TR/trace-context/#traceparent-header. + The traceparent allows correlation of logs to the request. + - name: "sap_passport" type: string required: false diff --git a/cf-java-logging-support-core/beats/request-metrics/etc/request-metrics.template.json b/cf-java-logging-support-core/beats/request-metrics/etc/request-metrics.template.json index 7b278818..53d71a4e 100644 --- a/cf-java-logging-support-core/beats/request-metrics/etc/request-metrics.template.json +++ b/cf-java-logging-support-core/beats/request-metrics/etc/request-metrics.template.json @@ -263,6 +263,11 @@ "index": "not_analyzed", "type": "string" }, + "w3c_traceparent": { + "doc_values": true, + "index": "not_analyzed", + "type": "string" + }, "written_at": { "doc_values": true, "ignore_malformed": true, diff --git a/cf-java-logging-support-core/src/main/java/com/sap/hcp/cf/logging/common/Fields.java b/cf-java-logging-support-core/src/main/java/com/sap/hcp/cf/logging/common/Fields.java index 4dd0f8d0..26150d41 100644 --- a/cf-java-logging-support-core/src/main/java/com/sap/hcp/cf/logging/common/Fields.java +++ b/cf-java-logging-support-core/src/main/java/com/sap/hcp/cf/logging/common/Fields.java @@ -13,6 +13,7 @@ public interface Fields { public String WRITTEN_TS = "written_ts"; public String CORRELATION_ID = "correlation_id"; public String REQUEST_ID = "request_id"; + public String W3C_TRACEPARENT = "w3c_traceparent"; public String SAP_PASSPORT = "sap_passport"; public String SAP_PASSPORT_ACTION = "sap_passport_Action"; public String SAP_PASSPORT_ACTIONTYPE = "sap_passport_ActionType"; diff --git a/cf-java-logging-support-core/src/main/java/com/sap/hcp/cf/logging/common/request/HttpHeaders.java b/cf-java-logging-support-core/src/main/java/com/sap/hcp/cf/logging/common/request/HttpHeaders.java index b7646745..b9cffc27 100644 --- a/cf-java-logging-support-core/src/main/java/com/sap/hcp/cf/logging/common/request/HttpHeaders.java +++ b/cf-java-logging-support-core/src/main/java/com/sap/hcp/cf/logging/common/request/HttpHeaders.java @@ -36,6 +36,7 @@ public enum HttpHeaders implements HttpHeader { X_VCAP_REQUEST_ID("x-vcap-request-id", Fields.REQUEST_ID, true), // CORRELATION_ID("X-CorrelationID", Fields.CORRELATION_ID, true, X_VCAP_REQUEST_ID), // + W3C_TRACEPARENT("traceparent", Fields.W3C_TRACEPARENT, true), SAP_PASSPORT("sap-passport", Fields.SAP_PASSPORT, true), // TENANT_ID("tenantid", Fields.TENANT_ID, true); // diff --git a/cf-java-logging-support-core/src/test/java/com/sap/hcp/cf/logging/common/request/HttpHeadersTest.java b/cf-java-logging-support-core/src/test/java/com/sap/hcp/cf/logging/common/request/HttpHeadersTest.java index 67aca3b3..9fe70c0b 100644 --- a/cf-java-logging-support-core/src/test/java/com/sap/hcp/cf/logging/common/request/HttpHeadersTest.java +++ b/cf-java-logging-support-core/src/test/java/com/sap/hcp/cf/logging/common/request/HttpHeadersTest.java @@ -24,7 +24,7 @@ public void resetLogContext() { @Test public void hasCorrectNumberOfTypes() throws Exception { - assertThat(HttpHeaders.values().length, is(equalTo(19))); + assertThat(HttpHeaders.values().length, is(equalTo(20))); } @Test @@ -34,6 +34,7 @@ public void hasCorrectNames() throws Exception { assertThat(HttpHeaders.CORRELATION_ID.getName(), is("X-CorrelationID")); assertThat(HttpHeaders.REFERER.getName(), is("referer")); assertThat(HttpHeaders.TENANT_ID.getName(), is("tenantid")); + assertThat(HttpHeaders.W3C_TRACEPARENT.getName(), is("traceparent")); assertThat(HttpHeaders.X_CUSTOM_HOST.getName(), is("x-custom-host")); assertThat(HttpHeaders.X_FORWARDED_FOR.getName(), is("x-forwarded-for")); assertThat(HttpHeaders.X_FORWARDED_HOST.getName(), is("x-forwarded-host")); @@ -56,6 +57,7 @@ public void hasCorrectFields() throws Exception { assertThat(HttpHeaders.CORRELATION_ID.getField(), is(Fields.CORRELATION_ID)); assertThat(HttpHeaders.REFERER.getField(), is(nullValue())); assertThat(HttpHeaders.TENANT_ID.getField(), is(Fields.TENANT_ID)); + assertThat(HttpHeaders.W3C_TRACEPARENT.getField(), is(Fields.W3C_TRACEPARENT)); assertThat(HttpHeaders.X_CUSTOM_HOST.getField(), is(Fields.X_CUSTOM_HOST)); assertThat(HttpHeaders.X_FORWARDED_FOR.getField(), is(Fields.X_FORWARDED_FOR)); assertThat(HttpHeaders.X_FORWARDED_HOST.getField(), is(Fields.X_FORWARDED_HOST)); @@ -93,6 +95,7 @@ public void hasCorrectAliases() throws Exception { assertThat(HttpHeaders.CORRELATION_ID.getAliases(), containsInAnyOrder(HttpHeaders.X_VCAP_REQUEST_ID)); assertThat(HttpHeaders.REFERER.getAliases(), is(empty())); assertThat(HttpHeaders.TENANT_ID.getAliases(), is(empty())); + assertThat(HttpHeaders.W3C_TRACEPARENT.getAliases(), is(empty())); assertThat(HttpHeaders.X_CUSTOM_HOST.getAliases(), is(empty())); assertThat(HttpHeaders.X_FORWARDED_FOR.getAliases(), is(empty())); assertThat(HttpHeaders.X_FORWARDED_HOST.getAliases(), is(empty())); @@ -111,7 +114,8 @@ public void hasCorrectAliases() throws Exception { @Test public void propagatesCorrectHeaders() throws Exception { assertThat(HttpHeaders.propagated(), containsInAnyOrder(HttpHeaders.CORRELATION_ID, HttpHeaders.SAP_PASSPORT, - HttpHeaders.TENANT_ID, HttpHeaders.X_VCAP_REQUEST_ID)); + HttpHeaders.TENANT_ID, HttpHeaders.W3C_TRACEPARENT, + HttpHeaders.X_VCAP_REQUEST_ID)); } } diff --git a/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilter.java b/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilter.java index 0e055817..a7772514 100644 --- a/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilter.java +++ b/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilter.java @@ -1,8 +1,11 @@ package com.sap.hcp.cf.logging.servlet.filter; import static com.sap.hcp.cf.logging.common.customfields.CustomField.customField; +import static com.sap.hcp.cf.logging.common.request.HttpHeaders.W3C_TRACEPARENT; +import static java.util.Optional.ofNullable; import java.util.UUID; +import java.util.function.Predicate; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -25,13 +28,19 @@ public class CorrelationIdFilter extends AbstractLoggingFilter { private static final Logger LOG = LoggerFactory.getLogger(CorrelationIdFilter.class); private HttpHeader correlationHeader; + private HttpHeader traceparentHeader; public CorrelationIdFilter() { this(HttpHeaders.CORRELATION_ID); } public CorrelationIdFilter(HttpHeader correlationHeader) { + this(correlationHeader, W3C_TRACEPARENT); + } + + public CorrelationIdFilter(HttpHeader correlationHeader, HttpHeader traceparentHeader) { this.correlationHeader = correlationHeader; + this.traceparentHeader = traceparentHeader; } @Override @@ -43,7 +52,10 @@ protected void beforeFilter(HttpServletRequest request, HttpServletResponse resp private String determineCorrelationId(HttpServletRequest request) { String correlationId = HttpHeaderUtilities.getHeaderValue(request, correlationHeader); - if (correlationId == null || correlationId.isEmpty() || correlationId.equals(Defaults.UNKNOWN)) { + if (isBlankOrDefault(correlationId)) { + correlationId = getCorrelationIdFromTraceparent(request); + } + if (isBlankOrDefault(correlationId)) { correlationId = String.valueOf(UUID.randomUUID()); // add correlation-id as custom field, since it is added to MDC only // in the next step @@ -53,6 +65,25 @@ private String determineCorrelationId(HttpServletRequest request) { return correlationId; } + private boolean isBlankOrDefault(String value) { + return value == null || value.isEmpty() || value.equals(Defaults.UNKNOWN); + } + + private String getCorrelationIdFromTraceparent(HttpServletRequest request) { + String traceparent = HttpHeaderUtilities.getHeaderValue(request, traceparentHeader); + return ofNullable(traceparent).filter(not(this::isBlankOrDefault)).map(this::parseTraceparent).orElse( + null); + } + + private Predicate not(Predicate p) { + return p.negate(); + } + + private String parseTraceparent(String value) { + String[] tokens = value.split("-"); + return tokens.length >= 2 ? tokens[1] : null; + } + private void addCorrelationIdHeader(HttpServletResponse response, String correlationId) { if (!response.isCommitted() && response.getHeader(correlationHeader.getName()) == null) { response.setHeader(correlationHeader.getName(), correlationId); @@ -63,5 +94,4 @@ private void addCorrelationIdHeader(HttpServletResponse response, String correla protected void cleanup(HttpServletRequest request, HttpServletResponse response) { LogContext.remove(correlationHeader.getField()); } - } diff --git a/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilterTest.java b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilterTest.java index a14c5b6d..38ea9fff 100644 --- a/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilterTest.java +++ b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilterTest.java @@ -29,7 +29,9 @@ @RunWith(MockitoJUnitRunner.class) public class CorrelationIdFilterTest { - private static String KNOWN_CORRELATION_ID = UUID.randomUUID().toString(); + private static final String KNOWN_CORRELATION_ID = UUID.randomUUID().toString(); + private static final String KNOWN_TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736"; + private static final String KNOWN_TRACEPARENT = "00-" + KNOWN_TRACE_ID + "-00f067aa0ba902b7-01"; @Mock private HttpServletRequest request; @@ -108,13 +110,40 @@ public void doesNotOverwriteCorrelationIdInResponse() throws Exception { } @Test - public void usesCustomHeader() throws Exception { - HttpHeader myHeader = new HttpTestHeader("my-header", "my-field", null, false); - when(request.getHeader("my-header")).thenReturn(KNOWN_CORRELATION_ID); + public void usesCustomCorrelationIdHeader() throws Exception { + HttpHeader myCorrelationIdHeader = new HttpTestHeader("my-correlationId-header", "my-correlationId-field", null, + false); + HttpHeader myTraceparentHeader = new HttpTestHeader("my-traceparent-header", "my-traceparent-field", null, + false); + when(request.getHeader("my-correlationId-header")).thenReturn(KNOWN_CORRELATION_ID); + when(request.getHeader("my-traceparent-header")).thenReturn(KNOWN_TRACEPARENT); + + new CorrelationIdFilter(myCorrelationIdHeader, myTraceparentHeader).doFilter(request, response, chain); + + assertThat(mdcExtractor.getField("my-correlationId-field"), is(equalTo(KNOWN_CORRELATION_ID))); + verify(response).setHeader("my-correlationId-header", KNOWN_CORRELATION_ID); + } + + @Test + public void usesCustomTraceparentHeader() throws Exception { + HttpHeader myCorrelationIdHeader = new HttpTestHeader("my-correlationId-header", "my-correlationId-field", null, + false); + HttpHeader myTraceparentHeader = new HttpTestHeader("my-traceparent-header", "my-traceparent-field", null, + false); + when(request.getHeader("my-traceparent-header")).thenReturn(KNOWN_TRACEPARENT); - new CorrelationIdFilter(myHeader).doFilter(request, response, chain); + new CorrelationIdFilter(myCorrelationIdHeader, myTraceparentHeader).doFilter(request, response, chain); + + assertThat(mdcExtractor.getField("my-correlationId-field"), is(equalTo(KNOWN_TRACE_ID))); + verify(response).setHeader("my-correlationId-header", KNOWN_TRACE_ID); + } + + @Test + public void usesTraceparentIfCorrelationIdHeaderNotPresent() throws Exception { + when(request.getHeader(HttpHeaders.W3C_TRACEPARENT.getName())).thenReturn(KNOWN_TRACEPARENT); + + new CorrelationIdFilter().doFilter(request, response, chain); - assertThat(mdcExtractor.getField("my-field"), is(equalTo(KNOWN_CORRELATION_ID))); - verify(response).setHeader("my-header", KNOWN_CORRELATION_ID); + assertThat(getExtractedCorrelationId(), is(equalTo(KNOWN_TRACE_ID))); } } diff --git a/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestLogTest.java b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestLogTest.java index 5d660a82..ad956224 100644 --- a/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestLogTest.java +++ b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestLogTest.java @@ -139,6 +139,19 @@ public void logsSapPassportFromRequestHeader() throws Exception { } } + @Test + public void logsW3cTraceparentFromRequestHeader() throws Exception { + String traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + HttpGet get = createRequestWithHeader(HttpHeaders.W3C_TRACEPARENT.getName(), traceparent); + try (CloseableHttpResponse response = client.execute(get)) { + assertThat("Application log without traceparent.", getRequestMessage(), hasEntry(Fields.W3C_TRACEPARENT, + traceparent)); + assertThat("Request log without traceparent.", getRequestLog(), hasEntry(Fields.W3C_TRACEPARENT, + traceparent)); + } + + } + @Test public void writesCorrelationIdFromHeadersAsResponseHeader() throws Exception { String correlationId = UUID.randomUUID().toString(); diff --git a/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestLoggingFilterTest.java b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestLoggingFilterTest.java index 2f38eafa..dc41c51c 100644 --- a/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestLoggingFilterTest.java +++ b/cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/RequestLoggingFilterTest.java @@ -47,6 +47,7 @@ public class RequestLoggingFilterTest { private static final String REQUEST_ID = "1234-56-7890-xxx"; private static final String CORRELATION_ID = "xxx-56-7890-xxx"; private static final String TENANT_ID = "tenant1"; + private static final String W3C_TRACEPARENT = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; private static final String REQUEST = "/foobar"; private static final String QUERY_STRING = "baz=bla"; private static final String FULL_REQUEST = REQUEST + "?" + QUERY_STRING; @@ -211,6 +212,14 @@ public void testExplicitCorrelationId() throws IOException, ServletException { assertThat(getField(Fields.TENANT_ID), is(nullValue())); } + @Test + public void testExplicitW3cTraceparent() throws IOException, ServletException { + mockGetHeader(HttpHeaders.W3C_TRACEPARENT, W3C_TRACEPARENT); + FilterChain mockFilterChain = mock(FilterChain.class); + new RequestLoggingFilter().doFilter(mockReq, mockResp, mockFilterChain); + assertThat(getField(Fields.W3C_TRACEPARENT), is(W3C_TRACEPARENT)); + } + @Test public void testExplicitTenantId() throws IOException, ServletException { mockGetHeader(HttpHeaders.TENANT_ID, TENANT_ID);