diff --git a/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/OkHttpClientInstance.java b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/OkHttpClientInstance.java index 33154e2b..9dcd40b1 100644 --- a/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/OkHttpClientInstance.java +++ b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/OkHttpClientInstance.java @@ -71,6 +71,7 @@ static OkHttpClient createClient() { .readTimeout(getReadTimeout()) .writeTimeout(getWriteTimeout()) .addInterceptor(LOGGING_INTERCEPTOR) + .addInterceptor(new TraceHeadersInterceptor()) .cache(getCache()) .build(); } diff --git a/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/TraceHeadersInterceptor.java b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/TraceHeadersInterceptor.java new file mode 100644 index 00000000..6c91a120 --- /dev/null +++ b/cwms-http-client/src/main/java/mil/army/usace/hec/cwms/http/client/TraceHeadersInterceptor.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026 + * United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) + * All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. + * Source may not be released without written approval from HEC + */ + +package mil.army.usace.hec.cwms.http.client; + +import java.io.IOException; +import java.util.UUID; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +final class TraceHeadersInterceptor implements Interceptor { + + private static final String TRACE_PARENT_HEADER = "traceparent"; + private static final String TRACE_ID_HEADER = "X-Trace-Id"; + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + + String traceId = generateTraceId(); + String traceParent = createTraceParent(traceId); + + Request tracedRequest = request.newBuilder() + .header(TRACE_ID_HEADER, traceId) + .header(TRACE_PARENT_HEADER, traceParent) + .build(); + + return chain.proceed(tracedRequest); + } + + private static String generateTraceId() { + return UUID.randomUUID().toString().toLowerCase(); + } + + private static String createTraceParent(String traceId) { + String traceIdHex = traceId.replace("-", ""); + String spanId = UUID.randomUUID().toString().replace("-", "").substring(0, 16).toLowerCase(); + return "00-" + traceIdHex + "-" + spanId + "-01"; + } +} diff --git a/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TraceHeadersInterceptorTest.java b/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TraceHeadersInterceptorTest.java new file mode 100644 index 00000000..9188020e --- /dev/null +++ b/cwms-http-client/src/test/java/mil/army/usace/hec/cwms/http/client/TraceHeadersInterceptorTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 + * United States Army Corps of Engineers - Hydrologic Engineering Center (USACE/HEC) + * All Rights Reserved. USACE PROPRIETARY/CONFIDENTIAL. + * Source may not be released without written approval from HEC + */ + +package mil.army.usace.hec.cwms.http.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.regex.Pattern; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.Test; + +final class TraceHeadersInterceptorTest { + + private static final Pattern TRACE_ID_PATTERN = + Pattern.compile("[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", Pattern.CASE_INSENSITIVE); + + + @Test + void testCookiesNotLogged() throws IOException, InterruptedException { + try(MockWebServer mockWebServer = new MockWebServer()) { + MockResponse mockResponse = new MockResponse() + .setBody("test") + .setResponseCode(200); + mockWebServer.enqueue(mockResponse); + mockWebServer.start(); + + Request request = new Request.Builder() + .url(mockWebServer.url("/test")) + .build(); + try (Response response = OkHttpClientInstance.getInstance().newCall(request).execute()) { + assertTrue(response.isSuccessful()); + } + + okhttp3.mockwebserver.RecordedRequest recordedRequest = mockWebServer.takeRequest(); + String traceId = recordedRequest.getHeader("X-Trace-Id"); + String traceParent = recordedRequest.getHeader("traceparent"); + + assertTrue(traceId != null && TRACE_ID_PATTERN.matcher(traceId).matches(), + "X-Trace-Id should be a UUID-like value"); + assertTrue(traceParent != null && traceParent.matches( + "00-[a-f0-9]{32}-[a-f0-9]{16}-[a-f0-9]{2}"), + "traceparent should follow W3C format: https://www.w3.org/TR/trace-context/#traceparent-header"); + assertTrue(traceParent.startsWith("00-")); + assertEquals(4, traceParent.split("-").length); + } + } +}