From a3e628b360708b3beddf5e819095d2b9d554546f Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Mon, 13 Oct 2025 13:28:39 +0700 Subject: [PATCH 01/14] add WoodyTraceLifecycleHandler --- .../dev/vality/wachter/config/WebConfig.java | 8 +- .../config/properties/TracingProperties.java | 16 ++ .../tracing/WoodyTraceLifecycleHandler.java | 228 ++++++++++++++++++ .../wachter/tracing/WoodyTracingFilter.java | 93 ++++--- src/main/resources/application.yml | 4 + 5 files changed, 297 insertions(+), 52 deletions(-) create mode 100644 src/main/java/dev/vality/wachter/config/properties/TracingProperties.java create mode 100644 src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java diff --git a/src/main/java/dev/vality/wachter/config/WebConfig.java b/src/main/java/dev/vality/wachter/config/WebConfig.java index 539c1f3..074a5c5 100644 --- a/src/main/java/dev/vality/wachter/config/WebConfig.java +++ b/src/main/java/dev/vality/wachter/config/WebConfig.java @@ -1,5 +1,7 @@ package dev.vality.wachter.config; +import dev.vality.wachter.config.properties.TracingProperties; +import dev.vality.wachter.tracing.WoodyTraceLifecycleHandler; import dev.vality.wachter.tracing.WoodyTracingFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -51,8 +53,10 @@ protected void doFilterInternal(HttpServletRequest request, } @Bean - public FilterRegistrationBean woodyTracingFilter() { - var registrationBean = new FilterRegistrationBean<>(new WoodyTracingFilter(serverPort, wachterEndpoint)); + public FilterRegistrationBean woodyTracingFilter(TracingProperties tracingProperties) { + var lifecycleHandler = new WoodyTraceLifecycleHandler(); + var filter = new WoodyTracingFilter(serverPort, wachterEndpoint, tracingProperties, lifecycleHandler); + var registrationBean = new FilterRegistrationBean<>(filter); registrationBean.setOrder(-50); registrationBean.setName("woodyTracingFilter"); registrationBean.addUrlPatterns(wachterEndpoint); diff --git a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java new file mode 100644 index 0000000..fedb403 --- /dev/null +++ b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java @@ -0,0 +1,16 @@ +package dev.vality.wachter.config.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "woody-http-bridge.tracing") +public class TracingProperties { + + private boolean traceRestore = true; + +} diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java b/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java new file mode 100644 index 0000000..41d2268 --- /dev/null +++ b/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java @@ -0,0 +1,228 @@ +package dev.vality.wachter.tracing; + +import dev.vality.woody.api.flow.error.WErrorDefinition; +import dev.vality.woody.api.flow.error.WErrorSource; +import dev.vality.woody.api.flow.error.WErrorType; +import dev.vality.woody.api.flow.error.WRuntimeException; +import dev.vality.woody.api.trace.ContextSpan; +import dev.vality.woody.api.trace.MetadataProperties; +import dev.vality.woody.api.trace.TraceData; +import dev.vality.woody.api.trace.context.TraceContext; +import dev.vality.woody.thrift.impl.http.THMetadataProperties; +import dev.vality.woody.thrift.impl.http.THResponseInfo; +import dev.vality.woody.thrift.impl.http.error.THProviderErrorMapper; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.semconv.HttpAttributes; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static dev.vality.wachter.constants.TraceHeadersConstants.*; + +@Slf4j +public final class WoodyTraceLifecycleHandler { + + private final THProviderErrorMapper errorMapper = new THProviderErrorMapper(); + + void handleSuccess(HttpServletResponse response) { + var traceData = TraceContext.getCurrentTraceData(); + updateSpanStatus(traceData, response.getStatus()); + applyHeaders(response, traceData, null); + } + + void handleWoodyException(HttpServletResponse response, Throwable throwable) { + var traceData = TraceContext.getCurrentTraceData(); + var responseInfo = resolveResponseInfo(traceData, throwable); + applyResponseInfo(response, responseInfo); + recordException(traceData, response, throwable); + applyHeaders(response, traceData, responseInfo); + flushQuietly(response); + } + + void handleUnexpectedError(HttpServletResponse response, Throwable throwable) { + var traceData = TraceContext.getCurrentTraceData(); + var responseInfo = resolveResponseInfo(traceData, fallbackDefinition(throwable)); + applyResponseInfo(response, responseInfo); + recordException(traceData, response, throwable); + applyHeaders(response, traceData, responseInfo); + flushQuietly(response); + } + + private void updateSpanStatus(TraceData traceData, int status) { + var span = extractSpan(traceData); + if (span == null || !span.getSpanContext().isValid()) { + return; + } + if (status > 0) { + span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, status); + span.setStatus(status >= 500 ? StatusCode.ERROR : StatusCode.OK); + } else { + span.setStatus(StatusCode.OK); + } + } + + private void recordException(TraceData traceData, HttpServletResponse response, Throwable throwable) { + var span = extractSpan(traceData); + if (span == null || !span.getSpanContext().isValid()) { + return; + } + var status = response.getStatus(); + if (status > 0) { + span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, status); + } + span.recordException(throwable); + span.setStatus(StatusCode.ERROR); + } + + private void applyHeaders(HttpServletResponse response, TraceData traceData, THResponseInfo responseInfo) { + if (response == null || response.isCommitted() || traceData == null) { + return; + } + var serviceSpan = traceData.getServiceSpan(); + if (serviceSpan == null || !serviceSpan.isFilled()) { + return; + } + + var headers = new HttpHeaders(); + var span = serviceSpan.getSpan(); + addHeader(headers, WOODY_TRACE_ID, span.getTraceId()); + addHeader(headers, WOODY_SPAN_ID, span.getId()); + addHeader(headers, WOODY_PARENT_ID, span.getParentId()); + var deadline = span.getDeadline(); + if (deadline != null) { + addHeader(headers, WOODY_DEADLINE, deadline.toString()); + } + serviceSpan.getCustomMetadata().getKeys() + .forEach(key -> addHeader(headers, WOODY_META_PREFIX + key, + serviceSpan.getCustomMetadata().getValue(key))); + + if (responseInfo != null) { + addHeader(headers, WOODY_ERROR_CLASS, responseInfo.getErrClass()); + addHeader(headers, WOODY_ERROR_REASON, responseInfo.getErrReason()); + } + + addHeader(headers, OTEL_TRACE_PARENT, traceData.getInboundTraceParent()); + addHeader(headers, OTEL_TRACE_STATE, traceData.getInboundTraceState()); + + headers.forEach((name, values) -> { + if (values != null) { + response.setHeader(name, join(values)); + } + }); + } + + private THResponseInfo resolveResponseInfo(TraceData traceData, Throwable throwable) { + if (traceData == null) { + return fallbackResponseInfo(fallbackDefinition(throwable)); + } + var serviceSpan = traceData.getServiceSpan(); + if (serviceSpan == null) { + return fallbackResponseInfo(fallbackDefinition(throwable)); + } + serviceSpan.getMetadata().putValue(MetadataProperties.CALL_ERROR, throwable); + var definition = extractDefinition(serviceSpan, throwable); + serviceSpan.getMetadata().putValue(MetadataProperties.ERROR_DEFINITION, definition); + var responseInfo = THProviderErrorMapper.getResponseInfo(serviceSpan); + serviceSpan.getMetadata().putValue(THMetadataProperties.TH_RESPONSE_INFO, responseInfo); + return responseInfo; + } + + private THResponseInfo resolveResponseInfo(TraceData traceData, WErrorDefinition definition) { + if (traceData == null) { + return fallbackResponseInfo(definition); + } + var serviceSpan = traceData.getServiceSpan(); + if (serviceSpan == null) { + return fallbackResponseInfo(definition); + } + serviceSpan.getMetadata().putValue(MetadataProperties.ERROR_DEFINITION, definition); + var responseInfo = THProviderErrorMapper.getResponseInfo(serviceSpan); + serviceSpan.getMetadata().putValue(THMetadataProperties.TH_RESPONSE_INFO, responseInfo); + return responseInfo; + } + + private WErrorDefinition extractDefinition(ContextSpan serviceSpan, Throwable throwable) { + if (throwable instanceof WRuntimeException runtime) { + return runtime.getErrorDefinition(); + } + var mapped = errorMapper.mapToDef(throwable, serviceSpan); + if (mapped != null) { + return mapped; + } + return fallbackDefinition(throwable); + } + + private WErrorDefinition fallbackDefinition(Throwable throwable) { + var definition = new WErrorDefinition(WErrorSource.INTERNAL); + definition.setErrorType(WErrorType.UNEXPECTED_ERROR); + definition.setErrorSource(WErrorSource.INTERNAL); + if (throwable != null) { + definition.setErrorReason(Objects.toString(throwable.getMessage(), WErrorType.UNEXPECTED_ERROR.getKey())); + definition.setErrorName(throwable.getClass().getSimpleName()); + definition.setErrorMessage(throwable.getMessage()); + } else { + definition.setErrorReason(WErrorType.UNEXPECTED_ERROR.getKey()); + definition.setErrorName(WErrorType.UNEXPECTED_ERROR.getKey()); + definition.setErrorMessage(WErrorType.UNEXPECTED_ERROR.getKey()); + } + return definition; + } + + private void applyResponseInfo(HttpServletResponse response, THResponseInfo responseInfo) { + if (response == null || response.isCommitted() || responseInfo == null) { + return; + } + if (responseInfo.getStatus() > 0) { + response.setStatus(responseInfo.getStatus()); + } + } + + private THResponseInfo fallbackResponseInfo(WErrorDefinition definition) { + var status = definition != null && definition.getErrorType() == WErrorType.BUSINESS_ERROR + ? HttpServletResponse.SC_OK + : HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + var errClass = definition != null && definition.getErrorType() != null + ? definition.getErrorType().getKey() + : WErrorType.UNEXPECTED_ERROR.getKey(); + var errReason = definition != null ? definition.getErrorReason() : WErrorType.UNEXPECTED_ERROR.getKey(); + return new THResponseInfo(status, errClass, errReason); + } + + private Span extractSpan(TraceData traceData) { + return traceData == null ? null : traceData.getOtelSpan(); + } + + private static void addHeader(HttpHeaders headers, String name, String value) { + if (name != null && value != null && !value.isEmpty()) { + headers.set(name, value); + } + } + + private static String join(List values) { + if (values == null || values.isEmpty()) { + return ""; + } + if (values.size() == 1) { + return values.getFirst(); + } + return String.join(",", new ArrayList<>(values)); + } + + private static void flushQuietly(HttpServletResponse response) { + if (response == null) { + return; + } + try { + response.flushBuffer(); + } catch (Exception exception) { + if (log.isDebugEnabled()) { + log.debug("Failed to flush response buffer", exception); + } + } + } +} diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java b/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java index 1fb1143..d877704 100644 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java +++ b/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java @@ -1,15 +1,14 @@ package dev.vality.wachter.tracing; +import dev.vality.wachter.config.properties.TracingProperties; import dev.vality.woody.api.flow.WFlow; -import dev.vality.woody.api.trace.context.TraceContext; -import io.opentelemetry.semconv.HttpAttributes; +import dev.vality.woody.api.flow.error.WRuntimeException; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.slf4j.MDC; import org.springframework.http.HttpHeaders; import org.springframework.web.filter.OncePerRequestFilter; @@ -20,9 +19,6 @@ import java.util.stream.Collectors; import static dev.vality.wachter.config.WebConfig.getRequestPath; -import static dev.vality.wachter.constants.TraceHeadersConstants.WOODY_TRACE_ID; -import static io.opentelemetry.api.trace.StatusCode.ERROR; -import static io.opentelemetry.api.trace.StatusCode.OK; @Slf4j @RequiredArgsConstructor @@ -36,6 +32,8 @@ public final class WoodyTracingFilter extends OncePerRequestFilter { private final int serverPort; private final String wachterEndpoint; + private final TracingProperties tracingProperties; + private final WoodyTraceLifecycleHandler woodyTraceLifecycleHandler; @Override protected void doFilterInternal(HttpServletRequest request, @@ -43,70 +41,65 @@ protected void doFilterInternal(HttpServletRequest request, FilterChain filterChain) { var requestPath = getRequestPath(request); if ((request.getLocalPort() == serverPort) && requestPath.equals(wachterEndpoint)) { - var normalized = TraceContextHeadersNormalizer.normalize(request); - var validated = TraceContextHeadersValidation.validate(normalized); - MDC.put(WOODY_TRACE_ID, validated.get(WOODY_TRACE_ID) != null ? validated.get(WOODY_TRACE_ID) : ""); - log.info("-> Received {} {} | params: {}, headers: {}", request.getMethod(), getRequestPath(request), - extractParams(request), sanitizeHeaders(request)); - MDC.remove(WOODY_TRACE_ID); - var restoredTraceData = TraceContextRestorer.restoreTraceData(validated); - - WFlow.create(() -> doFilterWithTraceHandling(request, response, filterChain), restoredTraceData).run(); - - MDC.put(WOODY_TRACE_ID, validated.get(WOODY_TRACE_ID) != null ? validated.get(WOODY_TRACE_ID) : ""); - log.info("<- Sent {} {} | status: {}, headers: {}", request.getMethod(), getRequestPath(request), - response.getStatus(), sanitizeResponseHeaders(response)); - MDC.remove(WOODY_TRACE_ID); + if (tracingProperties.isTraceRestore()) { + handleWithTraceRestore(request, response, filterChain); + } else { + handleLightweightRequest(request, response, filterChain); + } return; } doFilter(request, response, filterChain); } + private void handleWithTraceRestore(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) { + var normalized = TraceContextHeadersNormalizer.normalize(request); + var headersForTrace = TraceContextHeadersValidation.validate(normalized); + var restoredTraceData = TraceContextRestorer.restoreTraceData(headersForTrace); + WFlow.create(() -> { + logReceived(request); + doFilterWithTraceHandling(request, response, filterChain); + logSent(request, response); + }, restoredTraceData).run(); + } + + private void handleLightweightRequest(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) { + logReceived(request); + new WFlow().createServiceFork(() -> doFilter(request, response, filterChain)).run(); + logSent(request, response); + } + @SneakyThrows private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { filterChain.doFilter(request, response); } - @SneakyThrows private void doFilterWithTraceHandling(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { - var traceData = TraceContext.getCurrentTraceData(); - var span = traceData != null ? traceData.getOtelSpan() : null; try { filterChain.doFilter(request, response); - recordResponse(span, response); - } catch (Throwable t) { - recordException(span, response, t); - throw t; + woodyTraceLifecycleHandler.handleSuccess(response); + } catch (WRuntimeException woodyError) { + log.warn("Handled Woody exception during request processing", woodyError); + woodyTraceLifecycleHandler.handleWoodyException(response, woodyError); + } catch (Throwable unexpected) { + log.error("Unhandled exception during request processing", unexpected); + woodyTraceLifecycleHandler.handleUnexpectedError(response, unexpected); } } - private void recordResponse(io.opentelemetry.api.trace.Span span, HttpServletResponse response) { - if (span == null || !span.getSpanContext().isValid()) { - return; - } - var status = response.getStatus(); - if (status > 0) { - span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, status); - span.setStatus(status >= 500 ? ERROR : OK); - } else { - span.setStatus(OK); - } + private void logReceived(HttpServletRequest request) { + log.info("-> Received {} {} | params: {}, headers: {}", request.getMethod(), getRequestPath(request), + extractParams(request), sanitizeHeaders(request)); } - private void recordException(io.opentelemetry.api.trace.Span span, - HttpServletResponse response, - Throwable throwable) { - if (span == null || !span.getSpanContext().isValid()) { - return; - } - var status = response.getStatus(); - if (status > 0) { - span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, status); - } - span.recordException(throwable); - span.setStatus(ERROR); + private void logSent(HttpServletRequest request, HttpServletResponse response) { + log.info("<- Sent {} {} | status: {}, headers: {}", request.getMethod(), getRequestPath(request), + response.getStatus(), sanitizeResponseHeaders(response)); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9059e12..e9e4a40 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -43,6 +43,10 @@ info: version: '@project.version@' stage: dev +woody-http-bridge: + tracing: + trace-restore: true + wachter: endpoint: ${wachter.endpoint} auth: From 8e87703f8615db43b5afc3611c4d5703f1efb95f Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Mon, 13 Oct 2025 14:27:37 +0700 Subject: [PATCH 02/14] add response-header-mode --- .../tracing/WoodyTraceLifecycleHandler.java | 63 +++++++++++++++++-- src/main/resources/application.yml | 3 + 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java b/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java index 41d2268..a293f0a 100644 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java +++ b/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java @@ -1,5 +1,6 @@ package dev.vality.wachter.tracing; +import dev.vality.wachter.config.properties.TracingProperties.ResponseHeaderMode; import dev.vality.woody.api.flow.error.WErrorDefinition; import dev.vality.woody.api.flow.error.WErrorSource; import dev.vality.woody.api.flow.error.WErrorType; @@ -22,12 +23,18 @@ import java.util.List; import java.util.Objects; +import static dev.vality.wachter.constants.TraceHeadersConstants.ExternalHeaders.*; import static dev.vality.wachter.constants.TraceHeadersConstants.*; @Slf4j public final class WoodyTraceLifecycleHandler { private final THProviderErrorMapper errorMapper = new THProviderErrorMapper(); + private final ResponseHeaderMode responseHeaderMode; + + public WoodyTraceLifecycleHandler(ResponseHeaderMode responseHeaderMode) { + this.responseHeaderMode = responseHeaderMode == null ? ResponseHeaderMode.WOODY : responseHeaderMode; + } void handleSuccess(HttpServletResponse response) { var traceData = TraceContext.getCurrentTraceData(); @@ -109,11 +116,14 @@ private void applyHeaders(HttpServletResponse response, TraceData traceData, THR addHeader(headers, OTEL_TRACE_PARENT, traceData.getInboundTraceParent()); addHeader(headers, OTEL_TRACE_STATE, traceData.getInboundTraceState()); - headers.forEach((name, values) -> { - if (values != null) { - response.setHeader(name, join(values)); + switch (responseHeaderMode) { + case OFF -> { + // no headers } - }); + case WOODY -> applyWoodyHeaders(response, headers); + case X_WOODY -> applyXWoodyHeaders(response, headers); + case HTTP -> applyHttpHeaders(response, headers); + } } private THResponseInfo resolveResponseInfo(TraceData traceData, Throwable throwable) { @@ -213,6 +223,51 @@ private static String join(List values) { return String.join(",", new ArrayList<>(values)); } + private static void applyWoodyHeaders(HttpServletResponse response, HttpHeaders headers) { + headers.forEach((name, values) -> { + if (values != null) { + response.setHeader(name, join(values)); + } + }); + } + + private static void applyXWoodyHeaders(HttpServletResponse response, HttpHeaders headers) { + var normalized = TraceContextHeadersNormalizer.normalizeResponseHeaders(headers); + normalized.forEach((name, values) -> { + if (values != null) { + response.setHeader(name, join(values)); + } + }); + } + + private void applyHttpHeaders(HttpServletResponse response, HttpHeaders headers) { + var httpHeaders = new HttpHeaders(); + copyHeader(headers, OTEL_TRACE_PARENT, OTEL_TRACE_PARENT, httpHeaders); + copyHeader(headers, OTEL_TRACE_STATE, OTEL_TRACE_STATE, httpHeaders); + copyHeader(headers, WOODY_META_REQUEST_ID, X_REQUEST_ID, httpHeaders); + copyHeader(headers, WOODY_META_REQUEST_DEADLINE, X_REQUEST_DEADLINE, httpHeaders); + copyHeader(headers, WOODY_META_REQUEST_INVOICE_ID, X_INVOICE_ID, httpHeaders); + if (response.getStatus() >= 400) { + copyHeader(headers, WOODY_ERROR_CLASS, X_ERROR_CLASS, httpHeaders); + copyHeader(headers, WOODY_ERROR_REASON, X_ERROR_REASON, httpHeaders); + } + httpHeaders.forEach((name, values) -> { + if (values != null) { + response.setHeader(name, join(values)); + } + }); + } + + private static void copyHeader(HttpHeaders source, String sourceName, String targetName, HttpHeaders target) { + if (sourceName == null || targetName == null) { + return; + } + var values = source.get(sourceName); + if (values != null && !values.isEmpty()) { + target.put(targetName, new ArrayList<>(values)); + } + } + private static void flushQuietly(HttpServletResponse response) { if (response == null) { return; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e9e4a40..12cfc81 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,6 +46,9 @@ info: woody-http-bridge: tracing: trace-restore: true + response-header-mode: X_WOODY + endpoints: + - path: /${wachter.endpoint} wachter: endpoint: ${wachter.endpoint} From 3766cd2b71a477e4a5a5e4c5b513f5b17b85e665 Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Mon, 13 Oct 2025 16:49:20 +0700 Subject: [PATCH 03/14] refactor --- .../config/properties/TracingProperties.java | 28 +++++ .../constants/TraceHeadersConstants.java | 2 + .../tracing/TraceContextHeadersExtractor.java | 1 + .../wachter/tracing/TraceContextRestorer.java | 1 + .../tracing/WoodyTraceLifecycleHandler.java | 113 +++++++++--------- src/main/resources/application.yml | 3 +- 6 files changed, 92 insertions(+), 56 deletions(-) diff --git a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java index fedb403..e6f1cf7 100644 --- a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java +++ b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java @@ -5,6 +5,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; + @Getter @Setter @Component @@ -12,5 +15,30 @@ public class TracingProperties { private boolean traceRestore = true; + private ResponseHeaderMode responseHeaderMode = ResponseHeaderMode.OFF; + private List endpoints = new ArrayList<>(); + private Boolean propagateErrors; + + @Getter + @Setter + public static class Endpoint { + + private Integer port; + private String path; + + } + + public boolean shouldPropagateErrors() { + if (propagateErrors != null) { + return propagateErrors; + } + return responseHeaderMode == ResponseHeaderMode.OFF; + } + public enum ResponseHeaderMode { + WOODY, + X_WOODY, + HTTP, + OFF + } } diff --git a/src/main/java/dev/vality/wachter/constants/TraceHeadersConstants.java b/src/main/java/dev/vality/wachter/constants/TraceHeadersConstants.java index 2fc46af..e1fd21f 100644 --- a/src/main/java/dev/vality/wachter/constants/TraceHeadersConstants.java +++ b/src/main/java/dev/vality/wachter/constants/TraceHeadersConstants.java @@ -46,6 +46,8 @@ public static final class ExternalHeaders { public static final String X_WOODY_META_USERNAME = X_WOODY_META_PREFIX + XWoodyMetaHeaders.USERNAME; public static final String X_WOODY_META_EMAIL = X_WOODY_META_PREFIX + XWoodyMetaHeaders.EMAIL; public static final String X_WOODY_META_REALM = X_WOODY_META_PREFIX + XWoodyMetaHeaders.REALM; + public static final String X_ERROR_CLASS = "X-Error-Class"; + public static final String X_ERROR_REASON = "X-Error-Reason"; public static final class XWoodyMetaHeaders { diff --git a/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersExtractor.java b/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersExtractor.java index 70f496b..171cbc8 100644 --- a/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersExtractor.java +++ b/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersExtractor.java @@ -40,6 +40,7 @@ public Map extractHeaders() { GlobalOpenTelemetry.getPropagators() .getTextMapPropagator() .inject(traceData.getOtelContext(), headers, MAP_SETTER); + log.debug("Extracted trace headers: {}", headers); return headers; } diff --git a/src/main/java/dev/vality/wachter/tracing/TraceContextRestorer.java b/src/main/java/dev/vality/wachter/tracing/TraceContextRestorer.java index dd8faac..2a760c9 100644 --- a/src/main/java/dev/vality/wachter/tracing/TraceContextRestorer.java +++ b/src/main/java/dev/vality/wachter/tracing/TraceContextRestorer.java @@ -20,6 +20,7 @@ public class TraceContextRestorer { public TraceData restoreTraceData(Map headers) { + log.debug("Restoring trace data from headers: {}", headers); var traceData = TraceContext.initNewServiceTrace(new TraceData(), WFlow.createDefaultIdGenerator(), WFlow.createDefaultIdGenerator()); if (headers.isEmpty()) { diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java b/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java index a293f0a..369f443 100644 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java +++ b/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java @@ -38,7 +38,7 @@ public WoodyTraceLifecycleHandler(ResponseHeaderMode responseHeaderMode) { void handleSuccess(HttpServletResponse response) { var traceData = TraceContext.getCurrentTraceData(); - updateSpanStatus(traceData, response.getStatus()); + recordOtelSpanStatus(traceData, response.getStatus()); applyHeaders(response, traceData, null); } @@ -46,7 +46,7 @@ void handleWoodyException(HttpServletResponse response, Throwable throwable) { var traceData = TraceContext.getCurrentTraceData(); var responseInfo = resolveResponseInfo(traceData, throwable); applyResponseInfo(response, responseInfo); - recordException(traceData, response, throwable); + recordOtelSpanException(traceData, response, throwable); applyHeaders(response, traceData, responseInfo); flushQuietly(response); } @@ -55,12 +55,56 @@ void handleUnexpectedError(HttpServletResponse response, Throwable throwable) { var traceData = TraceContext.getCurrentTraceData(); var responseInfo = resolveResponseInfo(traceData, fallbackDefinition(throwable)); applyResponseInfo(response, responseInfo); - recordException(traceData, response, throwable); + recordOtelSpanException(traceData, response, throwable); applyHeaders(response, traceData, responseInfo); flushQuietly(response); } - private void updateSpanStatus(TraceData traceData, int status) { + void recordOtelSpanException(Throwable throwable) { + var traceData = TraceContext.getCurrentTraceData(); + recordOtelSpanException(traceData, null, throwable); + } + + private THResponseInfo resolveResponseInfo(TraceData traceData, Throwable throwable) { + if (traceData == null) { + return fallbackResponseInfo(fallbackDefinition(throwable)); + } + var serviceSpan = traceData.getServiceSpan(); + if (serviceSpan == null) { + return fallbackResponseInfo(fallbackDefinition(throwable)); + } + serviceSpan.getMetadata().putValue(MetadataProperties.CALL_ERROR, throwable); + var definition = extractDefinition(serviceSpan, throwable); + serviceSpan.getMetadata().putValue(MetadataProperties.ERROR_DEFINITION, definition); + var responseInfo = THProviderErrorMapper.getResponseInfo(serviceSpan); + serviceSpan.getMetadata().putValue(THMetadataProperties.TH_RESPONSE_INFO, responseInfo); + return responseInfo; + } + + private THResponseInfo resolveResponseInfo(TraceData traceData, WErrorDefinition definition) { + if (traceData == null) { + return fallbackResponseInfo(definition); + } + var serviceSpan = traceData.getServiceSpan(); + if (serviceSpan == null) { + return fallbackResponseInfo(definition); + } + serviceSpan.getMetadata().putValue(MetadataProperties.ERROR_DEFINITION, definition); + var responseInfo = THProviderErrorMapper.getResponseInfo(serviceSpan); + serviceSpan.getMetadata().putValue(THMetadataProperties.TH_RESPONSE_INFO, responseInfo); + return responseInfo; + } + + private void applyResponseInfo(HttpServletResponse response, THResponseInfo responseInfo) { + if (response == null || response.isCommitted() || responseInfo == null) { + return; + } + if (responseInfo.getStatus() > 0) { + response.setStatus(responseInfo.getStatus()); + } + } + + private void recordOtelSpanStatus(TraceData traceData, int status) { var span = extractSpan(traceData); if (span == null || !span.getSpanContext().isValid()) { return; @@ -73,12 +117,12 @@ private void updateSpanStatus(TraceData traceData, int status) { } } - private void recordException(TraceData traceData, HttpServletResponse response, Throwable throwable) { + private void recordOtelSpanException(TraceData traceData, HttpServletResponse response, Throwable throwable) { var span = extractSpan(traceData); if (span == null || !span.getSpanContext().isValid()) { return; } - var status = response.getStatus(); + var status = response != null ? response.getStatus() : 0; if (status > 0) { span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, status); } @@ -126,34 +170,15 @@ private void applyHeaders(HttpServletResponse response, TraceData traceData, THR } } - private THResponseInfo resolveResponseInfo(TraceData traceData, Throwable throwable) { - if (traceData == null) { - return fallbackResponseInfo(fallbackDefinition(throwable)); - } - var serviceSpan = traceData.getServiceSpan(); - if (serviceSpan == null) { - return fallbackResponseInfo(fallbackDefinition(throwable)); - } - serviceSpan.getMetadata().putValue(MetadataProperties.CALL_ERROR, throwable); - var definition = extractDefinition(serviceSpan, throwable); - serviceSpan.getMetadata().putValue(MetadataProperties.ERROR_DEFINITION, definition); - var responseInfo = THProviderErrorMapper.getResponseInfo(serviceSpan); - serviceSpan.getMetadata().putValue(THMetadataProperties.TH_RESPONSE_INFO, responseInfo); - return responseInfo; - } - - private THResponseInfo resolveResponseInfo(TraceData traceData, WErrorDefinition definition) { - if (traceData == null) { - return fallbackResponseInfo(definition); + private void flushQuietly(HttpServletResponse response) { + if (response == null) { + return; } - var serviceSpan = traceData.getServiceSpan(); - if (serviceSpan == null) { - return fallbackResponseInfo(definition); + try { + response.flushBuffer(); + } catch (Exception exception) { + log.debug("Failed to flush response buffer", exception); } - serviceSpan.getMetadata().putValue(MetadataProperties.ERROR_DEFINITION, definition); - var responseInfo = THProviderErrorMapper.getResponseInfo(serviceSpan); - serviceSpan.getMetadata().putValue(THMetadataProperties.TH_RESPONSE_INFO, responseInfo); - return responseInfo; } private WErrorDefinition extractDefinition(ContextSpan serviceSpan, Throwable throwable) { @@ -183,15 +208,6 @@ private WErrorDefinition fallbackDefinition(Throwable throwable) { return definition; } - private void applyResponseInfo(HttpServletResponse response, THResponseInfo responseInfo) { - if (response == null || response.isCommitted() || responseInfo == null) { - return; - } - if (responseInfo.getStatus() > 0) { - response.setStatus(responseInfo.getStatus()); - } - } - private THResponseInfo fallbackResponseInfo(WErrorDefinition definition) { var status = definition != null && definition.getErrorType() == WErrorType.BUSINESS_ERROR ? HttpServletResponse.SC_OK @@ -258,7 +274,7 @@ private void applyHttpHeaders(HttpServletResponse response, HttpHeaders headers) }); } - private static void copyHeader(HttpHeaders source, String sourceName, String targetName, HttpHeaders target) { + private void copyHeader(HttpHeaders source, String sourceName, String targetName, HttpHeaders target) { if (sourceName == null || targetName == null) { return; } @@ -267,17 +283,4 @@ private static void copyHeader(HttpHeaders source, String sourceName, String tar target.put(targetName, new ArrayList<>(values)); } } - - private static void flushQuietly(HttpServletResponse response) { - if (response == null) { - return; - } - try { - response.flushBuffer(); - } catch (Exception exception) { - if (log.isDebugEnabled()) { - log.debug("Failed to flush response buffer", exception); - } - } - } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 12cfc81..08fbf7e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,7 +46,8 @@ info: woody-http-bridge: tracing: trace-restore: true - response-header-mode: X_WOODY + response-header-mode: OFF + propagate-errors: false endpoints: - path: /${wachter.endpoint} From 56c0fca90c57b985fb19fdecca61847697eaf1e5 Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Mon, 13 Oct 2025 17:19:40 +0700 Subject: [PATCH 04/14] refactor --- .../config/properties/TracingProperties.java | 13 +++++++++---- src/main/resources/application.yml | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java index e6f1cf7..f327fd2 100644 --- a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java +++ b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java @@ -14,10 +14,10 @@ @ConfigurationProperties(prefix = "woody-http-bridge.tracing") public class TracingProperties { - private boolean traceRestore = true; + private RequestHeaderMode requestHeaderMode = RequestHeaderMode.OFF; private ResponseHeaderMode responseHeaderMode = ResponseHeaderMode.OFF; - private List endpoints = new ArrayList<>(); private Boolean propagateErrors; + private List endpoints = new ArrayList<>(); @Getter @Setter @@ -35,10 +35,15 @@ public boolean shouldPropagateErrors() { return responseHeaderMode == ResponseHeaderMode.OFF; } + public enum RequestHeaderMode { + OFF, + WOODY_OR_X_WOODY + } + public enum ResponseHeaderMode { + OFF, WOODY, X_WOODY, - HTTP, - OFF + HTTP } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 08fbf7e..0127958 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -45,7 +45,7 @@ info: woody-http-bridge: tracing: - trace-restore: true + request-header-mode: WOODY_OR_X_WOODY response-header-mode: OFF propagate-errors: false endpoints: From a5e9e6601dba97800f5819efe2e06b99cb71381f Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Mon, 13 Oct 2025 17:21:46 +0700 Subject: [PATCH 05/14] refactor --- .../wachter/tracing/WoodyTracingFilter.java | 86 +++++++++++++------ 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java b/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java index d877704..81cb61f 100644 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java +++ b/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java @@ -30,25 +30,43 @@ public final class WoodyTracingFilter extends OncePerRequestFilter { HttpHeaders.SET_COOKIE.toLowerCase(Locale.ROOT) ); - private final int serverPort; - private final String wachterEndpoint; + private final int defaultServerPort; + private final String defaultEndpoint; private final TracingProperties tracingProperties; private final WoodyTraceLifecycleHandler woodyTraceLifecycleHandler; @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) { - var requestPath = getRequestPath(request); - if ((request.getLocalPort() == serverPort) && requestPath.equals(wachterEndpoint)) { - if (tracingProperties.isTraceRestore()) { - handleWithTraceRestore(request, response, filterChain); - } else { - handleLightweightRequest(request, response, filterChain); + @SneakyThrows + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { + if (matchesConfiguredEndpoint(request)) { + switch (tracingProperties.getRequestHeaderMode()) { + case OFF -> handle(request, response, filterChain); + case WOODY_OR_X_WOODY -> handleWithTraceRestore(request, response, filterChain); } return; } - doFilter(request, response, filterChain); + filterChain.doFilter(request, response); + } + + private boolean matchesConfiguredEndpoint(HttpServletRequest request) { + var port = request.getLocalPort(); + var path = getRequestPath(request); + var endpoints = tracingProperties.getEndpoints(); + if (endpoints == null || endpoints.isEmpty()) { + var matched = port == defaultServerPort && path.equals(defaultEndpoint); + log.debug("Tracing filter endpoint match (default) port={} path={} matched={}", port, path, + matched); + return matched; + } + endpoints.forEach(endpoint -> log.debug("Tracing filter endpoint candidate port={} path={} -> " + + "portMatch={} pathMatch={}", + endpoint.getPort(), endpoint.getPath(), matchesPort(endpoint.getPort(), port), + matchesPath(endpoint.getPath(), path))); + var matched = endpoints.stream().anyMatch(endpoint -> matchesPort(endpoint.getPort(), port) + && matchesPath(endpoint.getPath(), path)); + log.debug("Tracing filter endpoint match port={} path={} matched={} endpoints={}", port, path, matched, + endpoints); + return matched; } private void handleWithTraceRestore(HttpServletRequest request, @@ -58,25 +76,23 @@ private void handleWithTraceRestore(HttpServletRequest request, var headersForTrace = TraceContextHeadersValidation.validate(normalized); var restoredTraceData = TraceContextRestorer.restoreTraceData(headersForTrace); WFlow.create(() -> { - logReceived(request); - doFilterWithTraceHandling(request, response, filterChain); - logSent(request, response); - }, restoredTraceData).run(); + logReceived(request); + doFilterWithTraceHandling(request, response, filterChain); + logSent(request, response); + }, restoredTraceData) + .run(); } - private void handleLightweightRequest(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) { - logReceived(request); - new WFlow().createServiceFork(() -> doFilter(request, response, filterChain)).run(); - logSent(request, response); + private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { + new WFlow().createServiceFork(() -> { + logReceived(request); + doFilterWithTraceHandling(request, response, filterChain); + logSent(request, response); + }) + .run(); } @SneakyThrows - private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { - filterChain.doFilter(request, response); - } - private void doFilterWithTraceHandling(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { @@ -85,9 +101,17 @@ private void doFilterWithTraceHandling(HttpServletRequest request, woodyTraceLifecycleHandler.handleSuccess(response); } catch (WRuntimeException woodyError) { log.warn("Handled Woody exception during request processing", woodyError); + woodyTraceLifecycleHandler.recordOtelSpanException(woodyError); + if (tracingProperties.shouldPropagateErrors()) { + throw woodyError; + } woodyTraceLifecycleHandler.handleWoodyException(response, woodyError); } catch (Throwable unexpected) { log.error("Unhandled exception during request processing", unexpected); + woodyTraceLifecycleHandler.recordOtelSpanException(unexpected); + if (tracingProperties.shouldPropagateErrors()) { + throw unexpected; + } woodyTraceLifecycleHandler.handleUnexpectedError(response, unexpected); } } @@ -102,6 +126,16 @@ private void logSent(HttpServletRequest request, HttpServletResponse response) { response.getStatus(), sanitizeResponseHeaders(response)); } + private boolean matchesPort(Integer configuredPort, int actualPort) { + return configuredPort == null || configuredPort == actualPort; + } + + private boolean matchesPath(String configuredPath, String actualPath) { + if (configuredPath == null || configuredPath.isBlank()) { + return true; + } + return actualPath.equals(configuredPath); + } public static String extractParams(HttpServletRequest servletRequest) { return servletRequest.getParameterMap().entrySet().stream() From cf0e36d3f048bbfa9f0815bd5c5b7e527a57af9e Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Mon, 13 Oct 2025 19:43:13 +0700 Subject: [PATCH 06/14] refactor --- pom.xml | 1 - .../dev/vality/wachter/config/WebConfig.java | 60 +--------- .../config/properties/TracingProperties.java | 46 ++++++-- ...er.java => WoodyTraceResponseHandler.java} | 26 ++--- .../wachter/tracing/WoodyTracingFilter.java | 106 ++++++++---------- 5 files changed, 99 insertions(+), 140 deletions(-) rename src/main/java/dev/vality/wachter/tracing/{WoodyTraceLifecycleHandler.java => WoodyTraceResponseHandler.java} (93%) diff --git a/pom.xml b/pom.xml index 39edfed..efa83a1 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,6 @@ UTF-8 21 8022 - wachter 8023 ${server.port} ${management.port} diff --git a/src/main/java/dev/vality/wachter/config/WebConfig.java b/src/main/java/dev/vality/wachter/config/WebConfig.java index 074a5c5..61391cb 100644 --- a/src/main/java/dev/vality/wachter/config/WebConfig.java +++ b/src/main/java/dev/vality/wachter/config/WebConfig.java @@ -1,77 +1,25 @@ package dev.vality.wachter.config; import dev.vality.wachter.config.properties.TracingProperties; -import dev.vality.wachter.tracing.WoodyTraceLifecycleHandler; +import dev.vality.wachter.tracing.WoodyTraceResponseHandler; import dev.vality.wachter.tracing.WoodyTracingFilter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; @Configuration @Slf4j public class WebConfig { - @Value("${server.port}") - private int serverPort; - - @Value("/${wachter.endpoint}") - private String wachterEndpoint; - - @Bean - public FilterRegistrationBean externalPortRestrictingFilter() { - var filter = new OncePerRequestFilter() { - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - var requestPath = getRequestPath(request); - if ((request.getLocalPort() == serverPort) && !requestPath.equals(wachterEndpoint)) { - var status = HttpServletResponse.SC_NOT_FOUND; - log.warn("<- Sent [redirecting {}]: Unknown address {}", status, requestPath); - response.sendError(status, "Unknown address"); - return; - } - filterChain.doFilter(request, response); - } - }; - - var filterRegistrationBean = new FilterRegistrationBean(); - filterRegistrationBean.setFilter(filter); - filterRegistrationBean.setOrder(-100); - filterRegistrationBean.setName("externalPortRestrictingFilter"); - filterRegistrationBean.addUrlPatterns("/*"); - return filterRegistrationBean; - } - @Bean public FilterRegistrationBean woodyTracingFilter(TracingProperties tracingProperties) { - var lifecycleHandler = new WoodyTraceLifecycleHandler(); - var filter = new WoodyTracingFilter(serverPort, wachterEndpoint, tracingProperties, lifecycleHandler); + var woodyTraceResponseHandler = new WoodyTraceResponseHandler(); + var filter = new WoodyTracingFilter(tracingProperties, woodyTraceResponseHandler); var registrationBean = new FilterRegistrationBean<>(filter); registrationBean.setOrder(-50); registrationBean.setName("woodyTracingFilter"); - registrationBean.addUrlPatterns(wachterEndpoint); + registrationBean.addUrlPatterns("/*"); return registrationBean; } - - public static String getRequestPath(HttpServletRequest request) { - var servletPath = request.getServletPath(); - if (servletPath != null && !servletPath.isBlank()) { - return servletPath; - } - var requestPath = request.getRequestURI(); - if (requestPath != null && !requestPath.isBlank()) { - return requestPath; - } - return ""; - } } diff --git a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java index f327fd2..171cf84 100644 --- a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java +++ b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java @@ -1,5 +1,6 @@ package dev.vality.wachter.config.properties; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -7,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; @Getter @Setter @@ -14,27 +16,25 @@ @ConfigurationProperties(prefix = "woody-http-bridge.tracing") public class TracingProperties { - private RequestHeaderMode requestHeaderMode = RequestHeaderMode.OFF; - private ResponseHeaderMode responseHeaderMode = ResponseHeaderMode.OFF; - private Boolean propagateErrors; + private static final RequestHeaderMode DEFAULT_REQUEST_MODE = RequestHeaderMode.OFF; + private static final ResponseHeaderMode DEFAULT_RESPONSE_MODE = ResponseHeaderMode.OFF; + private List endpoints = new ArrayList<>(); @Getter @Setter public static class Endpoint { + @NotNull private Integer port; + @NotNull private String path; + private RequestHeaderMode requestHeaderMode; + private ResponseHeaderMode responseHeaderMode; + private Boolean propagateErrors; } - public boolean shouldPropagateErrors() { - if (propagateErrors != null) { - return propagateErrors; - } - return responseHeaderMode == ResponseHeaderMode.OFF; - } - public enum RequestHeaderMode { OFF, WOODY_OR_X_WOODY @@ -46,4 +46,30 @@ public enum ResponseHeaderMode { X_WOODY, HTTP } + + public TracePolicy resolvePolicy(int port, String path) { + return endpoints.stream() + .filter(endpoint -> matches(endpoint, port, path)) + .findFirst() + .map(endpoint -> buildPolicy(endpoint, port, path)) + .orElse(null); + } + + private boolean matches(Endpoint endpoint, int port, String path) { + var portMatches = port == endpoint.getPort(); + var pathMatches = path.startsWith(endpoint.getPath()); + return portMatches && pathMatches; + } + + private TracePolicy buildPolicy(Endpoint endpoint, int port, String path) { + var effectiveRequestMode = Optional.of(endpoint.getRequestHeaderMode()).orElse(DEFAULT_REQUEST_MODE); + var effectiveResponseMode = Optional.of(endpoint.getResponseHeaderMode()).orElse(DEFAULT_RESPONSE_MODE); + var effectivePropagate = Optional.ofNullable(endpoint.getPropagateErrors()) + .orElse(effectiveResponseMode == ResponseHeaderMode.OFF); + return new TracePolicy(port, path, effectiveRequestMode, effectiveResponseMode, effectivePropagate); + } + + public record TracePolicy(int port, String path, RequestHeaderMode requestHeaderMode, + ResponseHeaderMode responseHeaderMode, boolean propagateErrors) { + } } diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java b/src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java similarity index 93% rename from src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java rename to src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java index 369f443..98c870b 100644 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTraceLifecycleHandler.java +++ b/src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java @@ -27,36 +27,33 @@ import static dev.vality.wachter.constants.TraceHeadersConstants.*; @Slf4j -public final class WoodyTraceLifecycleHandler { +public final class WoodyTraceResponseHandler { private final THProviderErrorMapper errorMapper = new THProviderErrorMapper(); - private final ResponseHeaderMode responseHeaderMode; - public WoodyTraceLifecycleHandler(ResponseHeaderMode responseHeaderMode) { - this.responseHeaderMode = responseHeaderMode == null ? ResponseHeaderMode.WOODY : responseHeaderMode; - } - - void handleSuccess(HttpServletResponse response) { + void handleSuccess(HttpServletResponse response, ResponseHeaderMode responseHeaderMode) { var traceData = TraceContext.getCurrentTraceData(); recordOtelSpanStatus(traceData, response.getStatus()); - applyHeaders(response, traceData, null); + applyHeaders(response, traceData, null, responseHeaderMode); } - void handleWoodyException(HttpServletResponse response, Throwable throwable) { + void handleWoodyException(HttpServletResponse response, Throwable throwable, + ResponseHeaderMode responseHeaderMode) { var traceData = TraceContext.getCurrentTraceData(); var responseInfo = resolveResponseInfo(traceData, throwable); applyResponseInfo(response, responseInfo); recordOtelSpanException(traceData, response, throwable); - applyHeaders(response, traceData, responseInfo); + applyHeaders(response, traceData, responseInfo, responseHeaderMode); flushQuietly(response); } - void handleUnexpectedError(HttpServletResponse response, Throwable throwable) { + void handleUnexpectedError(HttpServletResponse response, Throwable throwable, + ResponseHeaderMode responseHeaderMode) { var traceData = TraceContext.getCurrentTraceData(); var responseInfo = resolveResponseInfo(traceData, fallbackDefinition(throwable)); applyResponseInfo(response, responseInfo); recordOtelSpanException(traceData, response, throwable); - applyHeaders(response, traceData, responseInfo); + applyHeaders(response, traceData, responseInfo, responseHeaderMode); flushQuietly(response); } @@ -130,7 +127,10 @@ private void recordOtelSpanException(TraceData traceData, HttpServletResponse re span.setStatus(StatusCode.ERROR); } - private void applyHeaders(HttpServletResponse response, TraceData traceData, THResponseInfo responseInfo) { + private void applyHeaders(HttpServletResponse response, + TraceData traceData, + THResponseInfo responseInfo, + ResponseHeaderMode responseHeaderMode) { if (response == null || response.isCommitted() || traceData == null) { return; } diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java b/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java index 81cb61f..2acc9bf 100644 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java +++ b/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java @@ -1,6 +1,7 @@ package dev.vality.wachter.tracing; import dev.vality.wachter.config.properties.TracingProperties; +import dev.vality.wachter.config.properties.TracingProperties.TracePolicy; import dev.vality.woody.api.flow.WFlow; import dev.vality.woody.api.flow.error.WRuntimeException; import jakarta.servlet.FilterChain; @@ -18,8 +19,6 @@ import java.util.Set; import java.util.stream.Collectors; -import static dev.vality.wachter.config.WebConfig.getRequestPath; - @Slf4j @RequiredArgsConstructor public final class WoodyTracingFilter extends OncePerRequestFilter { @@ -30,89 +29,75 @@ public final class WoodyTracingFilter extends OncePerRequestFilter { HttpHeaders.SET_COOKIE.toLowerCase(Locale.ROOT) ); - private final int defaultServerPort; - private final String defaultEndpoint; private final TracingProperties tracingProperties; - private final WoodyTraceLifecycleHandler woodyTraceLifecycleHandler; + private final WoodyTraceResponseHandler woodyTraceResponseHandler; @Override @SneakyThrows + @SuppressWarnings("NullableProblems") protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { - if (matchesConfiguredEndpoint(request)) { - switch (tracingProperties.getRequestHeaderMode()) { - case OFF -> handle(request, response, filterChain); - case WOODY_OR_X_WOODY -> handleWithTraceRestore(request, response, filterChain); - } + var path = getRequestPath(request); + var port = request.getLocalPort(); + var policy = tracingProperties.resolvePolicy(port, path); + if (policy == null) { + filterChain.doFilter(request, response); return; } - filterChain.doFilter(request, response); + switch (policy.requestHeaderMode()) { + case OFF -> handleWithoutTraceRestore(request, response, filterChain, policy); + case WOODY_OR_X_WOODY -> handleWithTraceRestore(request, response, filterChain, policy); + } } - private boolean matchesConfiguredEndpoint(HttpServletRequest request) { - var port = request.getLocalPort(); - var path = getRequestPath(request); - var endpoints = tracingProperties.getEndpoints(); - if (endpoints == null || endpoints.isEmpty()) { - var matched = port == defaultServerPort && path.equals(defaultEndpoint); - log.debug("Tracing filter endpoint match (default) port={} path={} matched={}", port, path, - matched); - return matched; - } - endpoints.forEach(endpoint -> log.debug("Tracing filter endpoint candidate port={} path={} -> " - + "portMatch={} pathMatch={}", - endpoint.getPort(), endpoint.getPath(), matchesPort(endpoint.getPort(), port), - matchesPath(endpoint.getPath(), path))); - var matched = endpoints.stream().anyMatch(endpoint -> matchesPort(endpoint.getPort(), port) - && matchesPath(endpoint.getPath(), path)); - log.debug("Tracing filter endpoint match port={} path={} matched={} endpoints={}", port, path, matched, - endpoints); - return matched; + private void handleWithoutTraceRestore(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain, + TracePolicy policy) { + new WFlow().createServiceFork(() -> { + logReceived(request); + doFilterWithTraceHandling(request, response, filterChain, policy); + logSent(request, response); + }) + .run(); } private void handleWithTraceRestore(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) { + FilterChain filterChain, + TracePolicy policy) { var normalized = TraceContextHeadersNormalizer.normalize(request); var headersForTrace = TraceContextHeadersValidation.validate(normalized); var restoredTraceData = TraceContextRestorer.restoreTraceData(headersForTrace); WFlow.create(() -> { logReceived(request); - doFilterWithTraceHandling(request, response, filterChain); + doFilterWithTraceHandling(request, response, filterChain, policy); logSent(request, response); }, restoredTraceData) .run(); } - private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { - new WFlow().createServiceFork(() -> { - logReceived(request); - doFilterWithTraceHandling(request, response, filterChain); - logSent(request, response); - }) - .run(); - } - @SneakyThrows private void doFilterWithTraceHandling(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) { + FilterChain filterChain, + TracePolicy policy) { try { filterChain.doFilter(request, response); - woodyTraceLifecycleHandler.handleSuccess(response); + woodyTraceResponseHandler.handleSuccess(response, policy.responseHeaderMode()); } catch (WRuntimeException woodyError) { log.warn("Handled Woody exception during request processing", woodyError); - woodyTraceLifecycleHandler.recordOtelSpanException(woodyError); - if (tracingProperties.shouldPropagateErrors()) { + if (policy.propagateErrors()) { + woodyTraceResponseHandler.recordOtelSpanException(woodyError); throw woodyError; } - woodyTraceLifecycleHandler.handleWoodyException(response, woodyError); + woodyTraceResponseHandler.handleWoodyException(response, woodyError, policy.responseHeaderMode()); } catch (Throwable unexpected) { log.error("Unhandled exception during request processing", unexpected); - woodyTraceLifecycleHandler.recordOtelSpanException(unexpected); - if (tracingProperties.shouldPropagateErrors()) { + if (policy.propagateErrors()) { + woodyTraceResponseHandler.recordOtelSpanException(unexpected); throw unexpected; } - woodyTraceLifecycleHandler.handleUnexpectedError(response, unexpected); + woodyTraceResponseHandler.handleUnexpectedError(response, unexpected, policy.responseHeaderMode()); } } @@ -126,17 +111,6 @@ private void logSent(HttpServletRequest request, HttpServletResponse response) { response.getStatus(), sanitizeResponseHeaders(response)); } - private boolean matchesPort(Integer configuredPort, int actualPort) { - return configuredPort == null || configuredPort == actualPort; - } - - private boolean matchesPath(String configuredPath, String actualPath) { - if (configuredPath == null || configuredPath.isBlank()) { - return true; - } - return actualPath.equals(configuredPath); - } - public static String extractParams(HttpServletRequest servletRequest) { return servletRequest.getParameterMap().entrySet().stream() .map(entry -> entry.getKey() + "=" + String.join(",", entry.getValue())) @@ -186,4 +160,16 @@ private static HttpHeaders sanitizeResponseHeaders(HttpServletResponse response) }); return headers; } + + private static String getRequestPath(HttpServletRequest request) { + var servletPath = request.getServletPath(); + if (servletPath != null && !servletPath.isBlank()) { + return servletPath; + } + var requestPath = request.getRequestURI(); + if (requestPath != null && !requestPath.isBlank()) { + return requestPath; + } + return ""; + } } From a85b80d5ba3f021cae189b430a91f30e51a0aee4 Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Mon, 13 Oct 2025 19:43:20 +0700 Subject: [PATCH 07/14] refactor --- src/main/resources/application.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0127958..6136103 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -45,14 +45,13 @@ info: woody-http-bridge: tracing: - request-header-mode: WOODY_OR_X_WOODY - response-header-mode: OFF - propagate-errors: false endpoints: - - path: /${wachter.endpoint} + - path: /wachter + port: ${server.port} + request-header-mode: WOODY_OR_X_WOODY + response-header-mode: OFF wachter: - endpoint: ${wachter.endpoint} auth: enabled: true serviceHeader: Service From 42bb5b06253beaf9bc3b2e7a133d93429d7d4b71 Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Mon, 13 Oct 2025 21:12:20 +0700 Subject: [PATCH 08/14] refactor --- .../config/properties/TracingProperties.java | 4 +- .../tracing/WoodyTraceResponseHandler.java | 28 +- .../wachter/tracing/WoodyTracingFilter.java | 19 +- ...bstractKeycloakOpenIdAsWiremockConfig.java | 7 +- .../tracing/WoodyTracingFilterTest.java | 294 +++++++++++++++++- 5 files changed, 319 insertions(+), 33 deletions(-) diff --git a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java index 171cf84..eb333e9 100644 --- a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java +++ b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java @@ -62,8 +62,8 @@ private boolean matches(Endpoint endpoint, int port, String path) { } private TracePolicy buildPolicy(Endpoint endpoint, int port, String path) { - var effectiveRequestMode = Optional.of(endpoint.getRequestHeaderMode()).orElse(DEFAULT_REQUEST_MODE); - var effectiveResponseMode = Optional.of(endpoint.getResponseHeaderMode()).orElse(DEFAULT_RESPONSE_MODE); + var effectiveRequestMode = Optional.ofNullable(endpoint.getRequestHeaderMode()).orElse(DEFAULT_REQUEST_MODE); + var effectiveResponseMode = Optional.ofNullable(endpoint.getResponseHeaderMode()).orElse(DEFAULT_RESPONSE_MODE); var effectivePropagate = Optional.ofNullable(endpoint.getPropagateErrors()) .orElse(effectiveResponseMode == ResponseHeaderMode.OFF); return new TracePolicy(port, path, effectiveRequestMode, effectiveResponseMode, effectivePropagate); diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java b/src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java index 98c870b..949d05c 100644 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java +++ b/src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java @@ -62,6 +62,19 @@ void recordOtelSpanException(Throwable throwable) { recordOtelSpanException(traceData, null, throwable); } + private void recordOtelSpanException(TraceData traceData, HttpServletResponse response, Throwable throwable) { + var span = extractSpan(traceData); + if (span == null || !span.getSpanContext().isValid()) { + return; + } + var status = response != null ? response.getStatus() : 0; + if (status > 0) { + span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, status); + } + span.recordException(throwable); + span.setStatus(StatusCode.ERROR); + } + private THResponseInfo resolveResponseInfo(TraceData traceData, Throwable throwable) { if (traceData == null) { return fallbackResponseInfo(fallbackDefinition(throwable)); @@ -114,19 +127,6 @@ private void recordOtelSpanStatus(TraceData traceData, int status) { } } - private void recordOtelSpanException(TraceData traceData, HttpServletResponse response, Throwable throwable) { - var span = extractSpan(traceData); - if (span == null || !span.getSpanContext().isValid()) { - return; - } - var status = response != null ? response.getStatus() : 0; - if (status > 0) { - span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, status); - } - span.recordException(throwable); - span.setStatus(StatusCode.ERROR); - } - private void applyHeaders(HttpServletResponse response, TraceData traceData, THResponseInfo responseInfo, @@ -167,6 +167,8 @@ private void applyHeaders(HttpServletResponse response, case WOODY -> applyWoodyHeaders(response, headers); case X_WOODY -> applyXWoodyHeaders(response, headers); case HTTP -> applyHttpHeaders(response, headers); + default -> { + } } } diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java b/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java index 2acc9bf..b14cb93 100644 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java +++ b/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java @@ -46,6 +46,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse switch (policy.requestHeaderMode()) { case OFF -> handleWithoutTraceRestore(request, response, filterChain, policy); case WOODY_OR_X_WOODY -> handleWithTraceRestore(request, response, filterChain, policy); + default -> filterChain.doFilter(request, response); } } @@ -54,11 +55,10 @@ private void handleWithoutTraceRestore(HttpServletRequest request, FilterChain filterChain, TracePolicy policy) { new WFlow().createServiceFork(() -> { - logReceived(request); - doFilterWithTraceHandling(request, response, filterChain, policy); - logSent(request, response); - }) - .run(); + logReceived(request); + doFilterWithTraceHandling(request, response, filterChain, policy); + logSent(request, response); + }).run(); } private void handleWithTraceRestore(HttpServletRequest request, @@ -69,11 +69,10 @@ private void handleWithTraceRestore(HttpServletRequest request, var headersForTrace = TraceContextHeadersValidation.validate(normalized); var restoredTraceData = TraceContextRestorer.restoreTraceData(headersForTrace); WFlow.create(() -> { - logReceived(request); - doFilterWithTraceHandling(request, response, filterChain, policy); - logSent(request, response); - }, restoredTraceData) - .run(); + logReceived(request); + doFilterWithTraceHandling(request, response, filterChain, policy); + logSent(request, response); + }, restoredTraceData).run(); } @SneakyThrows diff --git a/src/test/java/dev/vality/wachter/config/AbstractKeycloakOpenIdAsWiremockConfig.java b/src/test/java/dev/vality/wachter/config/AbstractKeycloakOpenIdAsWiremockConfig.java index 05ec03a..1e2970c 100644 --- a/src/test/java/dev/vality/wachter/config/AbstractKeycloakOpenIdAsWiremockConfig.java +++ b/src/test/java/dev/vality/wachter/config/AbstractKeycloakOpenIdAsWiremockConfig.java @@ -21,7 +21,12 @@ "server.port=8083", "spring.security.oauth2.resourceserver.url=${wiremock.server.baseUrl}", "spring.security.oauth2.resourceserver.jwt.issuer-uri=${wiremock.server.baseUrl}/auth/realms/" + - "${spring.security.oauth2.resourceserver.jwt.realm}"}) + "${spring.security.oauth2.resourceserver.jwt.realm}", + "woody-http-bridge.tracing.endpoints[0].path=/wachter", + "woody-http-bridge.tracing.endpoints[0].port=8083", + "woody-http-bridge.tracing.endpoints[0].request-header-mode: WOODY_OR_X_WOODY", + "woody-http-bridge.tracing.endpoints[0].response-header-mode: OFF", + }) @AutoConfigureMockMvc @EnableWireMock @ExtendWith(SpringExtension.class) diff --git a/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java b/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java index 0e55046..8eee21a 100644 --- a/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java +++ b/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java @@ -1,5 +1,13 @@ package dev.vality.wachter.tracing; +import dev.vality.wachter.config.properties.TracingProperties; +import dev.vality.wachter.config.properties.TracingProperties.Endpoint; +import dev.vality.wachter.config.properties.TracingProperties.RequestHeaderMode; +import dev.vality.wachter.config.properties.TracingProperties.ResponseHeaderMode; +import dev.vality.woody.api.flow.error.WErrorDefinition; +import dev.vality.woody.api.flow.error.WErrorSource; +import dev.vality.woody.api.flow.error.WErrorType; +import dev.vality.woody.api.flow.error.WRuntimeException; import dev.vality.woody.api.trace.context.TraceContext; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; @@ -14,14 +22,15 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import static dev.vality.wachter.constants.TraceHeadersConstants.ExternalHeaders.X_WOODY_SPAN_ID; -import static dev.vality.wachter.constants.TraceHeadersConstants.ExternalHeaders.X_WOODY_TRACE_ID; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static dev.vality.wachter.constants.TraceHeadersConstants.ExternalHeaders.*; +import static dev.vality.wachter.constants.TraceHeadersConstants.*; +import static org.junit.jupiter.api.Assertions.*; class WoodyTracingFilterTest { private SdkTracerProvider tracerProvider; private WoodyTracingFilter filter; + private TracingProperties tracingProperties; @BeforeEach void setUp() { @@ -32,7 +41,8 @@ void setUp() { .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .build(); GlobalOpenTelemetry.set(openTelemetry); - filter = new WoodyTracingFilter(8080, "/wachter"); + tracingProperties = new TracingProperties(); + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, null); } @AfterEach @@ -73,12 +83,282 @@ void shouldSetSpanStatusErrorForServerError() throws Exception { final var request = new MockHttpServletRequest("GET", "/wachter"); final var response = new MockHttpServletResponse(); request.setLocalPort(8080); - final FilterChain chain = (req, res) -> { - ((MockHttpServletResponse) res).setStatus(503); - }; + final FilterChain chain = (req, res) -> ((MockHttpServletResponse) res).setStatus(503); filter.doFilter(request, response, chain); assertEquals(503, response.getStatus()); } + + @Test + void shouldEchoWoodyHeadersOnSuccess() throws Exception { + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.WOODY, null); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + request.addHeader(X_WOODY_TRACE_ID, "woody-trace-id"); + request.addHeader(X_WOODY_SPAN_ID, "woody-span-id"); + + filter.doFilter(request, response, new MockFilterChain()); + + assertEquals("woody-trace-id", response.getHeader(WOODY_TRACE_ID)); + assertEquals("woody-span-id", response.getHeader(WOODY_SPAN_ID)); + assertNull(response.getHeader(X_WOODY_TRACE_ID)); + assertNull(response.getHeader(X_WOODY_SPAN_ID)); + } + + @Test + void shouldReturnXWoodyHeadersWhenConfigured() throws Exception { + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.X_WOODY, null); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + request.addHeader(X_WOODY_TRACE_ID, "x-woody-trace"); + request.addHeader(X_WOODY_SPAN_ID, "x-woody-span"); + + filter.doFilter(request, response, new MockFilterChain()); + + assertEquals("x-woody-trace", response.getHeader(X_WOODY_TRACE_ID)); + assertEquals("x-woody-span", response.getHeader(X_WOODY_SPAN_ID)); + assertNull(response.getHeader(WOODY_TRACE_ID)); + assertNull(response.getHeader(WOODY_SPAN_ID)); + } + + @Test + void shouldMapWoodyExceptionToErrorResponse() throws Exception { + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.WOODY, null); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + request.addHeader(X_WOODY_TRACE_ID, "trace-err"); + request.addHeader(X_WOODY_SPAN_ID, "span-err"); + + var errorDefinition = new WErrorDefinition(WErrorSource.INTERNAL); + errorDefinition.setErrorType(WErrorType.UNEXPECTED_ERROR); + errorDefinition.setErrorReason("boom"); + + filter.doFilter(request, response, (req, res) -> { + throw new WRuntimeException(errorDefinition); + }); + + assertEquals(500, response.getStatus()); + assertEquals(WErrorType.UNEXPECTED_ERROR.getKey(), response.getHeader(WOODY_ERROR_CLASS)); + assertEquals("boom", response.getHeader(WOODY_ERROR_REASON)); + assertNull(response.getHeader(X_WOODY_ERROR_CLASS)); + assertNull(response.getHeader(X_WOODY_ERROR_REASON)); + } + + @Test + void shouldFallbackToLightweightModeWhenDisabled() throws Exception { + configureFilter(RequestHeaderMode.OFF, ResponseHeaderMode.OFF, null); + + final var request = new MockHttpServletRequest("GET", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + + filter.doFilter(request, response, new MockFilterChain()); + + assertNull(response.getHeader(WOODY_TRACE_ID)); + } + + @Test + void shouldApplyCustomEndpointConfiguration() throws Exception { + tracingProperties.getEndpoints().clear(); + var endpoint = new Endpoint(); + endpoint.setPort(8080); + endpoint.setPath("/custom"); + endpoint.setRequestHeaderMode(RequestHeaderMode.WOODY_OR_X_WOODY); + endpoint.setResponseHeaderMode(ResponseHeaderMode.WOODY); + tracingProperties.getEndpoints().add(endpoint); + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.WOODY, null); + + final var request = new MockHttpServletRequest("POST", "/custom"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + request.addHeader(X_WOODY_TRACE_ID, "custom-trace"); + request.addHeader(X_WOODY_SPAN_ID, "custom-span"); + + filter.doFilter(request, response, new MockFilterChain()); + + assertEquals("custom-trace", response.getHeader(WOODY_TRACE_ID)); + } + + @Test + void shouldSkipWhenEndpointDoesNotMatchConfiguration() throws Exception { + tracingProperties.getEndpoints().clear(); + var endpoint = new Endpoint(); + endpoint.setPort(9090); + endpoint.setPath("/other"); + endpoint.setRequestHeaderMode(RequestHeaderMode.WOODY_OR_X_WOODY); + endpoint.setResponseHeaderMode(ResponseHeaderMode.OFF); + tracingProperties.getEndpoints().add(endpoint); + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, null); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + request.addHeader(X_WOODY_TRACE_ID, "trace-skip"); + request.addHeader(X_WOODY_SPAN_ID, "span-skip"); + + filter.doFilter(request, response, new MockFilterChain()); + + assertNull(response.getHeader(WOODY_TRACE_ID)); + assertNull(response.getHeader(X_WOODY_TRACE_ID)); + } + + @Test + void shouldRespectWoodyModeExplicitly() throws Exception { + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.WOODY, null); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + request.addHeader(X_WOODY_TRACE_ID, "woody-trace-only"); + request.addHeader(X_WOODY_SPAN_ID, "woody-span-only"); + + filter.doFilter(request, response, new MockFilterChain()); + + assertEquals("woody-trace-only", response.getHeader(WOODY_TRACE_ID)); + assertNull(response.getHeader(X_WOODY_TRACE_ID)); + } + + @Test + void shouldExposeHttpHeadersWhenConfigured() throws Exception { + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.HTTP, null); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + request.addHeader(OTEL_TRACE_PARENT, "00-11111111111111111111111111111111-2222222222222222-01"); + request.addHeader(X_REQUEST_ID, "request-123"); + request.addHeader(X_REQUEST_DEADLINE, "2025-10-14T06:00:00Z"); + + filter.doFilter(request, response, new MockFilterChain()); + + assertEquals("00-11111111111111111111111111111111-2222222222222222-01", + response.getHeader(OTEL_TRACE_PARENT)); + assertEquals("request-123", response.getHeader(X_REQUEST_ID)); + assertEquals("2025-10-14T06:00:00Z", response.getHeader(X_REQUEST_DEADLINE)); + assertNull(response.getHeader(WOODY_TRACE_ID)); + assertNull(response.getHeader(X_WOODY_TRACE_ID)); + } + + @Test + void shouldExposeHttpErrorsWhenConfigured() throws Exception { + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.HTTP, null); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + request.addHeader(X_WOODY_TRACE_ID, "trace-err"); + request.addHeader(X_WOODY_SPAN_ID, "span-err"); + + var errorDefinition = new WErrorDefinition(WErrorSource.INTERNAL); + errorDefinition.setErrorType(WErrorType.UNEXPECTED_ERROR); + errorDefinition.setErrorReason("http-boom"); + + filter.doFilter(request, response, (req, res) -> { + throw new WRuntimeException(errorDefinition); + }); + + assertEquals(500, response.getStatus()); + assertEquals(WErrorType.UNEXPECTED_ERROR.getKey(), response.getHeader(X_ERROR_CLASS)); + assertEquals("http-boom", response.getHeader(X_ERROR_REASON)); + assertNull(response.getHeader(WOODY_ERROR_CLASS)); + assertNull(response.getHeader(WOODY_ERROR_REASON)); + } + + @Test + void shouldReturnNoHeadersWhenOffMode() throws Exception { + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, null); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + request.addHeader(X_WOODY_TRACE_ID, "trace-off"); + request.addHeader(X_WOODY_SPAN_ID, "span-off"); + + filter.doFilter(request, response, new MockFilterChain()); + + assertNull(response.getHeader(WOODY_TRACE_ID)); + assertNull(response.getHeader(X_WOODY_TRACE_ID)); + assertNull(response.getHeader(OTEL_TRACE_PARENT)); + } + + @Test + void shouldPropagateErrorsWhenOffModeByDefault() { + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, null); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + + var errorDefinition = new WErrorDefinition(WErrorSource.INTERNAL); + errorDefinition.setErrorType(WErrorType.UNEXPECTED_ERROR); + errorDefinition.setErrorReason("propagate"); + + assertThrows(WRuntimeException.class, () -> filter.doFilter(request, response, (req, res) -> { + throw new WRuntimeException(errorDefinition); + })); + assertEquals(200, response.getStatus()); + } + + @Test + void shouldAllowDisablingErrorPropagationExplicitly() throws Exception { + configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, false); + + final var request = new MockHttpServletRequest("POST", "/wachter"); + final var response = new MockHttpServletResponse(); + request.setLocalPort(8080); + + var errorDefinition = new WErrorDefinition(WErrorSource.INTERNAL); + errorDefinition.setErrorType(WErrorType.UNEXPECTED_ERROR); + errorDefinition.setErrorReason("handled"); + + filter.doFilter(request, response, (req, res) -> { + throw new WRuntimeException(errorDefinition); + }); + + assertEquals(500, response.getStatus()); + assertNull(response.getHeader(WOODY_TRACE_ID)); + assertNull(response.getHeader(X_WOODY_TRACE_ID)); + } + + private void configureFilter(RequestHeaderMode requestHeaderMode, + ResponseHeaderMode responseHeaderMode, + Boolean propagateErrors) { + var defaultEndpoint = tracingProperties.getEndpoints().stream() + .filter(this::matchesDefault) + .findFirst() + .orElseGet(() -> { + var endpoint = defaultEndpoint(); + tracingProperties.getEndpoints().add(endpoint); + return endpoint; + }); + defaultEndpoint.setRequestHeaderMode(requestHeaderMode); + defaultEndpoint.setResponseHeaderMode(responseHeaderMode); + defaultEndpoint.setPropagateErrors(propagateErrors); + rebuildFilter(); + } + + private Endpoint defaultEndpoint() { + var endpoint = new Endpoint(); + endpoint.setPort(8080); + endpoint.setPath("/wachter"); + return endpoint; + } + + private boolean matchesDefault(Endpoint endpoint) { + var portMatches = endpoint.getPort() == null || endpoint.getPort() == 8080; + var pathMatches = endpoint.getPath() == null || endpoint.getPath().equals("/wachter"); + return portMatches && pathMatches; + } + + private void rebuildFilter() { + var lifecycleHandler = new WoodyTraceResponseHandler(); + filter = new WoodyTracingFilter(tracingProperties, lifecycleHandler); + } } From 28e305ab8fe32aca78edb870766cb80b925ed8dd Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Tue, 14 Oct 2025 13:29:54 +0700 Subject: [PATCH 09/14] refactor woody.http.bridge --- pom.xml | 11 +- .../wachter/client/ProxyHeadersExtractor.java | 2 +- .../vality/wachter/client/WachterClient.java | 8 +- .../wachter/client/WachterClientResponse.java | 7 - .../dev/vality/wachter/config/OtelConfig.java | 70 ----- .../dev/vality/wachter/config/WebConfig.java | 25 -- .../properties/OtelConfigProperties.java | 17 -- .../config/properties/TracingProperties.java | 75 ----- .../constants/RequestAttributeNames.java | 9 - .../constants/TraceHeadersConstants.java | 75 ----- .../security/JwtTokenDetailsExtractor.java | 55 ---- .../wachter/service/WachterService.java | 5 +- .../tracing/TraceContextHeadersExtractor.java | 60 ---- .../TraceContextHeadersNormalizer.java | 153 ---------- .../TraceContextHeadersValidation.java | 26 -- .../wachter/tracing/TraceContextRestorer.java | 77 ----- .../tracing/WoodyTraceResponseHandler.java | 288 ------------------ .../wachter/tracing/WoodyTracingFilter.java | 174 ----------- .../vality/wachter/utils/DeadlineUtil.java | 110 ------- 19 files changed, 16 insertions(+), 1231 deletions(-) delete mode 100644 src/main/java/dev/vality/wachter/client/WachterClientResponse.java delete mode 100644 src/main/java/dev/vality/wachter/config/OtelConfig.java delete mode 100644 src/main/java/dev/vality/wachter/config/WebConfig.java delete mode 100644 src/main/java/dev/vality/wachter/config/properties/OtelConfigProperties.java delete mode 100644 src/main/java/dev/vality/wachter/config/properties/TracingProperties.java delete mode 100644 src/main/java/dev/vality/wachter/constants/RequestAttributeNames.java delete mode 100644 src/main/java/dev/vality/wachter/constants/TraceHeadersConstants.java delete mode 100644 src/main/java/dev/vality/wachter/security/JwtTokenDetailsExtractor.java delete mode 100644 src/main/java/dev/vality/wachter/tracing/TraceContextHeadersExtractor.java delete mode 100644 src/main/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizer.java delete mode 100644 src/main/java/dev/vality/wachter/tracing/TraceContextHeadersValidation.java delete mode 100644 src/main/java/dev/vality/wachter/tracing/TraceContextRestorer.java delete mode 100644 src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java delete mode 100644 src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java delete mode 100644 src/main/java/dev/vality/wachter/utils/DeadlineUtil.java diff --git a/pom.xml b/pom.xml index efa83a1..209da08 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ 2.0.12 2.15.0 + 0.0.1 UTF-8 UTF-8 21 @@ -35,6 +36,11 @@ dev.vality.geck serializer + + dev.vality + woody-http-bridge + ${woody-http-bridge.version} + @@ -82,11 +88,6 @@ - - io.opentelemetry.instrumentation - opentelemetry-spring-boot-starter - ${otel.instrumentation.version} - jakarta.servlet jakarta.servlet-api diff --git a/src/main/java/dev/vality/wachter/client/ProxyHeadersExtractor.java b/src/main/java/dev/vality/wachter/client/ProxyHeadersExtractor.java index dce2b53..5135111 100644 --- a/src/main/java/dev/vality/wachter/client/ProxyHeadersExtractor.java +++ b/src/main/java/dev/vality/wachter/client/ProxyHeadersExtractor.java @@ -1,6 +1,6 @@ package dev.vality.wachter.client; -import dev.vality.wachter.constants.TraceHeadersConstants; +import dev.vality.woody.http.bridge.tracing.TraceHeadersConstants; import jakarta.servlet.http.HttpServletRequest; import lombok.experimental.UtilityClass; import org.springframework.http.HttpHeaders; diff --git a/src/main/java/dev/vality/wachter/client/WachterClient.java b/src/main/java/dev/vality/wachter/client/WachterClient.java index b2764a1..2aa1499 100644 --- a/src/main/java/dev/vality/wachter/client/WachterClient.java +++ b/src/main/java/dev/vality/wachter/client/WachterClient.java @@ -1,12 +1,13 @@ package dev.vality.wachter.client; -import dev.vality.wachter.tracing.TraceContextHeadersExtractor; -import dev.vality.wachter.tracing.TraceContextHeadersNormalizer; +import dev.vality.woody.http.bridge.tracing.TraceContextHeadersExtractor; +import dev.vality.woody.http.bridge.tracing.TraceContextHeadersNormalizer; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import org.springframework.web.client.RestClient; @@ -59,4 +60,7 @@ private HttpMethod resolveMethod(HttpServletRequest servletRequest) { return HttpMethod.POST; } } + + public record WachterClientResponse(HttpStatusCode statusCode, HttpHeaders headers, byte[] body) { + } } diff --git a/src/main/java/dev/vality/wachter/client/WachterClientResponse.java b/src/main/java/dev/vality/wachter/client/WachterClientResponse.java deleted file mode 100644 index c5a6f26..0000000 --- a/src/main/java/dev/vality/wachter/client/WachterClientResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.vality.wachter.client; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatusCode; - -public record WachterClientResponse(HttpStatusCode statusCode, HttpHeaders headers, byte[] body) { -} diff --git a/src/main/java/dev/vality/wachter/config/OtelConfig.java b/src/main/java/dev/vality/wachter/config/OtelConfig.java deleted file mode 100644 index ee9dd89..0000000 --- a/src/main/java/dev/vality/wachter/config/OtelConfig.java +++ /dev/null @@ -1,70 +0,0 @@ -package dev.vality.wachter.config; - -import dev.vality.wachter.config.properties.OtelConfigProperties; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; -import io.opentelemetry.sdk.trace.samplers.Sampler; -import io.opentelemetry.semconv.ServiceAttributes; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.time.Duration; - -@Slf4j -@Configuration -@ConditionalOnProperty(value = "otel.enabled", havingValue = "true", matchIfMissing = true) -@RequiredArgsConstructor -public class OtelConfig { - - private final OtelConfigProperties otelConfigProperties; - - @Value("${spring.application.name}") - private String applicationName; - - @Bean - public OpenTelemetry openTelemetryConfig() { - var resource = Resource.getDefault() - .merge(Resource.create(Attributes.of(ServiceAttributes.SERVICE_NAME, applicationName))); - var sdkTracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(BatchSpanProcessor.builder(OtlpHttpSpanExporter.builder() - .setEndpoint(otelConfigProperties.getResource()) - .setTimeout(Duration.ofMillis(otelConfigProperties.getTimeout())) - .build()) - .build()) - .setSampler(Sampler.alwaysOn()) - .setResource(resource) - .build(); - var openTelemetrySdk = OpenTelemetrySdk.builder() - .setTracerProvider(sdkTracerProvider) - .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) - .build(); - registerGlobalOpenTelemetry(openTelemetrySdk); - return openTelemetrySdk; - } - - private static void registerGlobalOpenTelemetry(OpenTelemetry openTelemetry) { - try { - GlobalOpenTelemetry.set(openTelemetry); - } catch (Throwable ex) { - log.warn("Please initialize the ObservabilitySdk before starting the application", ex); - GlobalOpenTelemetry.resetForTest(); - try { - GlobalOpenTelemetry.set(openTelemetry); - } catch (Throwable ex1) { - log.warn("Unable to set GlobalOpenTelemetry", ex1); - } - } - } -} diff --git a/src/main/java/dev/vality/wachter/config/WebConfig.java b/src/main/java/dev/vality/wachter/config/WebConfig.java deleted file mode 100644 index 61391cb..0000000 --- a/src/main/java/dev/vality/wachter/config/WebConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package dev.vality.wachter.config; - -import dev.vality.wachter.config.properties.TracingProperties; -import dev.vality.wachter.tracing.WoodyTraceResponseHandler; -import dev.vality.wachter.tracing.WoodyTracingFilter; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@Slf4j -public class WebConfig { - - @Bean - public FilterRegistrationBean woodyTracingFilter(TracingProperties tracingProperties) { - var woodyTraceResponseHandler = new WoodyTraceResponseHandler(); - var filter = new WoodyTracingFilter(tracingProperties, woodyTraceResponseHandler); - var registrationBean = new FilterRegistrationBean<>(filter); - registrationBean.setOrder(-50); - registrationBean.setName("woodyTracingFilter"); - registrationBean.addUrlPatterns("/*"); - return registrationBean; - } -} diff --git a/src/main/java/dev/vality/wachter/config/properties/OtelConfigProperties.java b/src/main/java/dev/vality/wachter/config/properties/OtelConfigProperties.java deleted file mode 100644 index 407339b..0000000 --- a/src/main/java/dev/vality/wachter/config/properties/OtelConfigProperties.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.vality.wachter.config.properties; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Getter -@Setter -@Component -@ConfigurationProperties(prefix = "otel") -public class OtelConfigProperties { - - private String resource; - private Long timeout; - -} diff --git a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java b/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java deleted file mode 100644 index eb333e9..0000000 --- a/src/main/java/dev/vality/wachter/config/properties/TracingProperties.java +++ /dev/null @@ -1,75 +0,0 @@ -package dev.vality.wachter.config.properties; - -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Getter -@Setter -@Component -@ConfigurationProperties(prefix = "woody-http-bridge.tracing") -public class TracingProperties { - - private static final RequestHeaderMode DEFAULT_REQUEST_MODE = RequestHeaderMode.OFF; - private static final ResponseHeaderMode DEFAULT_RESPONSE_MODE = ResponseHeaderMode.OFF; - - private List endpoints = new ArrayList<>(); - - @Getter - @Setter - public static class Endpoint { - - @NotNull - private Integer port; - @NotNull - private String path; - private RequestHeaderMode requestHeaderMode; - private ResponseHeaderMode responseHeaderMode; - private Boolean propagateErrors; - - } - - public enum RequestHeaderMode { - OFF, - WOODY_OR_X_WOODY - } - - public enum ResponseHeaderMode { - OFF, - WOODY, - X_WOODY, - HTTP - } - - public TracePolicy resolvePolicy(int port, String path) { - return endpoints.stream() - .filter(endpoint -> matches(endpoint, port, path)) - .findFirst() - .map(endpoint -> buildPolicy(endpoint, port, path)) - .orElse(null); - } - - private boolean matches(Endpoint endpoint, int port, String path) { - var portMatches = port == endpoint.getPort(); - var pathMatches = path.startsWith(endpoint.getPath()); - return portMatches && pathMatches; - } - - private TracePolicy buildPolicy(Endpoint endpoint, int port, String path) { - var effectiveRequestMode = Optional.ofNullable(endpoint.getRequestHeaderMode()).orElse(DEFAULT_REQUEST_MODE); - var effectiveResponseMode = Optional.ofNullable(endpoint.getResponseHeaderMode()).orElse(DEFAULT_RESPONSE_MODE); - var effectivePropagate = Optional.ofNullable(endpoint.getPropagateErrors()) - .orElse(effectiveResponseMode == ResponseHeaderMode.OFF); - return new TracePolicy(port, path, effectiveRequestMode, effectiveResponseMode, effectivePropagate); - } - - public record TracePolicy(int port, String path, RequestHeaderMode requestHeaderMode, - ResponseHeaderMode responseHeaderMode, boolean propagateErrors) { - } -} diff --git a/src/main/java/dev/vality/wachter/constants/RequestAttributeNames.java b/src/main/java/dev/vality/wachter/constants/RequestAttributeNames.java deleted file mode 100644 index a8a58eb..0000000 --- a/src/main/java/dev/vality/wachter/constants/RequestAttributeNames.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.vality.wachter.constants; - -public final class RequestAttributeNames { - - public static final String NORMALIZED_WOODY_HEADERS = "wachter.normalizedWoodyHeaders"; - - private RequestAttributeNames() { - } -} diff --git a/src/main/java/dev/vality/wachter/constants/TraceHeadersConstants.java b/src/main/java/dev/vality/wachter/constants/TraceHeadersConstants.java deleted file mode 100644 index e1fd21f..0000000 --- a/src/main/java/dev/vality/wachter/constants/TraceHeadersConstants.java +++ /dev/null @@ -1,75 +0,0 @@ -package dev.vality.wachter.constants; - -import dev.vality.woody.api.trace.context.metadata.user.UserIdentityEmailExtensionKit; -import dev.vality.woody.api.trace.context.metadata.user.UserIdentityIdExtensionKit; -import dev.vality.woody.api.trace.context.metadata.user.UserIdentityRealmExtensionKit; -import dev.vality.woody.api.trace.context.metadata.user.UserIdentityUsernameExtensionKit; -import dev.vality.woody.thrift.impl.http.transport.THttpHeader; - -public class TraceHeadersConstants { - - public static final String WOODY_PREFIX = "woody."; - public static final String WOODY_TRACE_ID = THttpHeader.TRACE_ID.getKey(); - public static final String WOODY_SPAN_ID = THttpHeader.SPAN_ID.getKey(); - public static final String WOODY_PARENT_ID = THttpHeader.PARENT_ID.getKey(); - public static final String WOODY_DEADLINE = THttpHeader.DEADLINE.getKey(); - public static final String WOODY_ERROR_CLASS = THttpHeader.ERROR_CLASS.getKey(); - public static final String WOODY_ERROR_REASON = THttpHeader.ERROR_REASON.getKey(); - public static final String WOODY_META_PREFIX = THttpHeader.META.getKey(); - public static final String WOODY_META_ID = WOODY_META_PREFIX + WoodyMetaHeaders.ID; - public static final String WOODY_META_USERNAME = WOODY_META_PREFIX + WoodyMetaHeaders.USERNAME; - public static final String WOODY_META_EMAIL = WOODY_META_PREFIX + WoodyMetaHeaders.EMAIL; - public static final String WOODY_META_REALM = WOODY_META_PREFIX + WoodyMetaHeaders.REALM; - public static final String WOODY_META_REQUEST_ID = WOODY_META_PREFIX + WoodyMetaHeaders.X_REQUEST_ID; - public static final String WOODY_META_REQUEST_DEADLINE = - WOODY_META_PREFIX + WoodyMetaHeaders.X_REQUEST_DEADLINE; - public static final String WOODY_META_REQUEST_INVOICE_ID = - WOODY_META_PREFIX + WoodyMetaHeaders.X_INVOICE_ID; - - public static final String OTEL_TRACE_PARENT = THttpHeader.TRACE_PARENT.getKey(); - public static final String OTEL_TRACE_STATE = THttpHeader.TRACE_STATE.getKey(); - - public static final class ExternalHeaders { - - public static final String X_REQUEST_ID = "X-Request-ID"; - public static final String X_REQUEST_DEADLINE = "X-Request-Deadline"; - public static final String X_INVOICE_ID = "X-Invoice-ID"; - public static final String X_WOODY_PREFIX = "x-woody-"; - public static final String X_WOODY_TRACE_ID = X_WOODY_PREFIX + "trace-id"; - public static final String X_WOODY_SPAN_ID = X_WOODY_PREFIX + "span-id"; - public static final String X_WOODY_PARENT_ID = X_WOODY_PREFIX + "parent-id"; - public static final String X_WOODY_DEADLINE = X_WOODY_PREFIX + "deadline"; - public static final String X_WOODY_ERROR_CLASS = X_WOODY_PREFIX + "error-class"; - public static final String X_WOODY_ERROR_REASON = X_WOODY_PREFIX + "error-reason"; - public static final String X_WOODY_META_PREFIX = X_WOODY_PREFIX + "meta-"; - public static final String X_WOODY_META_ID = X_WOODY_META_PREFIX + XWoodyMetaHeaders.ID; - public static final String X_WOODY_META_USERNAME = X_WOODY_META_PREFIX + XWoodyMetaHeaders.USERNAME; - public static final String X_WOODY_META_EMAIL = X_WOODY_META_PREFIX + XWoodyMetaHeaders.EMAIL; - public static final String X_WOODY_META_REALM = X_WOODY_META_PREFIX + XWoodyMetaHeaders.REALM; - public static final String X_ERROR_CLASS = "X-Error-Class"; - public static final String X_ERROR_REASON = "X-Error-Reason"; - - public static final class XWoodyMetaHeaders { - - public static final String USER_IDENTITY_PREFIX = "user-identity-"; - public static final String ID = USER_IDENTITY_PREFIX + "id"; - public static final String USERNAME = USER_IDENTITY_PREFIX + "username"; - public static final String EMAIL = USER_IDENTITY_PREFIX + "email"; - public static final String REALM = USER_IDENTITY_PREFIX + "realm"; - - } - } - - public static final class WoodyMetaHeaders { - - public static final String USER_IDENTITY_PREFIX = "user-identity."; - public static final String ID = UserIdentityIdExtensionKit.KEY; - public static final String USERNAME = UserIdentityUsernameExtensionKit.KEY; - public static final String EMAIL = UserIdentityEmailExtensionKit.KEY; - public static final String REALM = UserIdentityRealmExtensionKit.KEY; - public static final String X_REQUEST_ID = USER_IDENTITY_PREFIX + ExternalHeaders.X_REQUEST_ID; - public static final String X_REQUEST_DEADLINE = USER_IDENTITY_PREFIX + ExternalHeaders.X_REQUEST_DEADLINE; - public static final String X_INVOICE_ID = USER_IDENTITY_PREFIX + ExternalHeaders.X_INVOICE_ID; - - } -} diff --git a/src/main/java/dev/vality/wachter/security/JwtTokenDetailsExtractor.java b/src/main/java/dev/vality/wachter/security/JwtTokenDetailsExtractor.java deleted file mode 100644 index 78510d7..0000000 --- a/src/main/java/dev/vality/wachter/security/JwtTokenDetailsExtractor.java +++ /dev/null @@ -1,55 +0,0 @@ -package dev.vality.wachter.security; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; - -import java.util.List; -import java.util.Optional; - -public final class JwtTokenDetailsExtractor { - - private static final String PREFERRED_USERNAME = "preferred_username"; - private static final String EMAIL = "email"; - private static final String ISSUER = "iss"; - - private JwtTokenDetailsExtractor() { - } - - public static Optional extractFromContext(Authentication authentication) { - if (!(authentication instanceof JwtAuthenticationToken jwtAuthentication)) { - return Optional.empty(); - } - var token = jwtAuthentication.getToken(); - return Optional.of(new JwtTokenDetails( - token.getClaimAsString(JwtClaimNames.SUB), - token.getClaimAsString(PREFERRED_USERNAME), - token.getClaimAsString(EMAIL), - extractRealm(token), - jwtAuthentication.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .toList() - )); - } - - private static String extractRealm(Jwt token) { - var issuer = token.getClaimAsString(ISSUER); - if (issuer == null || issuer.isBlank()) { - return null; - } - var lastSlash = issuer.lastIndexOf('/'); - if (lastSlash < 0) { - return issuer; - } - return issuer.substring(lastSlash); - } - - public record JwtTokenDetails(String subject, - String preferredUsername, - String email, - String realm, - List roles) { - } -} diff --git a/src/main/java/dev/vality/wachter/service/WachterService.java b/src/main/java/dev/vality/wachter/service/WachterService.java index 08d78b3..af0d40e 100644 --- a/src/main/java/dev/vality/wachter/service/WachterService.java +++ b/src/main/java/dev/vality/wachter/service/WachterService.java @@ -1,11 +1,10 @@ package dev.vality.wachter.service; import dev.vality.wachter.client.WachterClient; -import dev.vality.wachter.client.WachterClientResponse; import dev.vality.wachter.mapper.ServiceMapper; import dev.vality.wachter.security.AccessData; import dev.vality.wachter.security.AccessService; -import dev.vality.wachter.security.JwtTokenDetailsExtractor; +import dev.vality.woody.http.bridge.util.JwtTokenDetailsExtractor; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; @@ -15,6 +14,8 @@ import java.io.ByteArrayOutputStream; +import static dev.vality.wachter.client.WachterClient.WachterClientResponse; + @RequiredArgsConstructor @Service public class WachterService { diff --git a/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersExtractor.java b/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersExtractor.java deleted file mode 100644 index 171cbc8..0000000 --- a/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersExtractor.java +++ /dev/null @@ -1,60 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.woody.api.trace.context.TraceContext; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.context.propagation.TextMapSetter; -import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; - -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -import static dev.vality.wachter.constants.TraceHeadersConstants.*; - -@Slf4j -@UtilityClass -public class TraceContextHeadersExtractor { - - public Map extractHeaders() { - var traceData = Objects.requireNonNull(TraceContext.getCurrentTraceData(), - "TraceData should be present in TraceContext"); - var otelSpan = Objects.requireNonNull(traceData.getOtelSpan(), - "OTel span should be attached to TraceData"); - if (!otelSpan.getSpanContext().isValid()) { - throw new IllegalStateException("SpanContext must be valid"); - } - - var span = traceData.getActiveSpan().getSpan(); - var headers = new HashMap(); - putIfNotNull(headers, WOODY_TRACE_ID, span.getTraceId()); - putIfNotNull(headers, WOODY_SPAN_ID, span.getId()); - putIfNotNull(headers, WOODY_PARENT_ID, span.getParentId()); - putIfNotNull(headers, WOODY_DEADLINE, - Optional.ofNullable(span.getDeadline()).map(Instant::toString).orElse(null)); - var customMetadata = traceData.getActiveSpan().getCustomMetadata(); - customMetadata.getKeys() - .forEach(s -> putIfNotNull(headers, WOODY_META_PREFIX + s, customMetadata.getValue(s))); - GlobalOpenTelemetry.getPropagators() - .getTextMapPropagator() - .inject(traceData.getOtelContext(), headers, MAP_SETTER); - log.debug("Extracted trace headers: {}", headers); - return headers; - } - - private void putIfNotNull(Map headers, - String key, - String value) { - if (value != null && !value.isEmpty()) { - headers.put(key, value); - } - } - - private static final TextMapSetter> MAP_SETTER = (carrier, key, value) -> { - if (carrier != null && key != null && value != null && !value.isEmpty()) { - carrier.put(key, value); - } - }; -} diff --git a/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizer.java b/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizer.java deleted file mode 100644 index 8b2ba3e..0000000 --- a/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizer.java +++ /dev/null @@ -1,153 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.wachter.security.JwtTokenDetailsExtractor; -import jakarta.servlet.http.HttpServletRequest; -import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.*; - -import static dev.vality.wachter.constants.TraceHeadersConstants.*; -import static dev.vality.wachter.utils.DeadlineUtil.*; - -@Slf4j -@UtilityClass -public class TraceContextHeadersNormalizer { - - public Map normalize(HttpServletRequest request) { - var normalized = new HashMap(); - normalizeWoodyHeaders(request, normalized); - normalizeOtelHeaders(request, normalized); - mergeJwtIntoHeaders(normalized); - mergeRequestDeadline(request, normalized); - return normalized.isEmpty() ? Map.of() : Map.copyOf(normalized); - } - - public HttpHeaders normalizeResponseHeaders(HttpHeaders responseHeaders) { - var normalized = new HttpHeaders(); - for (var entry : responseHeaders.entrySet()) { - var headerName = entry.getKey(); - var lowerCase = headerName.toLowerCase(Locale.ROOT); - if (lowerCase.startsWith(WOODY_PREFIX)) { - normalizeWoodyResponseHeader(normalized, lowerCase, entry.getValue()); - } else if (lowerCase.equals(OTEL_TRACE_STATE.toLowerCase(Locale.ROOT)) - || lowerCase.equals(OTEL_TRACE_PARENT.toLowerCase(Locale.ROOT))) { - normalized.addAll(headerName, entry.getValue()); - } - } - return normalized; - } - - private void normalizeWoodyHeaders(HttpServletRequest request, Map headers) { - (request.getHeaderNames() != null ? Collections.list(request.getHeaderNames()) : new ArrayList()) - .stream() - .map(s -> s.toLowerCase(Locale.ROOT)) - .filter(s -> s.startsWith(WOODY_PREFIX) || s.startsWith(ExternalHeaders.X_WOODY_PREFIX)) - .forEach(s -> { - if (s.startsWith(ExternalHeaders.X_WOODY_META_PREFIX)) { - var metaKey = s.substring(ExternalHeaders.X_WOODY_META_PREFIX.length()); - if (metaKey.startsWith(ExternalHeaders.XWoodyMetaHeaders.USER_IDENTITY_PREFIX)) { - var userIdentityKey = - metaKey.substring(ExternalHeaders.XWoodyMetaHeaders.USER_IDENTITY_PREFIX.length()); - putIfNotNull(headers, - WOODY_META_PREFIX + WoodyMetaHeaders.USER_IDENTITY_PREFIX + userIdentityKey, - request.getHeader(s)); - } else { - putIfNotNull(headers, WOODY_META_PREFIX + metaKey, request.getHeader(s)); - } - } else if (s.startsWith(ExternalHeaders.X_WOODY_PREFIX)) { - putIfNotNull(headers, WOODY_PREFIX + s.substring(ExternalHeaders.X_WOODY_PREFIX.length()), - request.getHeader(s)); - } else if (s.startsWith(WOODY_PREFIX)) { - putIfNotNull(headers, s, request.getHeader(s)); - } - }); - putIfNotNull(headers, WOODY_META_REQUEST_ID, request.getHeader(ExternalHeaders.X_REQUEST_ID)); - putIfNotNull(headers, WOODY_META_REQUEST_DEADLINE, request.getHeader(ExternalHeaders.X_REQUEST_DEADLINE)); - putIfNotNull(headers, WOODY_META_REQUEST_INVOICE_ID, request.getHeader(ExternalHeaders.X_INVOICE_ID)); - } - - private void normalizeWoodyResponseHeader(HttpHeaders headers, - String lowerCase, - List values) { - if (lowerCase.startsWith(WOODY_META_PREFIX)) { - var metaKey = lowerCase.substring(WOODY_META_PREFIX.length()); - if (metaKey.startsWith(WoodyMetaHeaders.USER_IDENTITY_PREFIX)) { - if (metaKey.equals(WoodyMetaHeaders.X_REQUEST_ID.toLowerCase(Locale.ROOT))) { - headers.addAll(ExternalHeaders.X_REQUEST_ID, values); - } else if (metaKey.equals(WoodyMetaHeaders.X_REQUEST_DEADLINE.toLowerCase(Locale.ROOT))) { - headers.addAll(ExternalHeaders.X_REQUEST_DEADLINE, values); - } else if (metaKey.equals(WoodyMetaHeaders.X_INVOICE_ID.toLowerCase(Locale.ROOT))) { - headers.addAll(ExternalHeaders.X_INVOICE_ID, values); - } else { - var userIdentityKey = metaKey.substring(WoodyMetaHeaders.USER_IDENTITY_PREFIX.length()); - headers.addAll( - ExternalHeaders.X_WOODY_META_PREFIX + - ExternalHeaders.XWoodyMetaHeaders.USER_IDENTITY_PREFIX + - userIdentityKey, values); - } - } else { - headers.addAll(ExternalHeaders.X_WOODY_META_PREFIX + metaKey, values); - } - } else { - headers.addAll(ExternalHeaders.X_WOODY_PREFIX + lowerCase.substring(WOODY_PREFIX.length()), values); - } - } - - private void normalizeOtelHeaders(HttpServletRequest request, Map headers) { - putIfNotNull(headers, OTEL_TRACE_PARENT, request.getHeader(OTEL_TRACE_PARENT)); - putIfNotNull(headers, OTEL_TRACE_STATE, request.getHeader(OTEL_TRACE_STATE)); - } - - private void mergeJwtIntoHeaders(Map headers) { - var tokenDetails = JwtTokenDetailsExtractor.extractFromContext(SecurityContextHolder - .getContext() - .getAuthentication()); - if (tokenDetails.isEmpty()) { - return; - } - var details = tokenDetails.get(); - putIfNotNull(headers, WOODY_META_ID, details.subject()); - putIfNotNull(headers, WOODY_META_USERNAME, details.preferredUsername()); - putIfNotNull(headers, WOODY_META_EMAIL, details.email()); - putIfNotNull(headers, WOODY_META_REALM, details.realm()); - } - - private void mergeRequestDeadline(HttpServletRequest request, Map headers) { - var requestDeadlineHeader = request.getHeader(ExternalHeaders.X_REQUEST_DEADLINE); - var requestIdHeader = request.getHeader(ExternalHeaders.X_REQUEST_ID); - if (requestDeadlineHeader == null) { - return; - } - try { - var normalizedDeadline = getInstant(requestDeadlineHeader, requestIdHeader).toString(); - headers.putIfAbsent(WOODY_DEADLINE, normalizedDeadline); - headers.put(WOODY_META_REQUEST_DEADLINE, normalizedDeadline); - } catch (Exception e) { - log.warn("Unable to parse '" + ExternalHeaders.X_REQUEST_DEADLINE + "' header value '{}'", - requestDeadlineHeader); - } - } - - private void putIfNotNull(Map headers, - String key, - String value) { - if (value != null && !value.isEmpty()) { - headers.put(key, value); - } - } - - private Instant getInstant(String requestDeadlineHeader, String requestIdHeader) { - if (containsRelativeValues(requestDeadlineHeader, requestIdHeader)) { - return Instant.now() - .plus(extractMilliseconds(requestDeadlineHeader, requestIdHeader), ChronoUnit.MILLIS) - .plus(extractSeconds(requestDeadlineHeader, requestIdHeader), ChronoUnit.MILLIS) - .plus(extractMinutes(requestDeadlineHeader, requestIdHeader), ChronoUnit.MILLIS); - } - return Instant.parse(requestDeadlineHeader); - } -} diff --git a/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersValidation.java b/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersValidation.java deleted file mode 100644 index 1c98bba..0000000 --- a/src/main/java/dev/vality/wachter/tracing/TraceContextHeadersValidation.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.vality.wachter.tracing; - -import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; - -import java.util.LinkedHashMap; -import java.util.Map; - -import static dev.vality.wachter.constants.TraceHeadersConstants.*; - -@Slf4j -@UtilityClass -public class TraceContextHeadersValidation { - - public LinkedHashMap validate(Map normalized) { - var copy = new LinkedHashMap<>(normalized); - var traceId = copy.get(WOODY_TRACE_ID); - if (traceId != null && traceId.equals(copy.get(WOODY_SPAN_ID))) { - copy.remove(WOODY_SPAN_ID); - } - if ("undefined".equals(copy.get(WOODY_PARENT_ID))) { - copy.remove(WOODY_PARENT_ID); - } - return copy; - } -} diff --git a/src/main/java/dev/vality/wachter/tracing/TraceContextRestorer.java b/src/main/java/dev/vality/wachter/tracing/TraceContextRestorer.java deleted file mode 100644 index 2a760c9..0000000 --- a/src/main/java/dev/vality/wachter/tracing/TraceContextRestorer.java +++ /dev/null @@ -1,77 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.woody.api.flow.WFlow; -import dev.vality.woody.api.trace.TraceData; -import dev.vality.woody.api.trace.context.TraceContext; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.propagation.TextMapGetter; -import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; - -import java.time.Instant; -import java.util.Map; -import java.util.function.Consumer; - -import static dev.vality.wachter.constants.TraceHeadersConstants.*; - -@Slf4j -@UtilityClass -public class TraceContextRestorer { - - public TraceData restoreTraceData(Map headers) { - log.debug("Restoring trace data from headers: {}", headers); - var traceData = TraceContext.initNewServiceTrace(new TraceData(), - WFlow.createDefaultIdGenerator(), WFlow.createDefaultIdGenerator()); - if (headers.isEmpty()) { - return traceData; - } - var span = traceData.getActiveSpan().getSpan(); - setIfPresent(headers, WOODY_TRACE_ID, span::setTraceId); - setIfPresent(headers, WOODY_SPAN_ID, span::setId); - setIfPresent(headers, WOODY_PARENT_ID, span::setParentId); - setIfPresent(headers, WOODY_DEADLINE, value -> span.setDeadline(Instant.parse(value))); - span.setTimestamp(0); - span.setDuration(0); - var customMetadata = traceData.getActiveSpan().getCustomMetadata(); - headers.keySet() - .stream() - .filter(s -> s.startsWith(WOODY_META_PREFIX)) - .forEach(s -> { - var metaKey = s.substring(WOODY_META_PREFIX.length()); - setIfPresent(headers, s, value -> customMetadata.putValue(metaKey, value)); - }); - var extracted = GlobalOpenTelemetry.getPropagators() - .getTextMapPropagator() - .extract(Context.root(), headers, HEADER_GETTER); - if (io.opentelemetry.api.trace.Span.fromContext(extracted).getSpanContext().isValid()) { - traceData.setPendingParentContext(extracted); - traceData.setInboundTraceParent(headers.get(OTEL_TRACE_PARENT)); - traceData.setInboundTraceState(headers.getOrDefault(OTEL_TRACE_STATE, null)); - } - return traceData; - } - - private void setIfPresent(Map headers, String key, Consumer consumer) { - var value = headers.get(key); - if (value != null && !value.isEmpty()) { - try { - consumer.accept(value); - } catch (Exception e) { - log.warn("Unable to set header with key '{}' value '{}'", key, value); - } - } - } - - private static final TextMapGetter> HEADER_GETTER = new TextMapGetter<>() { - @Override - public Iterable keys(Map carrier) { - return carrier.keySet(); - } - - @Override - public String get(Map carrier, String key) { - return carrier.get(key); - } - }; -} diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java b/src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java deleted file mode 100644 index 949d05c..0000000 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTraceResponseHandler.java +++ /dev/null @@ -1,288 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.wachter.config.properties.TracingProperties.ResponseHeaderMode; -import dev.vality.woody.api.flow.error.WErrorDefinition; -import dev.vality.woody.api.flow.error.WErrorSource; -import dev.vality.woody.api.flow.error.WErrorType; -import dev.vality.woody.api.flow.error.WRuntimeException; -import dev.vality.woody.api.trace.ContextSpan; -import dev.vality.woody.api.trace.MetadataProperties; -import dev.vality.woody.api.trace.TraceData; -import dev.vality.woody.api.trace.context.TraceContext; -import dev.vality.woody.thrift.impl.http.THMetadataProperties; -import dev.vality.woody.thrift.impl.http.THResponseInfo; -import dev.vality.woody.thrift.impl.http.error.THProviderErrorMapper; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.StatusCode; -import io.opentelemetry.semconv.HttpAttributes; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import static dev.vality.wachter.constants.TraceHeadersConstants.ExternalHeaders.*; -import static dev.vality.wachter.constants.TraceHeadersConstants.*; - -@Slf4j -public final class WoodyTraceResponseHandler { - - private final THProviderErrorMapper errorMapper = new THProviderErrorMapper(); - - void handleSuccess(HttpServletResponse response, ResponseHeaderMode responseHeaderMode) { - var traceData = TraceContext.getCurrentTraceData(); - recordOtelSpanStatus(traceData, response.getStatus()); - applyHeaders(response, traceData, null, responseHeaderMode); - } - - void handleWoodyException(HttpServletResponse response, Throwable throwable, - ResponseHeaderMode responseHeaderMode) { - var traceData = TraceContext.getCurrentTraceData(); - var responseInfo = resolveResponseInfo(traceData, throwable); - applyResponseInfo(response, responseInfo); - recordOtelSpanException(traceData, response, throwable); - applyHeaders(response, traceData, responseInfo, responseHeaderMode); - flushQuietly(response); - } - - void handleUnexpectedError(HttpServletResponse response, Throwable throwable, - ResponseHeaderMode responseHeaderMode) { - var traceData = TraceContext.getCurrentTraceData(); - var responseInfo = resolveResponseInfo(traceData, fallbackDefinition(throwable)); - applyResponseInfo(response, responseInfo); - recordOtelSpanException(traceData, response, throwable); - applyHeaders(response, traceData, responseInfo, responseHeaderMode); - flushQuietly(response); - } - - void recordOtelSpanException(Throwable throwable) { - var traceData = TraceContext.getCurrentTraceData(); - recordOtelSpanException(traceData, null, throwable); - } - - private void recordOtelSpanException(TraceData traceData, HttpServletResponse response, Throwable throwable) { - var span = extractSpan(traceData); - if (span == null || !span.getSpanContext().isValid()) { - return; - } - var status = response != null ? response.getStatus() : 0; - if (status > 0) { - span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, status); - } - span.recordException(throwable); - span.setStatus(StatusCode.ERROR); - } - - private THResponseInfo resolveResponseInfo(TraceData traceData, Throwable throwable) { - if (traceData == null) { - return fallbackResponseInfo(fallbackDefinition(throwable)); - } - var serviceSpan = traceData.getServiceSpan(); - if (serviceSpan == null) { - return fallbackResponseInfo(fallbackDefinition(throwable)); - } - serviceSpan.getMetadata().putValue(MetadataProperties.CALL_ERROR, throwable); - var definition = extractDefinition(serviceSpan, throwable); - serviceSpan.getMetadata().putValue(MetadataProperties.ERROR_DEFINITION, definition); - var responseInfo = THProviderErrorMapper.getResponseInfo(serviceSpan); - serviceSpan.getMetadata().putValue(THMetadataProperties.TH_RESPONSE_INFO, responseInfo); - return responseInfo; - } - - private THResponseInfo resolveResponseInfo(TraceData traceData, WErrorDefinition definition) { - if (traceData == null) { - return fallbackResponseInfo(definition); - } - var serviceSpan = traceData.getServiceSpan(); - if (serviceSpan == null) { - return fallbackResponseInfo(definition); - } - serviceSpan.getMetadata().putValue(MetadataProperties.ERROR_DEFINITION, definition); - var responseInfo = THProviderErrorMapper.getResponseInfo(serviceSpan); - serviceSpan.getMetadata().putValue(THMetadataProperties.TH_RESPONSE_INFO, responseInfo); - return responseInfo; - } - - private void applyResponseInfo(HttpServletResponse response, THResponseInfo responseInfo) { - if (response == null || response.isCommitted() || responseInfo == null) { - return; - } - if (responseInfo.getStatus() > 0) { - response.setStatus(responseInfo.getStatus()); - } - } - - private void recordOtelSpanStatus(TraceData traceData, int status) { - var span = extractSpan(traceData); - if (span == null || !span.getSpanContext().isValid()) { - return; - } - if (status > 0) { - span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, status); - span.setStatus(status >= 500 ? StatusCode.ERROR : StatusCode.OK); - } else { - span.setStatus(StatusCode.OK); - } - } - - private void applyHeaders(HttpServletResponse response, - TraceData traceData, - THResponseInfo responseInfo, - ResponseHeaderMode responseHeaderMode) { - if (response == null || response.isCommitted() || traceData == null) { - return; - } - var serviceSpan = traceData.getServiceSpan(); - if (serviceSpan == null || !serviceSpan.isFilled()) { - return; - } - - var headers = new HttpHeaders(); - var span = serviceSpan.getSpan(); - addHeader(headers, WOODY_TRACE_ID, span.getTraceId()); - addHeader(headers, WOODY_SPAN_ID, span.getId()); - addHeader(headers, WOODY_PARENT_ID, span.getParentId()); - var deadline = span.getDeadline(); - if (deadline != null) { - addHeader(headers, WOODY_DEADLINE, deadline.toString()); - } - serviceSpan.getCustomMetadata().getKeys() - .forEach(key -> addHeader(headers, WOODY_META_PREFIX + key, - serviceSpan.getCustomMetadata().getValue(key))); - - if (responseInfo != null) { - addHeader(headers, WOODY_ERROR_CLASS, responseInfo.getErrClass()); - addHeader(headers, WOODY_ERROR_REASON, responseInfo.getErrReason()); - } - - addHeader(headers, OTEL_TRACE_PARENT, traceData.getInboundTraceParent()); - addHeader(headers, OTEL_TRACE_STATE, traceData.getInboundTraceState()); - - switch (responseHeaderMode) { - case OFF -> { - // no headers - } - case WOODY -> applyWoodyHeaders(response, headers); - case X_WOODY -> applyXWoodyHeaders(response, headers); - case HTTP -> applyHttpHeaders(response, headers); - default -> { - } - } - } - - private void flushQuietly(HttpServletResponse response) { - if (response == null) { - return; - } - try { - response.flushBuffer(); - } catch (Exception exception) { - log.debug("Failed to flush response buffer", exception); - } - } - - private WErrorDefinition extractDefinition(ContextSpan serviceSpan, Throwable throwable) { - if (throwable instanceof WRuntimeException runtime) { - return runtime.getErrorDefinition(); - } - var mapped = errorMapper.mapToDef(throwable, serviceSpan); - if (mapped != null) { - return mapped; - } - return fallbackDefinition(throwable); - } - - private WErrorDefinition fallbackDefinition(Throwable throwable) { - var definition = new WErrorDefinition(WErrorSource.INTERNAL); - definition.setErrorType(WErrorType.UNEXPECTED_ERROR); - definition.setErrorSource(WErrorSource.INTERNAL); - if (throwable != null) { - definition.setErrorReason(Objects.toString(throwable.getMessage(), WErrorType.UNEXPECTED_ERROR.getKey())); - definition.setErrorName(throwable.getClass().getSimpleName()); - definition.setErrorMessage(throwable.getMessage()); - } else { - definition.setErrorReason(WErrorType.UNEXPECTED_ERROR.getKey()); - definition.setErrorName(WErrorType.UNEXPECTED_ERROR.getKey()); - definition.setErrorMessage(WErrorType.UNEXPECTED_ERROR.getKey()); - } - return definition; - } - - private THResponseInfo fallbackResponseInfo(WErrorDefinition definition) { - var status = definition != null && definition.getErrorType() == WErrorType.BUSINESS_ERROR - ? HttpServletResponse.SC_OK - : HttpServletResponse.SC_INTERNAL_SERVER_ERROR; - var errClass = definition != null && definition.getErrorType() != null - ? definition.getErrorType().getKey() - : WErrorType.UNEXPECTED_ERROR.getKey(); - var errReason = definition != null ? definition.getErrorReason() : WErrorType.UNEXPECTED_ERROR.getKey(); - return new THResponseInfo(status, errClass, errReason); - } - - private Span extractSpan(TraceData traceData) { - return traceData == null ? null : traceData.getOtelSpan(); - } - - private static void addHeader(HttpHeaders headers, String name, String value) { - if (name != null && value != null && !value.isEmpty()) { - headers.set(name, value); - } - } - - private static String join(List values) { - if (values == null || values.isEmpty()) { - return ""; - } - if (values.size() == 1) { - return values.getFirst(); - } - return String.join(",", new ArrayList<>(values)); - } - - private static void applyWoodyHeaders(HttpServletResponse response, HttpHeaders headers) { - headers.forEach((name, values) -> { - if (values != null) { - response.setHeader(name, join(values)); - } - }); - } - - private static void applyXWoodyHeaders(HttpServletResponse response, HttpHeaders headers) { - var normalized = TraceContextHeadersNormalizer.normalizeResponseHeaders(headers); - normalized.forEach((name, values) -> { - if (values != null) { - response.setHeader(name, join(values)); - } - }); - } - - private void applyHttpHeaders(HttpServletResponse response, HttpHeaders headers) { - var httpHeaders = new HttpHeaders(); - copyHeader(headers, OTEL_TRACE_PARENT, OTEL_TRACE_PARENT, httpHeaders); - copyHeader(headers, OTEL_TRACE_STATE, OTEL_TRACE_STATE, httpHeaders); - copyHeader(headers, WOODY_META_REQUEST_ID, X_REQUEST_ID, httpHeaders); - copyHeader(headers, WOODY_META_REQUEST_DEADLINE, X_REQUEST_DEADLINE, httpHeaders); - copyHeader(headers, WOODY_META_REQUEST_INVOICE_ID, X_INVOICE_ID, httpHeaders); - if (response.getStatus() >= 400) { - copyHeader(headers, WOODY_ERROR_CLASS, X_ERROR_CLASS, httpHeaders); - copyHeader(headers, WOODY_ERROR_REASON, X_ERROR_REASON, httpHeaders); - } - httpHeaders.forEach((name, values) -> { - if (values != null) { - response.setHeader(name, join(values)); - } - }); - } - - private void copyHeader(HttpHeaders source, String sourceName, String targetName, HttpHeaders target) { - if (sourceName == null || targetName == null) { - return; - } - var values = source.get(sourceName); - if (values != null && !values.isEmpty()) { - target.put(targetName, new ArrayList<>(values)); - } - } -} diff --git a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java b/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java deleted file mode 100644 index b14cb93..0000000 --- a/src/main/java/dev/vality/wachter/tracing/WoodyTracingFilter.java +++ /dev/null @@ -1,174 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.wachter.config.properties.TracingProperties; -import dev.vality.wachter.config.properties.TracingProperties.TracePolicy; -import dev.vality.woody.api.flow.WFlow; -import dev.vality.woody.api.flow.error.WRuntimeException; -import jakarta.servlet.FilterChain; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.util.LinkedHashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -@Slf4j -@RequiredArgsConstructor -public final class WoodyTracingFilter extends OncePerRequestFilter { - - private static final Set SENSITIVE_HEADERS = Set.of( - HttpHeaders.AUTHORIZATION.toLowerCase(Locale.ROOT), - HttpHeaders.COOKIE.toLowerCase(Locale.ROOT), - HttpHeaders.SET_COOKIE.toLowerCase(Locale.ROOT) - ); - - private final TracingProperties tracingProperties; - private final WoodyTraceResponseHandler woodyTraceResponseHandler; - - @Override - @SneakyThrows - @SuppressWarnings("NullableProblems") - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { - var path = getRequestPath(request); - var port = request.getLocalPort(); - var policy = tracingProperties.resolvePolicy(port, path); - if (policy == null) { - filterChain.doFilter(request, response); - return; - } - switch (policy.requestHeaderMode()) { - case OFF -> handleWithoutTraceRestore(request, response, filterChain, policy); - case WOODY_OR_X_WOODY -> handleWithTraceRestore(request, response, filterChain, policy); - default -> filterChain.doFilter(request, response); - } - } - - private void handleWithoutTraceRestore(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain, - TracePolicy policy) { - new WFlow().createServiceFork(() -> { - logReceived(request); - doFilterWithTraceHandling(request, response, filterChain, policy); - logSent(request, response); - }).run(); - } - - private void handleWithTraceRestore(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain, - TracePolicy policy) { - var normalized = TraceContextHeadersNormalizer.normalize(request); - var headersForTrace = TraceContextHeadersValidation.validate(normalized); - var restoredTraceData = TraceContextRestorer.restoreTraceData(headersForTrace); - WFlow.create(() -> { - logReceived(request); - doFilterWithTraceHandling(request, response, filterChain, policy); - logSent(request, response); - }, restoredTraceData).run(); - } - - @SneakyThrows - private void doFilterWithTraceHandling(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain, - TracePolicy policy) { - try { - filterChain.doFilter(request, response); - woodyTraceResponseHandler.handleSuccess(response, policy.responseHeaderMode()); - } catch (WRuntimeException woodyError) { - log.warn("Handled Woody exception during request processing", woodyError); - if (policy.propagateErrors()) { - woodyTraceResponseHandler.recordOtelSpanException(woodyError); - throw woodyError; - } - woodyTraceResponseHandler.handleWoodyException(response, woodyError, policy.responseHeaderMode()); - } catch (Throwable unexpected) { - log.error("Unhandled exception during request processing", unexpected); - if (policy.propagateErrors()) { - woodyTraceResponseHandler.recordOtelSpanException(unexpected); - throw unexpected; - } - woodyTraceResponseHandler.handleUnexpectedError(response, unexpected, policy.responseHeaderMode()); - } - } - - private void logReceived(HttpServletRequest request) { - log.info("-> Received {} {} | params: {}, headers: {}", request.getMethod(), getRequestPath(request), - extractParams(request), sanitizeHeaders(request)); - } - - private void logSent(HttpServletRequest request, HttpServletResponse response) { - log.info("<- Sent {} {} | status: {}, headers: {}", request.getMethod(), getRequestPath(request), - response.getStatus(), sanitizeResponseHeaders(response)); - } - - public static String extractParams(HttpServletRequest servletRequest) { - return servletRequest.getParameterMap().entrySet().stream() - .map(entry -> entry.getKey() + "=" + String.join(",", entry.getValue())) - .collect(Collectors.joining(", ")); - } - - private static HttpHeaders sanitizeHeaders(HttpServletRequest request) { - var headers = new HttpHeaders(); - var collectedHeaders = collectHeaders(request); - collectedHeaders.forEach((name, value) -> { - if (isSensitive(name)) { - headers.add(name, "***"); - } else { - headers.add(name, value); - } - }); - return headers; - } - - private static Map collectHeaders(HttpServletRequest request) { - var headers = new LinkedHashMap(); - var headerNames = request.getHeaderNames(); - if (headerNames != null) { - while (headerNames.hasMoreElements()) { - var name = headerNames.nextElement(); - var value = request.getHeader(name); - if (value != null) { - headers.put(name, value); - } - } - } - return headers; - } - - private static boolean isSensitive(String headerName) { - return SENSITIVE_HEADERS.contains(headerName.toLowerCase(Locale.ROOT)); - } - - private static HttpHeaders sanitizeResponseHeaders(HttpServletResponse response) { - var headers = new HttpHeaders(); - response.getHeaderNames().forEach(name -> { - if (isSensitive(name)) { - headers.add(name, "***"); - } else { - response.getHeaders(name).forEach(value -> headers.add(name, value)); - } - }); - return headers; - } - - private static String getRequestPath(HttpServletRequest request) { - var servletPath = request.getServletPath(); - if (servletPath != null && !servletPath.isBlank()) { - return servletPath; - } - var requestPath = request.getRequestURI(); - if (requestPath != null && !requestPath.isBlank()) { - return requestPath; - } - return ""; - } -} diff --git a/src/main/java/dev/vality/wachter/utils/DeadlineUtil.java b/src/main/java/dev/vality/wachter/utils/DeadlineUtil.java deleted file mode 100644 index 8606e50..0000000 --- a/src/main/java/dev/vality/wachter/utils/DeadlineUtil.java +++ /dev/null @@ -1,110 +0,0 @@ -package dev.vality.wachter.utils; - -import dev.vality.wachter.exceptions.DeadlineException; -import jakarta.annotation.Nullable; -import lombok.experimental.UtilityClass; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.regex.Pattern; - -@UtilityClass -@SuppressWarnings("ParameterName") -public class DeadlineUtil { - - public static void checkDeadline(@Nullable String xRequestDeadline, String xRequestId) { - if (xRequestDeadline == null) { - return; - } - if (containsRelativeValues(xRequestDeadline, xRequestId)) { - return; - } - try { - Instant instant = Instant.parse(xRequestDeadline); - if (Instant.now().isAfter(instant)) { - throw new DeadlineException(String.format("Deadline has expired, xRequestId=%s ", xRequestId)); - } - } catch (Exception e) { - throw new DeadlineException( - String.format("Deadline has invalid 'Instant' format, xRequestId=%s ", xRequestId)); - } - } - - public static boolean containsRelativeValues(String xRequestDeadline, String xRequestId) { - return (extractMinutes(xRequestDeadline, xRequestId) + extractSeconds(xRequestDeadline, xRequestId) + - extractMilliseconds(xRequestDeadline, xRequestId)) > 0; - } - - public static Long extractMinutes(String xRequestDeadline, String xRequestId) { - var format = "minutes"; - - checkNegativeValues(xRequestDeadline, xRequestId, "([-][0-9]+([.][0-9]+)?(?!ms)[m])", format); - - var minutes = extractValue(xRequestDeadline, "([0-9]+([.][0-9]+)?(?!ms)[m])", xRequestId, format); - - return Optional.ofNullable(minutes).map(min -> min * 60000.0).map(Double::longValue).orElse(0L); - } - - public static Long extractSeconds(String xRequestDeadline, String xRequestId) { - var format = "seconds"; - - checkNegativeValues(xRequestDeadline, xRequestId, "([-][0-9]+([.][0-9]+)?[s])", format); - - var seconds = extractValue(xRequestDeadline, "([0-9]+([.][0-9]+)?[s])", xRequestId, format); - - return Optional.ofNullable(seconds).map(s -> s * 1000.0).map(Double::longValue).orElse(0L); - } - - public static Long extractMilliseconds(String xRequestDeadline, String xRequestId) { - var format = "milliseconds"; - - checkNegativeValues(xRequestDeadline, xRequestId, "([-][0-9]+([.][0-9]+)?[m][s])", format); - - var milliseconds = extractValue(xRequestDeadline, "([0-9]+([.][0-9]+)?[m][s])", xRequestId, format); - - if (milliseconds != null && Math.ceil(milliseconds % 1) > 0) { - throw new DeadlineException( - String.format("Deadline 'milliseconds' parameter can have only integer value, xRequestId=%s ", - xRequestId)); - } - - return Optional.ofNullable(milliseconds).map(Double::longValue).orElse(0L); - } - - private static void checkNegativeValues(String xRequestDeadline, String xRequestId, String regex, String format) { - if (!match(regex, xRequestDeadline).isEmpty()) { - throw new DeadlineException( - String.format("Deadline '%s' parameter has negative value, xRequestId=%s ", format, xRequestId)); - } - } - - private static Double extractValue(String xRequestDeadline, String formatRegex, String xRequestId, String format) { - var numberRegex = "([0-9]+([.][0-9]+)?)"; - - var doubles = new ArrayList(); - for (String string : match(formatRegex, xRequestDeadline)) { - doubles.addAll(match(numberRegex, string)); - } - if (doubles.size() > 1) { - throw new DeadlineException( - String.format("Deadline '%s' parameter has a few relative value, xRequestId=%s ", format, - xRequestId)); - } - if (doubles.isEmpty()) { - return null; - } - return Double.valueOf(doubles.getFirst()); - } - - private static List match(String regex, String data) { - var pattern = Pattern.compile(regex); - var matcher = pattern.matcher(data); - var strings = new ArrayList(); - while (matcher.find()) { - strings.add(matcher.group()); - } - return strings; - } -} From ee27341d640b9209b98cc6fb6058ae0024a1951c Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Tue, 14 Oct 2025 13:31:46 +0700 Subject: [PATCH 10/14] refactor woody.http.bridge --- .../client/WachterClientOperationsTest.java | 2 +- .../WachterControllerDisabledAuthTest.java | 2 +- .../wachter/controller/WachterControllerTest.java | 4 ++-- .../integration/WachterIntegrationTest.java | 2 +- .../tracing/TraceContextHeadersExtractorTest.java | 3 ++- .../tracing/TraceContextHeadersNormalizerTest.java | 7 ++++--- .../wachter/tracing/TraceContextPipelineTest.java | 4 +++- .../wachter/tracing/TraceContextRestorerTest.java | 3 ++- .../wachter/tracing/WoodyTracingFilterTest.java | 14 ++++++++------ 9 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/test/java/dev/vality/wachter/client/WachterClientOperationsTest.java b/src/test/java/dev/vality/wachter/client/WachterClientOperationsTest.java index 9327a72..2cf2b6e 100644 --- a/src/test/java/dev/vality/wachter/client/WachterClientOperationsTest.java +++ b/src/test/java/dev/vality/wachter/client/WachterClientOperationsTest.java @@ -19,7 +19,7 @@ import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestClient; -import static dev.vality.wachter.constants.TraceHeadersConstants.ExternalHeaders.X_WOODY_TRACE_ID; +import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.ExternalHeaders.X_WOODY_TRACE_ID; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; diff --git a/src/test/java/dev/vality/wachter/controller/WachterControllerDisabledAuthTest.java b/src/test/java/dev/vality/wachter/controller/WachterControllerDisabledAuthTest.java index 3cf3139..ddc302b 100644 --- a/src/test/java/dev/vality/wachter/controller/WachterControllerDisabledAuthTest.java +++ b/src/test/java/dev/vality/wachter/controller/WachterControllerDisabledAuthTest.java @@ -1,7 +1,6 @@ package dev.vality.wachter.controller; import dev.vality.wachter.client.WachterClient; -import dev.vality.wachter.client.WachterClientResponse; import dev.vality.wachter.config.AbstractKeycloakOpenIdAsWiremockConfig; import dev.vality.wachter.testutil.TMessageUtil; import lombok.SneakyThrows; @@ -20,6 +19,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import static dev.vality.wachter.client.WachterClient.WachterClientResponse; import static java.util.UUID.randomUUID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; diff --git a/src/test/java/dev/vality/wachter/controller/WachterControllerTest.java b/src/test/java/dev/vality/wachter/controller/WachterControllerTest.java index d405d47..07ae6da 100644 --- a/src/test/java/dev/vality/wachter/controller/WachterControllerTest.java +++ b/src/test/java/dev/vality/wachter/controller/WachterControllerTest.java @@ -1,7 +1,6 @@ package dev.vality.wachter.controller; import dev.vality.wachter.client.WachterClient; -import dev.vality.wachter.client.WachterClientResponse; import dev.vality.wachter.config.AbstractKeycloakOpenIdAsWiremockConfig; import dev.vality.wachter.testutil.TMessageUtil; import lombok.SneakyThrows; @@ -19,7 +18,8 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; -import static dev.vality.wachter.constants.TraceHeadersConstants.*; +import static dev.vality.wachter.client.WachterClient.WachterClientResponse; +import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; import static java.util.UUID.randomUUID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; diff --git a/src/test/java/dev/vality/wachter/integration/WachterIntegrationTest.java b/src/test/java/dev/vality/wachter/integration/WachterIntegrationTest.java index 383ed47..a2d12d0 100644 --- a/src/test/java/dev/vality/wachter/integration/WachterIntegrationTest.java +++ b/src/test/java/dev/vality/wachter/integration/WachterIntegrationTest.java @@ -28,7 +28,7 @@ import java.util.UUID; import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static dev.vality.wachter.constants.TraceHeadersConstants.*; +import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; import static org.junit.jupiter.api.Assertions.*; @TestPropertySource(properties = { diff --git a/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersExtractorTest.java b/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersExtractorTest.java index 313ab64..ad80d4e 100644 --- a/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersExtractorTest.java +++ b/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersExtractorTest.java @@ -3,6 +3,7 @@ import dev.vality.woody.api.flow.WFlow; import dev.vality.woody.api.trace.TraceData; import dev.vality.woody.api.trace.context.TraceContext; +import dev.vality.woody.http.bridge.tracing.TraceContextHeadersExtractor; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; @@ -18,7 +19,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import static dev.vality.wachter.constants.TraceHeadersConstants.*; +import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; import static org.junit.jupiter.api.Assertions.*; class TraceContextHeadersExtractorTest { diff --git a/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizerTest.java b/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizerTest.java index 7537a93..1810c48 100644 --- a/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizerTest.java +++ b/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizerTest.java @@ -1,7 +1,8 @@ package dev.vality.wachter.tracing; -import dev.vality.wachter.security.JwtTokenDetailsExtractor; -import dev.vality.wachter.security.JwtTokenDetailsExtractor.JwtTokenDetails; +import dev.vality.woody.http.bridge.tracing.TraceContextHeadersNormalizer; +import dev.vality.woody.http.bridge.util.JwtTokenDetailsExtractor; +import dev.vality.woody.http.bridge.util.JwtTokenDetailsExtractor.JwtTokenDetails; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,7 +20,7 @@ import java.util.List; import java.util.Optional; -import static dev.vality.wachter.constants.TraceHeadersConstants.*; +import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/src/test/java/dev/vality/wachter/tracing/TraceContextPipelineTest.java b/src/test/java/dev/vality/wachter/tracing/TraceContextPipelineTest.java index c2f2144..71daf45 100644 --- a/src/test/java/dev/vality/wachter/tracing/TraceContextPipelineTest.java +++ b/src/test/java/dev/vality/wachter/tracing/TraceContextPipelineTest.java @@ -2,6 +2,8 @@ import dev.vality.woody.api.flow.WFlow; import dev.vality.woody.api.trace.context.TraceContext; +import dev.vality.woody.http.bridge.tracing.TraceContextHeadersExtractor; +import dev.vality.woody.http.bridge.tracing.TraceContextRestorer; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; @@ -15,7 +17,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import static dev.vality.wachter.constants.TraceHeadersConstants.*; +import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; import static org.junit.jupiter.api.Assertions.*; class TraceContextPipelineTest { diff --git a/src/test/java/dev/vality/wachter/tracing/TraceContextRestorerTest.java b/src/test/java/dev/vality/wachter/tracing/TraceContextRestorerTest.java index aef69d9..3f5ff91 100644 --- a/src/test/java/dev/vality/wachter/tracing/TraceContextRestorerTest.java +++ b/src/test/java/dev/vality/wachter/tracing/TraceContextRestorerTest.java @@ -2,6 +2,7 @@ import dev.vality.woody.api.trace.TraceData; import dev.vality.woody.api.trace.context.TraceContext; +import dev.vality.woody.http.bridge.tracing.TraceContextRestorer; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; @@ -16,7 +17,7 @@ import java.util.HashMap; import java.util.Map; -import static dev.vality.wachter.constants.TraceHeadersConstants.*; +import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; import static org.junit.jupiter.api.Assertions.*; class TraceContextRestorerTest { diff --git a/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java b/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java index 8eee21a..e7a0908 100644 --- a/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java +++ b/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java @@ -1,14 +1,16 @@ package dev.vality.wachter.tracing; -import dev.vality.wachter.config.properties.TracingProperties; -import dev.vality.wachter.config.properties.TracingProperties.Endpoint; -import dev.vality.wachter.config.properties.TracingProperties.RequestHeaderMode; -import dev.vality.wachter.config.properties.TracingProperties.ResponseHeaderMode; import dev.vality.woody.api.flow.error.WErrorDefinition; import dev.vality.woody.api.flow.error.WErrorSource; import dev.vality.woody.api.flow.error.WErrorType; import dev.vality.woody.api.flow.error.WRuntimeException; import dev.vality.woody.api.trace.context.TraceContext; +import dev.vality.woody.http.bridge.properties.TracingProperties; +import dev.vality.woody.http.bridge.properties.TracingProperties.Endpoint; +import dev.vality.woody.http.bridge.properties.TracingProperties.RequestHeaderMode; +import dev.vality.woody.http.bridge.properties.TracingProperties.ResponseHeaderMode; +import dev.vality.woody.http.bridge.tracing.WoodyTraceResponseHandler; +import dev.vality.woody.http.bridge.tracing.WoodyTracingFilter; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; @@ -22,8 +24,8 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import static dev.vality.wachter.constants.TraceHeadersConstants.ExternalHeaders.*; -import static dev.vality.wachter.constants.TraceHeadersConstants.*; +import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.ExternalHeaders.*; +import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; import static org.junit.jupiter.api.Assertions.*; class WoodyTracingFilterTest { From 2e99fb98d0ea18b04e76a98fd6efdd24ff91c8f5 Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Tue, 14 Oct 2025 19:29:06 +0700 Subject: [PATCH 11/14] refactor woody.http.bridge --- .../vality/wachter/testutil/ContextUtil.java | 33 -- .../tracing/ServletInstrumentationTest.java | 120 ------ .../ServletInstrumentationTestConfig.java | 57 --- .../TraceContextHeadersExtractorTest.java | 274 ------------- .../TraceContextHeadersNormalizerTest.java | 346 ----------------- .../tracing/TraceContextPipelineTest.java | 90 ----- .../tracing/TraceContextRestorerTest.java | 274 ------------- .../tracing/WoodyTracingFilterTest.java | 366 ------------------ 8 files changed, 1560 deletions(-) delete mode 100644 src/test/java/dev/vality/wachter/testutil/ContextUtil.java delete mode 100644 src/test/java/dev/vality/wachter/tracing/ServletInstrumentationTest.java delete mode 100644 src/test/java/dev/vality/wachter/tracing/ServletInstrumentationTestConfig.java delete mode 100644 src/test/java/dev/vality/wachter/tracing/TraceContextHeadersExtractorTest.java delete mode 100644 src/test/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizerTest.java delete mode 100644 src/test/java/dev/vality/wachter/tracing/TraceContextPipelineTest.java delete mode 100644 src/test/java/dev/vality/wachter/tracing/TraceContextRestorerTest.java delete mode 100644 src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java diff --git a/src/test/java/dev/vality/wachter/testutil/ContextUtil.java b/src/test/java/dev/vality/wachter/testutil/ContextUtil.java deleted file mode 100644 index 34be37f..0000000 --- a/src/test/java/dev/vality/wachter/testutil/ContextUtil.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.vality.wachter.testutil; - -import dev.vality.geck.serializer.kit.mock.FieldHandler; -import dev.vality.geck.serializer.kit.mock.MockMode; -import dev.vality.geck.serializer.kit.mock.MockTBaseProcessor; -import dev.vality.geck.serializer.kit.tbase.TBaseHandler; -import lombok.SneakyThrows; -import lombok.experimental.UtilityClass; -import org.apache.thrift.TBase; - -import java.time.Instant; -import java.util.Map; - -@UtilityClass -public class ContextUtil { - - private static final MockTBaseProcessor mockRequiredTBaseProcessor; - - static { - mockRequiredTBaseProcessor = new MockTBaseProcessor(MockMode.REQUIRED_ONLY, 15, 1); - Map.Entry timeFields = Map.entry( - structHandler -> structHandler.value(Instant.now().toString()), - new String[] {"conversation_id", "messages", "status", "user_id", "email", "fullname", - "held_until", "from_time", "to_time"} - ); - mockRequiredTBaseProcessor.addFieldHandler(timeFields.getKey(), timeFields.getValue()); - } - - @SneakyThrows - public static T fillRequiredTBaseObject(T tbase, Class type) { - return ContextUtil.mockRequiredTBaseProcessor.process(tbase, new TBaseHandler<>(type)); - } -} diff --git a/src/test/java/dev/vality/wachter/tracing/ServletInstrumentationTest.java b/src/test/java/dev/vality/wachter/tracing/ServletInstrumentationTest.java deleted file mode 100644 index a7fd331..0000000 --- a/src/test/java/dev/vality/wachter/tracing/ServletInstrumentationTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package dev.vality.wachter.tracing; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.data.SpanData; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.TestPropertySource; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestClient; - -import java.time.Duration; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@TestPropertySource(properties = { - "otel.enabled=false", - "auth.enabled=false" -}) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@Import(ServletInstrumentationTestConfig.class) -class ServletInstrumentationTest { - - private static final AttributeKey HTTP_METHOD = AttributeKey.stringKey("http.request.method"); - private static final AttributeKey HTTP_STATUS_LONG = AttributeKey.longKey("http.response.status_code"); - - @Value("${local.server.port}") - private int port; - - @Autowired - private RestClient restClient; - - @Autowired - private InMemorySpanExporter spanExporter; - - @Autowired - private SdkTracerProvider tracerProvider; - - @BeforeEach - void setUp() { - spanExporter.reset(); - } - - @AfterEach - void tearDown() { - spanExporter.reset(); - } - - @AfterAll - void shutdownTelemetry() { - tracerProvider.close(); - spanExporter.shutdown(); - GlobalOpenTelemetry.resetForTest(); - } - - @Test - void shouldCaptureServletSpanWithHttpAttributes() throws InterruptedException { - int statusCode; - try { - var response = restClient.get() - .uri("http://localhost:" + port + "/test/ping") - .retrieve() - .toEntity(String.class); - statusCode = response.getStatusCode().value(); - } catch (HttpStatusCodeException ex) { - statusCode = ex.getStatusCode().value(); - } - - assertTrue(statusCode > 0); - - List spans = waitForSpans(); - SpanData serverSpan = spans.stream() - .filter(span -> span.getKind() == SpanKind.SERVER) - .findFirst() - .orElseThrow(() -> new AssertionError("Expected SERVER span")); - - assertEquals("GET", serverSpan.getAttributes().get(HTTP_METHOD)); - int expectedStatus = statusCode; - Long statusLong = serverSpan.getAttributes().get(HTTP_STATUS_LONG); - if (statusLong != null) { - assertEquals(expectedStatus, statusLong.intValue()); - } else { - assertEquals(expectedStatus, serverSpan.getAttributes().get(HTTP_STATUS_LONG)); - } - assertTrue(serverSpan.getName().contains("/test/ping")); - - SpanData clientSpan = spans.stream() - .filter(span -> span.getKind() == SpanKind.CLIENT) - .findFirst() - .orElseThrow(() -> new AssertionError("Expected CLIENT span")); - - assertEquals("GET", clientSpan.getAttributes().get(HTTP_METHOD)); - Long clientStatusLong = clientSpan.getAttributes().get(HTTP_STATUS_LONG); - if (clientStatusLong != null) { - assertEquals(expectedStatus, clientStatusLong.intValue()); - } else { - assertEquals(expectedStatus, clientSpan.getAttributes().get(HTTP_STATUS_LONG)); - } - } - - private List waitForSpans() throws InterruptedException { - long deadline = System.nanoTime() + Duration.ofSeconds(2).toNanos(); - while ((spanExporter.getFinishedSpanItems().size() < 2) && System.nanoTime() < deadline) { - Thread.sleep(25); - } - List spans = spanExporter.getFinishedSpanItems(); - if (spans.size() < 2) { - fail("Expected client and server spans to be exported"); - } - return spans; - } -} diff --git a/src/test/java/dev/vality/wachter/tracing/ServletInstrumentationTestConfig.java b/src/test/java/dev/vality/wachter/tracing/ServletInstrumentationTestConfig.java deleted file mode 100644 index f72ad6b..0000000 --- a/src/test/java/dev/vality/wachter/tracing/ServletInstrumentationTestConfig.java +++ /dev/null @@ -1,57 +0,0 @@ -package dev.vality.wachter.tracing; - -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@TestConfiguration(proxyBeanMethods = false) -class ServletInstrumentationTestConfig { - - @Bean - @Primary - InMemorySpanExporter inMemorySpanExporter() { - return InMemorySpanExporter.create(); - } - - @Bean - @Primary - SdkTracerProvider sdkTracerProvider(InMemorySpanExporter exporter) { - return SdkTracerProvider.builder() - .addSpanProcessor(SimpleSpanProcessor.create(exporter)) - .setResource(Resource.create(Attributes.of(AttributeKey.stringKey("service.name"), "wachter-test"))) - .build(); - } - - @Bean - @Primary - OpenTelemetrySdk openTelemetrySdk(SdkTracerProvider tracerProvider) { - GlobalOpenTelemetry.resetForTest(); - OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() - .setTracerProvider(tracerProvider) - .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) - .build(); - GlobalOpenTelemetry.set(sdk); - return sdk; - } - - @RestController - static class TestController { - - @GetMapping("/test/ping") - String ping() { - return "pong"; - } - } -} diff --git a/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersExtractorTest.java b/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersExtractorTest.java deleted file mode 100644 index ad80d4e..0000000 --- a/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersExtractorTest.java +++ /dev/null @@ -1,274 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.woody.api.flow.WFlow; -import dev.vality.woody.api.trace.TraceData; -import dev.vality.woody.api.trace.context.TraceContext; -import dev.vality.woody.http.bridge.tracing.TraceContextHeadersExtractor; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; - -import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; -import static org.junit.jupiter.api.Assertions.*; - -class TraceContextHeadersExtractorTest { - - private SdkTracerProvider tracerProvider; - private Tracer tracer; - - @BeforeEach - void setUp() { - GlobalOpenTelemetry.resetForTest(); - tracerProvider = SdkTracerProvider.builder().build(); - final var openTelemetry = OpenTelemetrySdk.builder() - .setTracerProvider(tracerProvider) - .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) - .build(); - GlobalOpenTelemetry.set(openTelemetry); - tracer = openTelemetry.getTracer("test"); - } - - @AfterEach - void tearDown() { - TraceContext.setCurrentTraceData(null); - GlobalOpenTelemetry.resetForTest(); - if (tracerProvider != null) { - tracerProvider.close(); - } - } - - @Test - void shouldExtractWoodyHeadersFromTraceContext() { - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); - - final var activeSpan = traceData.getActiveSpan(); - final var span = activeSpan.getSpan(); - span.setTraceId("trace-id"); - span.setId("span-id"); - span.setParentId("parent-id"); - span.setDeadline(Instant.parse("2030-01-01T00:00:00Z")); - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.ID, "user-id"); - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.USERNAME, "username"); - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.EMAIL, "user@example.com"); - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.REALM, "/realm"); - - final Map headers = TraceContextHeadersExtractor.extractHeaders(); - - assertNotNull(headers); - assertTrue(headers.containsKey(WOODY_TRACE_ID)); - assertTrue(headers.containsKey(WOODY_SPAN_ID)); - assertTrue(headers.containsKey(OTEL_TRACE_PARENT)); - - otelSpan.end(); - } - - @Test - void shouldExtractOnlyAvailableHeaders() { - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); - - final var activeSpan = traceData.getActiveSpan(); - final var span = activeSpan.getSpan(); - span.setTraceId("trace-id"); - span.setId("span-id"); - - final Map headers = TraceContextHeadersExtractor.extractHeaders(); - - assertEquals("trace-id", headers.get(WOODY_TRACE_ID)); - assertEquals("span-id", headers.get(WOODY_SPAN_ID)); - assertNull(headers.get(WOODY_PARENT_ID)); - assertNull(headers.get(WOODY_DEADLINE)); - assertNull(headers.get(WOODY_META_ID)); - assertNotNull(headers.get(OTEL_TRACE_PARENT)); - - otelSpan.end(); - } - - @Test - void shouldIncludeRequestMetadata() { - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); - - final var serviceSpan = traceData.getServiceSpan().getSpan(); - serviceSpan.setTraceId("trace-id"); - serviceSpan.setId("span-id"); - traceData.getActiveSpan().getCustomMetadata().putValue(WoodyMetaHeaders.X_REQUEST_ID, "request-123"); - traceData.getActiveSpan().getCustomMetadata() - .putValue(WoodyMetaHeaders.X_REQUEST_DEADLINE, "2030-12-31T23:59:59Z"); - - final Map headers = TraceContextHeadersExtractor.extractHeaders(); - - assertEquals("request-123", headers.get(WOODY_META_REQUEST_ID)); - assertEquals("2030-12-31T23:59:59Z", headers.get(WOODY_META_REQUEST_DEADLINE)); - - otelSpan.end(); - } - - @Test - void shouldThrowWhenSpanContextIsInvalid() { - final var traceData = new TraceData(); - traceData.setOtelSpan(Span.getInvalid()); - TraceContext.setCurrentTraceData(traceData); - - try { - TraceContextHeadersExtractor.extractHeaders(); - fail("Expected IllegalStateException"); - } catch (IllegalStateException e) { - // Expected - } - } - - @Test - void shouldNotIncludeEmptyValues() { - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); - - final var activeSpan = traceData.getActiveSpan(); - final var span = activeSpan.getSpan(); - span.setTraceId("trace-id"); - span.setId("span-id"); - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.ID, ""); - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.USERNAME, null); - - final Map headers = TraceContextHeadersExtractor.extractHeaders(); - - assertFalse(headers.containsKey(WOODY_META_ID)); - assertFalse(headers.containsKey(WOODY_META_USERNAME)); - - otelSpan.end(); - } - - @Test - void shouldExtractAllUserIdentityMetadata() { - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); - - final var activeSpan = traceData.getActiveSpan(); - final var span = activeSpan.getSpan(); - span.setTraceId("GZvsthKQAAA"); - span.setId("GZvsthKQBBB"); - span.setParentId("undefined"); - - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.ID, "b54a93c4-415d-4f33-a5e9-3608fd043ff4"); - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.USERNAME, "noreply@valitydev.com"); - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.EMAIL, "noreply@valitydev.com"); - activeSpan.getCustomMetadata().putValue(WoodyMetaHeaders.REALM, "/internal"); - - final Map headers = TraceContextHeadersExtractor.extractHeaders(); - - assertEquals("b54a93c4-415d-4f33-a5e9-3608fd043ff4", headers.get(WOODY_META_ID)); - assertEquals("noreply@valitydev.com", headers.get(WOODY_META_USERNAME)); - assertEquals("noreply@valitydev.com", headers.get(WOODY_META_EMAIL)); - assertEquals("/internal", headers.get(WOODY_META_REALM)); - - otelSpan.end(); - } - - @Test - void shouldGenerateValidTraceparent() { - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); - - final var span = traceData.getActiveSpan().getSpan(); - span.setTraceId("trace-id"); - span.setId("span-id"); - - final Map headers = TraceContextHeadersExtractor.extractHeaders(); - - final String traceparent = headers.get(OTEL_TRACE_PARENT); - assertNotNull(traceparent); - assertTrue(traceparent.matches("00-[0-9a-f]{32}-[0-9a-f]{16}-0[0-1]")); - - otelSpan.end(); - } - - @Test - void shouldExtractComplexScenarioWithAllHeaders() { - final var traceData = new TraceData(); - TraceContext.initNewServiceTrace(traceData, WFlow.createDefaultIdGenerator(), WFlow.createDefaultIdGenerator()); - - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); - - final var activeSpan = traceData.getActiveSpan(); - final var span = activeSpan.getSpan(); - span.setTraceId("GZyWNGugAAA"); - span.setId("GZyWNGugBBB"); - span.setParentId("undefined"); - span.setDeadline(Instant.parse("2030-01-01T00:00:00Z")); - - final var metadata = activeSpan.getCustomMetadata(); - metadata.putValue(WoodyMetaHeaders.ID, "b54a93c4-415d-4f33-a5e9-3608fd043ff4"); - metadata.putValue(WoodyMetaHeaders.USERNAME, "noreply@valitydev.com"); - metadata.putValue(WoodyMetaHeaders.EMAIL, "noreply@valitydev.com"); - metadata.putValue(WoodyMetaHeaders.REALM, "/internal"); - metadata.putValue(WoodyMetaHeaders.X_REQUEST_ID, "req-12345"); - metadata.putValue(WoodyMetaHeaders.X_REQUEST_DEADLINE, "2030-01-01T00:00:00Z"); - - final Map headers = TraceContextHeadersExtractor.extractHeaders(); - - assertEquals("GZyWNGugAAA", headers.get(WOODY_TRACE_ID)); - assertEquals("GZyWNGugBBB", headers.get(WOODY_SPAN_ID)); - assertEquals("undefined", headers.get(WOODY_PARENT_ID)); - assertEquals("2030-01-01T00:00:00Z", headers.get(WOODY_DEADLINE)); - assertEquals("b54a93c4-415d-4f33-a5e9-3608fd043ff4", headers.get(WOODY_META_ID)); - assertEquals("noreply@valitydev.com", headers.get(WOODY_META_USERNAME)); - assertEquals("noreply@valitydev.com", headers.get(WOODY_META_EMAIL)); - assertEquals("/internal", headers.get(WOODY_META_REALM)); - assertEquals("req-12345", headers.get(WOODY_META_REQUEST_ID)); - assertEquals("2030-01-01T00:00:00Z", headers.get(WOODY_META_REQUEST_DEADLINE)); - assertNotNull(headers.get(OTEL_TRACE_PARENT)); - - otelSpan.end(); - } - - @Test - void shouldReturnHeadersWhenTraceDataIsAbsent() throws InterruptedException { - TraceContext.setCurrentTraceData(null); - - var captured = new AtomicReference>(); - var thread = new Thread(() -> { - captured.set(TraceContextHeadersExtractor.extractHeaders()); - }); - thread.start(); - thread.join(); - - var headers = captured.get(); - assertNotNull(headers); - assertNotNull(headers.get(OTEL_TRACE_PARENT)); - } - - @Test - void shouldThrowWhenOtelSpanIsNull() { - final var traceData = new TraceData(); - traceData.setOtelSpan(null); - TraceContext.setCurrentTraceData(traceData); - - assertThrows(IllegalStateException.class, TraceContextHeadersExtractor::extractHeaders); - } -} diff --git a/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizerTest.java b/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizerTest.java deleted file mode 100644 index 1810c48..0000000 --- a/src/test/java/dev/vality/wachter/tracing/TraceContextHeadersNormalizerTest.java +++ /dev/null @@ -1,346 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.woody.http.bridge.tracing.TraceContextHeadersNormalizer; -import dev.vality.woody.http.bridge.util.JwtTokenDetailsExtractor; -import dev.vality.woody.http.bridge.util.JwtTokenDetailsExtractor.JwtTokenDetails; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpHeaders; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class TraceContextHeadersNormalizerTest { - - @Mock - private HttpServletRequest request; - - @Mock - private SecurityContext securityContext; - - @Mock - private Authentication authentication; - - @BeforeEach - void setUp() { - SecurityContextHolder.clearContext(); - lenient().when(request.getHeader(WOODY_TRACE_ID)).thenReturn(null); - lenient().when(request.getHeader(WOODY_SPAN_ID)).thenReturn(null); - lenient().when(request.getHeader(WOODY_PARENT_ID)).thenReturn(null); - lenient().when(request.getHeader(WOODY_DEADLINE)).thenReturn(null); - lenient().when(request.getHeader(OTEL_TRACE_PARENT)).thenReturn(null); - lenient().when(request.getHeader(OTEL_TRACE_STATE)).thenReturn(null); - lenient().when(request.getHeader(ExternalHeaders.X_WOODY_TRACE_ID)).thenReturn(null); - lenient().when(request.getHeader(ExternalHeaders.X_WOODY_SPAN_ID)).thenReturn(null); - lenient().when(request.getHeader(ExternalHeaders.X_WOODY_PARENT_ID)).thenReturn(null); - lenient().when(request.getHeader(ExternalHeaders.X_WOODY_DEADLINE)).thenReturn(null); - lenient().when(request.getHeader(ExternalHeaders.X_REQUEST_ID)).thenReturn(null); - lenient().when(request.getHeader(ExternalHeaders.X_REQUEST_DEADLINE)).thenReturn(null); - lenient().when(request.getHeader(ExternalHeaders.X_INVOICE_ID)).thenReturn(null); - } - - @Test - void shouldNormalizeWoodyHeadersFromLowercase() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of( - WOODY_TRACE_ID, WOODY_SPAN_ID, WOODY_PARENT_ID, WOODY_DEADLINE - ))); - when(request.getHeader(WOODY_TRACE_ID)).thenReturn("trace-123"); - when(request.getHeader(WOODY_SPAN_ID)).thenReturn("span-456"); - when(request.getHeader(WOODY_PARENT_ID)).thenReturn("parent-789"); - when(request.getHeader(WOODY_DEADLINE)).thenReturn("2030-01-01T00:00:00Z"); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("trace-123", normalized.get(WOODY_TRACE_ID)); - assertEquals("span-456", normalized.get(WOODY_SPAN_ID)); - assertEquals("parent-789", normalized.get(WOODY_PARENT_ID)); - assertEquals("2030-01-01T00:00:00Z", normalized.get(WOODY_DEADLINE)); - } - - @Test - void shouldNormalizeXWoodyHeadersToWoody() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of( - ExternalHeaders.X_WOODY_TRACE_ID, ExternalHeaders.X_WOODY_SPAN_ID, ExternalHeaders.X_WOODY_PARENT_ID - ))); - when(request.getHeader(ExternalHeaders.X_WOODY_TRACE_ID)).thenReturn("trace-123"); - when(request.getHeader(ExternalHeaders.X_WOODY_SPAN_ID)).thenReturn("span-456"); - when(request.getHeader(ExternalHeaders.X_WOODY_PARENT_ID)).thenReturn("parent-789"); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("trace-123", normalized.get(WOODY_TRACE_ID)); - assertEquals("span-456", normalized.get(WOODY_SPAN_ID)); - assertEquals("parent-789", normalized.get(WOODY_PARENT_ID)); - } - - @Test - void shouldNormalizeUserIdentityMetadataFromXWoody() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of( - ExternalHeaders.X_WOODY_META_ID, - ExternalHeaders.X_WOODY_META_USERNAME, - ExternalHeaders.X_WOODY_META_EMAIL, - ExternalHeaders.X_WOODY_META_REALM - ))); - when(request.getHeader(ExternalHeaders.X_WOODY_META_ID)).thenReturn("user-id-123"); - when(request.getHeader(ExternalHeaders.X_WOODY_META_USERNAME)).thenReturn("john.doe"); - when(request.getHeader(ExternalHeaders.X_WOODY_META_EMAIL)).thenReturn("john@example.com"); - when(request.getHeader(ExternalHeaders.X_WOODY_META_REALM)).thenReturn("/internal"); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("user-id-123", normalized.get(WOODY_META_ID)); - assertEquals("john.doe", normalized.get(WOODY_META_USERNAME)); - assertEquals("john@example.com", normalized.get(WOODY_META_EMAIL)); - assertEquals("/internal", normalized.get(WOODY_META_REALM)); - } - - @Test - void shouldNormalizeTraceparentHeader() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of(OTEL_TRACE_PARENT))); - when(request.getHeader(OTEL_TRACE_PARENT)).thenReturn("00-123abc-456def-01"); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("00-123abc-456def-01", normalized.get(OTEL_TRACE_PARENT)); - } - - @Test - void shouldMergeJwtMetadataIntoHeaders() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(Collections.emptyList())); - SecurityContextHolder.setContext(securityContext); - when(securityContext.getAuthentication()).thenReturn(authentication); - - try (MockedStatic extractor = mockStatic(JwtTokenDetailsExtractor.class)) { - var tokenDetails = new JwtTokenDetails( - "user-jwt-id", - "jwt-username", - "jwt@email.com", - "/jwt-realm", - List.of("ROLE_USER") - ); - extractor.when(() -> JwtTokenDetailsExtractor.extractFromContext(authentication)) - .thenReturn(Optional.of(tokenDetails)); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("user-jwt-id", normalized.get(WOODY_META_ID)); - assertEquals("jwt-username", normalized.get(WOODY_META_USERNAME)); - assertEquals("jwt@email.com", normalized.get(WOODY_META_EMAIL)); - assertEquals("/jwt-realm", normalized.get(WOODY_META_REALM)); - } - } - - @Test - void shouldMergeJwtMetadataWithHeaders() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of( - ExternalHeaders.X_WOODY_META_ID - ))); - when(request.getHeader(ExternalHeaders.X_WOODY_META_ID)).thenReturn("header-user-id"); - when(request.getHeader(OTEL_TRACE_PARENT)).thenReturn(null); - - SecurityContextHolder.setContext(securityContext); - when(securityContext.getAuthentication()).thenReturn(authentication); - - try (MockedStatic extractor = mockStatic(JwtTokenDetailsExtractor.class)) { - var tokenDetails = new JwtTokenDetails( - "jwt-user-id", - "jwt-username", - null, - null, - List.of() - ); - extractor.when(() -> JwtTokenDetailsExtractor.extractFromContext(authentication)) - .thenReturn(Optional.of(tokenDetails)); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("jwt-user-id", normalized.get(WOODY_META_ID)); - assertEquals("jwt-username", normalized.get(WOODY_META_USERNAME)); - } - } - - @Test - void shouldMergeRequestDeadlineToWoodyDeadline() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(Collections.emptyList())); - when(request.getHeader(ExternalHeaders.X_REQUEST_DEADLINE)).thenReturn("2030-12-31T23:59:59Z"); - when(request.getHeader(ExternalHeaders.X_REQUEST_ID)).thenReturn(null); - when(request.getHeader(OTEL_TRACE_PARENT)).thenReturn(null); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("2030-12-31T23:59:59Z", normalized.get(WOODY_DEADLINE)); - assertEquals("2030-12-31T23:59:59Z", normalized.get(WOODY_META_REQUEST_DEADLINE)); - assertFalse(normalized.containsKey(ExternalHeaders.X_REQUEST_DEADLINE)); - } - - @Test - void shouldMergeRelativeDeadline() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(Collections.emptyList())); - when(request.getHeader(ExternalHeaders.X_REQUEST_DEADLINE)).thenReturn("30s"); - when(request.getHeader(ExternalHeaders.X_REQUEST_ID)).thenReturn("req-123"); - when(request.getHeader(OTEL_TRACE_PARENT)).thenReturn(null); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertNotNull(normalized.get(WOODY_DEADLINE)); - assertEquals(normalized.get(WOODY_DEADLINE), normalized.get(WOODY_META_REQUEST_DEADLINE)); - assertFalse(normalized.containsKey(ExternalHeaders.X_REQUEST_DEADLINE)); - assertTrue(Instant.parse(normalized.get(WOODY_DEADLINE)).isAfter(Instant.now())); - } - - @Test - void shouldNotOverwriteExistingWoodyDeadline() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of(WOODY_DEADLINE))); - when(request.getHeader(WOODY_DEADLINE)).thenReturn("2025-01-01T00:00:00Z"); - when(request.getHeader(ExternalHeaders.X_REQUEST_DEADLINE)).thenReturn("2030-12-31T23:59:59Z"); - when(request.getHeader(ExternalHeaders.X_REQUEST_ID)).thenReturn(null); - when(request.getHeader(OTEL_TRACE_PARENT)).thenReturn(null); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("2025-01-01T00:00:00Z", normalized.get(WOODY_DEADLINE)); - assertEquals("2030-12-31T23:59:59Z", normalized.get(WOODY_META_REQUEST_DEADLINE)); - assertFalse(normalized.containsKey(ExternalHeaders.X_REQUEST_DEADLINE)); - } - - @Test - void shouldPreserveRequestIdWithoutDeadline() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(Collections.emptyList())); - when(request.getHeader(ExternalHeaders.X_REQUEST_ID)).thenReturn("req-no-deadline"); - when(request.getHeader(OTEL_TRACE_PARENT)).thenReturn(null); - when(request.getHeader(ExternalHeaders.X_REQUEST_DEADLINE)).thenReturn(null); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("req-no-deadline", normalized.get(WOODY_META_REQUEST_ID)); - assertFalse(normalized.containsKey(WOODY_META_REQUEST_DEADLINE)); - assertFalse(normalized.containsKey(ExternalHeaders.X_REQUEST_ID)); - } - - @Test - void shouldHandleEmptyHeaders() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(Collections.emptyList())); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertTrue(normalized.isEmpty()); - } - - @Test - void shouldIgnoreNullHeaderValues() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of( - WOODY_TRACE_ID, WOODY_SPAN_ID - ))); - when(request.getHeader(WOODY_TRACE_ID)).thenReturn(null); - when(request.getHeader(WOODY_SPAN_ID)).thenReturn("span-456"); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertFalse(normalized.containsKey(WOODY_TRACE_ID)); - assertEquals("span-456", normalized.get(WOODY_SPAN_ID)); - } - - @Test - void shouldHandleComplexScenarioWithAllHeaderTypes() { - when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of( - WOODY_TRACE_ID, - ExternalHeaders.X_WOODY_SPAN_ID, - ExternalHeaders.X_WOODY_PARENT_ID, - ExternalHeaders.X_WOODY_META_EMAIL, - OTEL_TRACE_PARENT, - "content-type", - "authorization" - ))); - when(request.getHeader(WOODY_TRACE_ID)).thenReturn("GZyWNGugAAA"); - when(request.getHeader(ExternalHeaders.X_WOODY_SPAN_ID)).thenReturn("GZyWNGugBBB"); - when(request.getHeader(ExternalHeaders.X_WOODY_PARENT_ID)).thenReturn("undefined"); - when(request.getHeader(ExternalHeaders.X_WOODY_META_EMAIL)).thenReturn("noreply@valitydev.com"); - when(request.getHeader(OTEL_TRACE_PARENT)).thenReturn( - "00-cfa3d3072a4e3e99fc14829a65311819-6e4609576fa4d077-01"); - when(request.getHeader(ExternalHeaders.X_REQUEST_ID)).thenReturn("req-complex"); - when(request.getHeader(ExternalHeaders.X_REQUEST_DEADLINE)).thenReturn("2030-01-01T00:00:00Z"); - - SecurityContextHolder.setContext(securityContext); - when(securityContext.getAuthentication()).thenReturn(authentication); - - try (MockedStatic extractor = mockStatic(JwtTokenDetailsExtractor.class)) { - var tokenDetails = new JwtTokenDetails( - "b54a93c4-415d-4f33-a5e9-3608fd043ff4", - "noreply@valitydev.com", - "noreply@valitydev.com", - "/internal", - List.of("ROLE_USER") - ); - extractor.when(() -> JwtTokenDetailsExtractor.extractFromContext(authentication)) - .thenReturn(Optional.of(tokenDetails)); - - var normalized = TraceContextHeadersNormalizer.normalize(request); - - assertEquals("GZyWNGugAAA", normalized.get(WOODY_TRACE_ID)); - assertEquals("GZyWNGugBBB", normalized.get(WOODY_SPAN_ID)); - assertEquals("undefined", normalized.get(WOODY_PARENT_ID)); - assertEquals("noreply@valitydev.com", normalized.get(WOODY_META_EMAIL)); - assertEquals("b54a93c4-415d-4f33-a5e9-3608fd043ff4", - normalized.get(WOODY_META_ID)); - assertEquals("noreply@valitydev.com", normalized.get(WOODY_META_USERNAME)); - assertEquals("/internal", normalized.get(WOODY_META_REALM)); - assertEquals("00-cfa3d3072a4e3e99fc14829a65311819-6e4609576fa4d077-01", normalized.get(OTEL_TRACE_PARENT)); - assertEquals("2030-01-01T00:00:00Z", normalized.get(WOODY_DEADLINE)); - assertEquals("req-complex", normalized.get(WOODY_META_REQUEST_ID)); - assertEquals("2030-01-01T00:00:00Z", normalized.get(WOODY_META_REQUEST_DEADLINE)); - assertFalse(normalized.containsKey(ExternalHeaders.X_REQUEST_ID)); - assertFalse(normalized.containsKey(ExternalHeaders.X_REQUEST_DEADLINE)); - } - } - - @Test - void shouldNormalizeResponseHeaders() { - var responseHeaders = new HttpHeaders(); - responseHeaders.add(WOODY_TRACE_ID, "resp-trace"); - responseHeaders.add(WOODY_SPAN_ID, "resp-span"); - responseHeaders.add(WOODY_PARENT_ID, "resp-parent"); - responseHeaders.add(WOODY_META_ID, "resp-user"); - responseHeaders.add(WOODY_META_REQUEST_ID, "resp-req"); - responseHeaders.add(WOODY_META_REQUEST_DEADLINE, "2030-01-01T00:00:00Z"); - responseHeaders.add(WOODY_META_REQUEST_INVOICE_ID, "resp-req"); - responseHeaders.add(WOODY_DEADLINE, "2030-01-01T00:00:00Z"); - responseHeaders.add(WOODY_ERROR_CLASS, "resp-req"); - responseHeaders.add(WOODY_ERROR_REASON, "resp-req"); - responseHeaders.add(OTEL_TRACE_PARENT, "00-abc-def-01"); - responseHeaders.add(OTEL_TRACE_STATE, "00-abc-def-01"); - responseHeaders.add("Content-Type", "application/json"); - responseHeaders.add("Cache-Control", "no-cache"); - - var normalized = TraceContextHeadersNormalizer.normalizeResponseHeaders(responseHeaders); - - assertTrue(normalized.containsKey(ExternalHeaders.X_WOODY_TRACE_ID)); - assertTrue(normalized.containsKey(ExternalHeaders.X_WOODY_SPAN_ID)); - assertTrue(normalized.containsKey(ExternalHeaders.X_WOODY_PARENT_ID)); - assertTrue(normalized.containsKey(ExternalHeaders.X_WOODY_DEADLINE)); - assertTrue(normalized.containsKey(ExternalHeaders.X_WOODY_ERROR_CLASS)); - assertTrue(normalized.containsKey(ExternalHeaders.X_WOODY_ERROR_REASON)); - assertTrue(normalized.containsKey(ExternalHeaders.X_WOODY_META_ID)); - assertTrue(normalized.containsKey(ExternalHeaders.X_REQUEST_ID)); - assertTrue(normalized.containsKey(ExternalHeaders.X_REQUEST_DEADLINE)); - assertTrue(normalized.containsKey(ExternalHeaders.X_INVOICE_ID)); - assertTrue(normalized.containsKey(OTEL_TRACE_PARENT)); - assertTrue(normalized.containsKey(OTEL_TRACE_STATE)); - assertFalse(normalized.containsKey("Content-Type")); - assertFalse(normalized.containsKey("Cache-Control")); - } -} diff --git a/src/test/java/dev/vality/wachter/tracing/TraceContextPipelineTest.java b/src/test/java/dev/vality/wachter/tracing/TraceContextPipelineTest.java deleted file mode 100644 index 71daf45..0000000 --- a/src/test/java/dev/vality/wachter/tracing/TraceContextPipelineTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.woody.api.flow.WFlow; -import dev.vality.woody.api.trace.context.TraceContext; -import dev.vality.woody.http.bridge.tracing.TraceContextHeadersExtractor; -import dev.vality.woody.http.bridge.tracing.TraceContextRestorer; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; - -import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; -import static org.junit.jupiter.api.Assertions.*; - -class TraceContextPipelineTest { - - private SdkTracerProvider tracerProvider; - - @BeforeEach - void setUp() { - GlobalOpenTelemetry.resetForTest(); - tracerProvider = SdkTracerProvider.builder().build(); - var openTelemetry = OpenTelemetrySdk.builder() - .setTracerProvider(tracerProvider) - .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) - .build(); - GlobalOpenTelemetry.set(openTelemetry); - } - - @AfterEach - void tearDown() { - TraceContext.setCurrentTraceData(null); - GlobalOpenTelemetry.resetForTest(); - if (tracerProvider != null) { - tracerProvider.close(); - } - } - - @Test - void shouldRestoreAndExtractTraceContextWithinWFlow() { - var normalized = new HashMap(); - normalized.put(WOODY_TRACE_ID, "GZyWNGugAAA"); - normalized.put(WOODY_SPAN_ID, "GZyWNGugBBB"); - normalized.put(WOODY_PARENT_ID, "undefined"); - normalized.put(WOODY_DEADLINE, "2030-01-01T00:00:00Z"); - var otelTraceId = "3d8202ad198e4d37771c995246e1b356"; - normalized.put(OTEL_TRACE_PARENT, "00-" + otelTraceId + "-9cfa814ae977266e-01"); - normalized.put(WOODY_META_ID, "user-id"); - normalized.put(WOODY_META_USERNAME, "user-name"); - normalized.put(WOODY_META_EMAIL, "user@example.com"); - normalized.put(WOODY_META_REALM, "/internal"); - normalized.put(WOODY_META_REQUEST_ID, "request-id"); - normalized.put(WOODY_META_REQUEST_DEADLINE, "2030-01-01T00:00:00Z"); - - var traceData = TraceContextRestorer.restoreTraceData(normalized); - assertTrue(traceData.getServiceSpan().getSpan().isFilled()); - assertFalse(traceData.isClient()); - var extractedRef = new AtomicReference>(); - - WFlow.create(() -> { - var current = TraceContext.getCurrentTraceData(); - assertNotNull(current); - assertTrue(current.getServiceSpan().getSpan().isFilled()); - assertFalse(current.isClient()); - extractedRef.set(TraceContextHeadersExtractor.extractHeaders()); - }, traceData).run(); - - var extracted = extractedRef.get(); - assertNotNull(extracted); - assertEquals("GZyWNGugAAA", extracted.get(WOODY_TRACE_ID)); - assertEquals("GZyWNGugBBB", extracted.get(WOODY_SPAN_ID)); - assertEquals("undefined", extracted.get(WOODY_PARENT_ID)); - assertEquals("2030-01-01T00:00:00Z", extracted.get(WOODY_DEADLINE)); - assertEquals("user-id", extracted.get(WOODY_META_ID)); - assertEquals("user-name", extracted.get(WOODY_META_USERNAME)); - assertEquals("user@example.com", extracted.get(WOODY_META_EMAIL)); - assertEquals("/internal", extracted.get(WOODY_META_REALM)); - assertEquals("request-id", extracted.get(WOODY_META_REQUEST_ID)); - assertEquals("2030-01-01T00:00:00Z", extracted.get(WOODY_META_REQUEST_DEADLINE)); - assertTrue(extracted.get(OTEL_TRACE_PARENT).contains(otelTraceId)); - } -} diff --git a/src/test/java/dev/vality/wachter/tracing/TraceContextRestorerTest.java b/src/test/java/dev/vality/wachter/tracing/TraceContextRestorerTest.java deleted file mode 100644 index 3f5ff91..0000000 --- a/src/test/java/dev/vality/wachter/tracing/TraceContextRestorerTest.java +++ /dev/null @@ -1,274 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.woody.api.trace.TraceData; -import dev.vality.woody.api.trace.context.TraceContext; -import dev.vality.woody.http.bridge.tracing.TraceContextRestorer; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; - -import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; -import static org.junit.jupiter.api.Assertions.*; - -class TraceContextRestorerTest { - - private SdkTracerProvider tracerProvider; - - @BeforeEach - void setUp() { - GlobalOpenTelemetry.resetForTest(); - tracerProvider = SdkTracerProvider.builder().build(); - var openTelemetry = OpenTelemetrySdk.builder() - .setTracerProvider(tracerProvider) - .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) - .build(); - GlobalOpenTelemetry.set(openTelemetry); - } - - @AfterEach - void tearDown() { - TraceContext.setCurrentTraceData(null); - GlobalOpenTelemetry.resetForTest(); - if (tracerProvider != null) { - tracerProvider.close(); - } - } - - @Test - void shouldRestoreWoodyTraceHeaders() { - var headers = Map.of( - WOODY_TRACE_ID, "GZyWNGugAAA", - WOODY_SPAN_ID, "GZyWNGugBBB", - WOODY_PARENT_ID, "undefined", - WOODY_DEADLINE, "2030-01-01T00:00:00Z" - ); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - assertNotNull(traceData); - var span = traceData.getServiceSpan().getSpan(); - assertEquals("GZyWNGugAAA", span.getTraceId()); - assertEquals("GZyWNGugBBB", span.getId()); - assertEquals("undefined", span.getParentId()); - assertEquals(Instant.parse("2030-01-01T00:00:00Z"), span.getDeadline()); - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldRestoreUserIdentityMetadata() { - var headers = new HashMap(); - headers.put(WOODY_TRACE_ID, "trace-123"); - headers.put(WOODY_META_ID, "b54a93c4-415d-4f33-a5e9-3608fd043ff4"); - headers.put(WOODY_META_USERNAME, "noreply@valitydev.com"); - headers.put(WOODY_META_EMAIL, "noreply@valitydev.com"); - headers.put(WOODY_META_REALM, "/internal"); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - var metadata = traceData.getActiveSpan().getCustomMetadata(); - assertEquals("b54a93c4-415d-4f33-a5e9-3608fd043ff4", metadata.getValue(WoodyMetaHeaders.ID)); - assertEquals("noreply@valitydev.com", metadata.getValue(WoodyMetaHeaders.USERNAME)); - assertEquals("noreply@valitydev.com", metadata.getValue(WoodyMetaHeaders.EMAIL)); - assertEquals("/internal", metadata.getValue(WoodyMetaHeaders.REALM)); - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldRestoreRequestMetadata() { - var headers = Map.of( - WOODY_TRACE_ID, "trace-123", - WOODY_META_REQUEST_ID, "req-456", - WOODY_META_REQUEST_DEADLINE, "2030-12-31T23:59:59Z" - ); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - var metadata = traceData.getActiveSpan().getCustomMetadata(); - assertEquals("req-456", metadata.getValue(WoodyMetaHeaders.X_REQUEST_ID)); - assertEquals("2030-12-31T23:59:59Z", metadata.getValue(WoodyMetaHeaders.X_REQUEST_DEADLINE)); - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldRestoreTraceparentAndCreateOtelSpan() { - var traceId = "cfa3d3072a4e3e99fc14829a65311819"; - var headers = Map.of( - WOODY_TRACE_ID, "trace-123", - OTEL_TRACE_PARENT, "00-" + traceId + "-6e4609576fa4d077-01" - ); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - assertEquals("00-" + traceId + "-6e4609576fa4d077-01", traceData.getInboundTraceParent()); - var parentContext = Span.fromContext(traceData.consumePendingParentContext()).getSpanContext(); - assertTrue(parentContext.isValid()); - assertEquals(traceId, parentContext.getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldHandleEmptyHeaders() { - TraceData traceData = TraceContextRestorer.restoreTraceData(Map.of()); - - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldHandlePartialHeaders() { - var headers = Map.of( - WOODY_TRACE_ID, "trace-123" - ); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - assertEquals("trace-123", traceData.getServiceSpan().getSpan().getTraceId()); - assertNull(traceData.getServiceSpan().getSpan().getDeadline()); - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldHandleInvalidDeadline() { - var headers = Map.of( - WOODY_TRACE_ID, "trace-123", - WOODY_DEADLINE, "invalid-date" - ); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - assertEquals("trace-123", traceData.getServiceSpan().getSpan().getTraceId()); - assertNull(traceData.getServiceSpan().getSpan().getDeadline()); - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldHandleComplexScenarioWithAllData() { - var headers = new HashMap(); - headers.put(WOODY_TRACE_ID, "GZvsthKQAAA"); - headers.put(WOODY_SPAN_ID, "GZvsthKQBBB"); - headers.put(WOODY_PARENT_ID, "parent-123"); - headers.put(WOODY_DEADLINE, "2030-06-15T12:30:00Z"); - var otelTraceId = "3d8202ad198e4d37771c995246e1b356"; - headers.put(OTEL_TRACE_PARENT, "00-" + otelTraceId + "-9cfa814ae977266e-01"); - headers.put(WOODY_META_ID, "user-uuid"); - headers.put(WOODY_META_USERNAME, "john.doe"); - headers.put(WOODY_META_EMAIL, "john@example.com"); - headers.put(WOODY_META_REALM, "/external"); - headers.put(WOODY_META_REQUEST_ID, "complex-request-id"); - headers.put(WOODY_META_REQUEST_DEADLINE, "2030-06-15T13:00:00Z"); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - var span = traceData.getServiceSpan().getSpan(); - assertEquals("GZvsthKQAAA", span.getTraceId()); - assertEquals("GZvsthKQBBB", span.getId()); - assertEquals("parent-123", span.getParentId()); - assertEquals(Instant.parse("2030-06-15T12:30:00Z"), span.getDeadline()); - - var metadata = traceData.getActiveSpan().getCustomMetadata(); - assertEquals("user-uuid", metadata.getValue(WoodyMetaHeaders.ID)); - assertEquals("john.doe", metadata.getValue(WoodyMetaHeaders.USERNAME)); - assertEquals("john@example.com", metadata.getValue(WoodyMetaHeaders.EMAIL)); - assertEquals("/external", metadata.getValue(WoodyMetaHeaders.REALM)); - assertEquals("complex-request-id", metadata.getValue(WoodyMetaHeaders.X_REQUEST_ID)); - assertEquals("2030-06-15T13:00:00Z", metadata.getValue(WoodyMetaHeaders.X_REQUEST_DEADLINE)); - - assertEquals("00-" + otelTraceId + "-9cfa814ae977266e-01", traceData.getInboundTraceParent()); - var parentContext = Span.fromContext(traceData.consumePendingParentContext()).getSpanContext(); - assertTrue(parentContext.isValid()); - assertEquals(otelTraceId, parentContext.getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldHandleNullAndEmptyValues() { - var headers = new HashMap(); - headers.put(WOODY_TRACE_ID, "trace-123"); - headers.put(WOODY_SPAN_ID, ""); - headers.put(WOODY_PARENT_ID, null); - headers.put(WOODY_META_ID, ""); - headers.put(WOODY_META_USERNAME, null); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - var span = traceData.getServiceSpan().getSpan(); - assertEquals("trace-123", span.getTraceId()); - assertNotNull(span.getId()); - assertNotNull(span.getParentId()); - - var metadata = traceData.getActiveSpan().getCustomMetadata(); - assertNull(metadata.getValue(WoodyMetaHeaders.ID)); - assertNull(metadata.getValue(WoodyMetaHeaders.USERNAME)); - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldHandleInvalidTraceparentGracefully() { - var headers = Map.of( - WOODY_TRACE_ID, "trace-123", - OTEL_TRACE_PARENT, "invalid-traceparent" - ); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - assertEquals("trace-123", traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getTraceId()); - assertNotNull(traceData.getServiceSpan().getSpan().getId()); - assertTrue(traceData.getOtelSpan().getSpanContext().isValid()); - assertNotNull(traceData.getOtelSpan().getSpanContext().getTraceId()); - } - - @Test - void shouldRestoreMetadataWithSpecialCharacters() { - var headers = new HashMap(); - headers.put(WOODY_TRACE_ID, "trace-123"); - headers.put(WOODY_META_USERNAME, "user@domain.com"); - headers.put(WOODY_META_EMAIL, "user+test@domain.com"); - headers.put(WOODY_META_REALM, "/realm/with/slashes"); - headers.put(WOODY_META_REQUEST_ID, "req-with-dashes-123"); - - TraceData traceData = TraceContextRestorer.restoreTraceData(headers); - - var metadata = traceData.getActiveSpan().getCustomMetadata(); - assertEquals("user@domain.com", metadata.getValue(WoodyMetaHeaders.USERNAME)); - assertEquals("user+test@domain.com", metadata.getValue(WoodyMetaHeaders.EMAIL)); - assertEquals("/realm/with/slashes", metadata.getValue(WoodyMetaHeaders.REALM)); - assertEquals("req-with-dashes-123", metadata.getValue(WoodyMetaHeaders.X_REQUEST_ID)); - } -} diff --git a/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java b/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java deleted file mode 100644 index e7a0908..0000000 --- a/src/test/java/dev/vality/wachter/tracing/WoodyTracingFilterTest.java +++ /dev/null @@ -1,366 +0,0 @@ -package dev.vality.wachter.tracing; - -import dev.vality.woody.api.flow.error.WErrorDefinition; -import dev.vality.woody.api.flow.error.WErrorSource; -import dev.vality.woody.api.flow.error.WErrorType; -import dev.vality.woody.api.flow.error.WRuntimeException; -import dev.vality.woody.api.trace.context.TraceContext; -import dev.vality.woody.http.bridge.properties.TracingProperties; -import dev.vality.woody.http.bridge.properties.TracingProperties.Endpoint; -import dev.vality.woody.http.bridge.properties.TracingProperties.RequestHeaderMode; -import dev.vality.woody.http.bridge.properties.TracingProperties.ResponseHeaderMode; -import dev.vality.woody.http.bridge.tracing.WoodyTraceResponseHandler; -import dev.vality.woody.http.bridge.tracing.WoodyTracingFilter; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import jakarta.servlet.FilterChain; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.mock.web.MockFilterChain; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; - -import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.ExternalHeaders.*; -import static dev.vality.woody.http.bridge.tracing.TraceHeadersConstants.*; -import static org.junit.jupiter.api.Assertions.*; - -class WoodyTracingFilterTest { - - private SdkTracerProvider tracerProvider; - private WoodyTracingFilter filter; - private TracingProperties tracingProperties; - - @BeforeEach - void setUp() { - GlobalOpenTelemetry.resetForTest(); - tracerProvider = SdkTracerProvider.builder().build(); - final var openTelemetry = OpenTelemetrySdk.builder() - .setTracerProvider(tracerProvider) - .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) - .build(); - GlobalOpenTelemetry.set(openTelemetry); - tracingProperties = new TracingProperties(); - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, null); - } - - @AfterEach - void tearDown() { - TraceContext.setCurrentTraceData(null); - GlobalOpenTelemetry.resetForTest(); - if (tracerProvider != null) { - tracerProvider.close(); - } - } - - @Test - void shouldInitializeTraceContext() throws Exception { - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(X_WOODY_TRACE_ID, "test-trace"); - request.addHeader(X_WOODY_SPAN_ID, "test-span"); - - filter.doFilter(request, response, new MockFilterChain()); - - assertEquals(200, response.getStatus()); - } - - @Test - void shouldHandleRequestCorrectly() throws Exception { - final var request = new MockHttpServletRequest("GET", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - - filter.doFilter(request, response, new MockFilterChain()); - - assertEquals(200, response.getStatus()); - } - - @Test - void shouldSetSpanStatusErrorForServerError() throws Exception { - final var request = new MockHttpServletRequest("GET", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - final FilterChain chain = (req, res) -> ((MockHttpServletResponse) res).setStatus(503); - - filter.doFilter(request, response, chain); - - assertEquals(503, response.getStatus()); - } - - @Test - void shouldEchoWoodyHeadersOnSuccess() throws Exception { - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.WOODY, null); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(X_WOODY_TRACE_ID, "woody-trace-id"); - request.addHeader(X_WOODY_SPAN_ID, "woody-span-id"); - - filter.doFilter(request, response, new MockFilterChain()); - - assertEquals("woody-trace-id", response.getHeader(WOODY_TRACE_ID)); - assertEquals("woody-span-id", response.getHeader(WOODY_SPAN_ID)); - assertNull(response.getHeader(X_WOODY_TRACE_ID)); - assertNull(response.getHeader(X_WOODY_SPAN_ID)); - } - - @Test - void shouldReturnXWoodyHeadersWhenConfigured() throws Exception { - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.X_WOODY, null); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(X_WOODY_TRACE_ID, "x-woody-trace"); - request.addHeader(X_WOODY_SPAN_ID, "x-woody-span"); - - filter.doFilter(request, response, new MockFilterChain()); - - assertEquals("x-woody-trace", response.getHeader(X_WOODY_TRACE_ID)); - assertEquals("x-woody-span", response.getHeader(X_WOODY_SPAN_ID)); - assertNull(response.getHeader(WOODY_TRACE_ID)); - assertNull(response.getHeader(WOODY_SPAN_ID)); - } - - @Test - void shouldMapWoodyExceptionToErrorResponse() throws Exception { - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.WOODY, null); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(X_WOODY_TRACE_ID, "trace-err"); - request.addHeader(X_WOODY_SPAN_ID, "span-err"); - - var errorDefinition = new WErrorDefinition(WErrorSource.INTERNAL); - errorDefinition.setErrorType(WErrorType.UNEXPECTED_ERROR); - errorDefinition.setErrorReason("boom"); - - filter.doFilter(request, response, (req, res) -> { - throw new WRuntimeException(errorDefinition); - }); - - assertEquals(500, response.getStatus()); - assertEquals(WErrorType.UNEXPECTED_ERROR.getKey(), response.getHeader(WOODY_ERROR_CLASS)); - assertEquals("boom", response.getHeader(WOODY_ERROR_REASON)); - assertNull(response.getHeader(X_WOODY_ERROR_CLASS)); - assertNull(response.getHeader(X_WOODY_ERROR_REASON)); - } - - @Test - void shouldFallbackToLightweightModeWhenDisabled() throws Exception { - configureFilter(RequestHeaderMode.OFF, ResponseHeaderMode.OFF, null); - - final var request = new MockHttpServletRequest("GET", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - - filter.doFilter(request, response, new MockFilterChain()); - - assertNull(response.getHeader(WOODY_TRACE_ID)); - } - - @Test - void shouldApplyCustomEndpointConfiguration() throws Exception { - tracingProperties.getEndpoints().clear(); - var endpoint = new Endpoint(); - endpoint.setPort(8080); - endpoint.setPath("/custom"); - endpoint.setRequestHeaderMode(RequestHeaderMode.WOODY_OR_X_WOODY); - endpoint.setResponseHeaderMode(ResponseHeaderMode.WOODY); - tracingProperties.getEndpoints().add(endpoint); - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.WOODY, null); - - final var request = new MockHttpServletRequest("POST", "/custom"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(X_WOODY_TRACE_ID, "custom-trace"); - request.addHeader(X_WOODY_SPAN_ID, "custom-span"); - - filter.doFilter(request, response, new MockFilterChain()); - - assertEquals("custom-trace", response.getHeader(WOODY_TRACE_ID)); - } - - @Test - void shouldSkipWhenEndpointDoesNotMatchConfiguration() throws Exception { - tracingProperties.getEndpoints().clear(); - var endpoint = new Endpoint(); - endpoint.setPort(9090); - endpoint.setPath("/other"); - endpoint.setRequestHeaderMode(RequestHeaderMode.WOODY_OR_X_WOODY); - endpoint.setResponseHeaderMode(ResponseHeaderMode.OFF); - tracingProperties.getEndpoints().add(endpoint); - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, null); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(X_WOODY_TRACE_ID, "trace-skip"); - request.addHeader(X_WOODY_SPAN_ID, "span-skip"); - - filter.doFilter(request, response, new MockFilterChain()); - - assertNull(response.getHeader(WOODY_TRACE_ID)); - assertNull(response.getHeader(X_WOODY_TRACE_ID)); - } - - @Test - void shouldRespectWoodyModeExplicitly() throws Exception { - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.WOODY, null); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(X_WOODY_TRACE_ID, "woody-trace-only"); - request.addHeader(X_WOODY_SPAN_ID, "woody-span-only"); - - filter.doFilter(request, response, new MockFilterChain()); - - assertEquals("woody-trace-only", response.getHeader(WOODY_TRACE_ID)); - assertNull(response.getHeader(X_WOODY_TRACE_ID)); - } - - @Test - void shouldExposeHttpHeadersWhenConfigured() throws Exception { - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.HTTP, null); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(OTEL_TRACE_PARENT, "00-11111111111111111111111111111111-2222222222222222-01"); - request.addHeader(X_REQUEST_ID, "request-123"); - request.addHeader(X_REQUEST_DEADLINE, "2025-10-14T06:00:00Z"); - - filter.doFilter(request, response, new MockFilterChain()); - - assertEquals("00-11111111111111111111111111111111-2222222222222222-01", - response.getHeader(OTEL_TRACE_PARENT)); - assertEquals("request-123", response.getHeader(X_REQUEST_ID)); - assertEquals("2025-10-14T06:00:00Z", response.getHeader(X_REQUEST_DEADLINE)); - assertNull(response.getHeader(WOODY_TRACE_ID)); - assertNull(response.getHeader(X_WOODY_TRACE_ID)); - } - - @Test - void shouldExposeHttpErrorsWhenConfigured() throws Exception { - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.HTTP, null); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(X_WOODY_TRACE_ID, "trace-err"); - request.addHeader(X_WOODY_SPAN_ID, "span-err"); - - var errorDefinition = new WErrorDefinition(WErrorSource.INTERNAL); - errorDefinition.setErrorType(WErrorType.UNEXPECTED_ERROR); - errorDefinition.setErrorReason("http-boom"); - - filter.doFilter(request, response, (req, res) -> { - throw new WRuntimeException(errorDefinition); - }); - - assertEquals(500, response.getStatus()); - assertEquals(WErrorType.UNEXPECTED_ERROR.getKey(), response.getHeader(X_ERROR_CLASS)); - assertEquals("http-boom", response.getHeader(X_ERROR_REASON)); - assertNull(response.getHeader(WOODY_ERROR_CLASS)); - assertNull(response.getHeader(WOODY_ERROR_REASON)); - } - - @Test - void shouldReturnNoHeadersWhenOffMode() throws Exception { - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, null); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - request.addHeader(X_WOODY_TRACE_ID, "trace-off"); - request.addHeader(X_WOODY_SPAN_ID, "span-off"); - - filter.doFilter(request, response, new MockFilterChain()); - - assertNull(response.getHeader(WOODY_TRACE_ID)); - assertNull(response.getHeader(X_WOODY_TRACE_ID)); - assertNull(response.getHeader(OTEL_TRACE_PARENT)); - } - - @Test - void shouldPropagateErrorsWhenOffModeByDefault() { - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, null); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - - var errorDefinition = new WErrorDefinition(WErrorSource.INTERNAL); - errorDefinition.setErrorType(WErrorType.UNEXPECTED_ERROR); - errorDefinition.setErrorReason("propagate"); - - assertThrows(WRuntimeException.class, () -> filter.doFilter(request, response, (req, res) -> { - throw new WRuntimeException(errorDefinition); - })); - assertEquals(200, response.getStatus()); - } - - @Test - void shouldAllowDisablingErrorPropagationExplicitly() throws Exception { - configureFilter(RequestHeaderMode.WOODY_OR_X_WOODY, ResponseHeaderMode.OFF, false); - - final var request = new MockHttpServletRequest("POST", "/wachter"); - final var response = new MockHttpServletResponse(); - request.setLocalPort(8080); - - var errorDefinition = new WErrorDefinition(WErrorSource.INTERNAL); - errorDefinition.setErrorType(WErrorType.UNEXPECTED_ERROR); - errorDefinition.setErrorReason("handled"); - - filter.doFilter(request, response, (req, res) -> { - throw new WRuntimeException(errorDefinition); - }); - - assertEquals(500, response.getStatus()); - assertNull(response.getHeader(WOODY_TRACE_ID)); - assertNull(response.getHeader(X_WOODY_TRACE_ID)); - } - - private void configureFilter(RequestHeaderMode requestHeaderMode, - ResponseHeaderMode responseHeaderMode, - Boolean propagateErrors) { - var defaultEndpoint = tracingProperties.getEndpoints().stream() - .filter(this::matchesDefault) - .findFirst() - .orElseGet(() -> { - var endpoint = defaultEndpoint(); - tracingProperties.getEndpoints().add(endpoint); - return endpoint; - }); - defaultEndpoint.setRequestHeaderMode(requestHeaderMode); - defaultEndpoint.setResponseHeaderMode(responseHeaderMode); - defaultEndpoint.setPropagateErrors(propagateErrors); - rebuildFilter(); - } - - private Endpoint defaultEndpoint() { - var endpoint = new Endpoint(); - endpoint.setPort(8080); - endpoint.setPath("/wachter"); - return endpoint; - } - - private boolean matchesDefault(Endpoint endpoint) { - var portMatches = endpoint.getPort() == null || endpoint.getPort() == 8080; - var pathMatches = endpoint.getPath() == null || endpoint.getPath().equals("/wachter"); - return portMatches && pathMatches; - } - - private void rebuildFilter() { - var lifecycleHandler = new WoodyTraceResponseHandler(); - filter = new WoodyTracingFilter(tracingProperties, lifecycleHandler); - } -} From 30389df35095bf08387acbe185182b3195aae2b3 Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Tue, 14 Oct 2025 19:47:26 +0700 Subject: [PATCH 12/14] refactor woody.http.bridge --- .../client/WachterClientOperationsTest.java | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/test/java/dev/vality/wachter/client/WachterClientOperationsTest.java b/src/test/java/dev/vality/wachter/client/WachterClientOperationsTest.java index 2cf2b6e..0c856e3 100644 --- a/src/test/java/dev/vality/wachter/client/WachterClientOperationsTest.java +++ b/src/test/java/dev/vality/wachter/client/WachterClientOperationsTest.java @@ -3,8 +3,9 @@ import dev.vality.woody.api.trace.TraceData; import dev.vality.woody.api.trace.context.TraceContext; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.trace.SdkTracerProvider; @@ -29,7 +30,6 @@ class WachterClientOperationsTest { private SdkTracerProvider tracerProvider; - private Tracer tracer; @BeforeEach void setUp() { @@ -40,7 +40,6 @@ void setUp() { .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .build(); GlobalOpenTelemetry.set(openTelemetry); - tracer = openTelemetry.getTracer("test"); } @AfterEach @@ -58,10 +57,7 @@ void shouldSendRequestWithTracingHeaders() { final var server = MockRestServiceServer.bindTo(builder).build(); final var restClient = builder.build(); - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); + final var traceData = prepareTraceData("test-span"); final var serviceSpan = traceData.getServiceSpan().getSpan(); serviceSpan.setTraceId("test-trace-id"); @@ -91,7 +87,7 @@ void shouldSendRequestWithTracingHeaders() { assertEquals(HttpStatus.OK, actualResponse.statusCode()); assertArrayEquals(expectedResponse, actualResponse.body()); server.verify(); - otelSpan.end(); + traceData.finishOtelSpan(); } @Test @@ -100,10 +96,7 @@ void shouldFilterDisallowedHeaders() { final var server = MockRestServiceServer.bindTo(builder).build(); final var restClient = builder.build(); - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); + final var traceData = prepareTraceData("test-span"); traceData.getServiceSpan().getSpan().setTraceId("filter-trace-id"); traceData.getServiceSpan().getSpan().setId("filter-span-id"); @@ -127,7 +120,7 @@ void shouldFilterDisallowedHeaders() { client.send(servletRequest, null, "http://upstream/disallowed"); server.verify(); - otelSpan.end(); + traceData.finishOtelSpan(); } @Test @@ -136,10 +129,7 @@ void shouldHandleGetRequestWithoutBody() { final var server = MockRestServiceServer.bindTo(builder).build(); final var restClient = builder.build(); - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); + final var traceData = prepareTraceData("test-span"); final var serviceSpan = traceData.getServiceSpan().getSpan(); serviceSpan.setTraceId("get-trace-id"); @@ -158,7 +148,7 @@ void shouldHandleGetRequestWithoutBody() { assertEquals(HttpStatus.OK, response.statusCode()); assertArrayEquals("{}".getBytes(), response.body()); server.verify(); - otelSpan.end(); + traceData.finishOtelSpan(); } @Test @@ -167,10 +157,7 @@ void shouldReturnErrorResponseWithoutThrowing() { final var server = MockRestServiceServer.bindTo(builder).build(); final var restClient = builder.build(); - final var traceData = new TraceData(); - final var otelSpan = tracer.spanBuilder("test-span").startSpan(); - traceData.setOtelSpan(otelSpan); - TraceContext.setCurrentTraceData(traceData); + final var traceData = prepareTraceData("test-span"); final var serviceSpan = traceData.getServiceSpan().getSpan(); serviceSpan.setTraceId("error-trace-id"); @@ -192,6 +179,14 @@ void shouldReturnErrorResponseWithoutThrowing() { assertEquals(HttpStatus.BAD_GATEWAY, response.statusCode()); assertArrayEquals("bad-gateway".getBytes(), response.body()); server.verify(); - otelSpan.end(); + traceData.finishOtelSpan(); + } + + private TraceData prepareTraceData(String spanName) { + final var traceData = new TraceData(); + traceData.startNewOtelSpan(spanName, SpanKind.SERVER, Context.current()); + traceData.openOtelScope(); + TraceContext.setCurrentTraceData(traceData); + return traceData; } } From 5d38545ca09c24d5783849724cb12a3cc4d97225 Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Wed, 15 Oct 2025 12:57:46 +0700 Subject: [PATCH 13/14] refactor woody.http.bridge --- pom.xml | 8 +---- .../properties/HttpClientProperties.java | 32 ----------------- .../config/properties/WachterProperties.java | 1 - src/main/resources/application.yml | 34 +++++++------------ 4 files changed, 13 insertions(+), 62 deletions(-) delete mode 100644 src/main/java/dev/vality/wachter/config/properties/HttpClientProperties.java diff --git a/pom.xml b/pom.xml index 209da08..098eed1 100644 --- a/pom.xml +++ b/pom.xml @@ -15,8 +15,7 @@ 2.0.12 - 2.15.0 - 0.0.1 + 0.0.4 UTF-8 UTF-8 21 @@ -112,11 +111,6 @@ spring-boot-starter-test test - - io.opentelemetry - opentelemetry-sdk-testing - test - io.jsonwebtoken jjwt-api diff --git a/src/main/java/dev/vality/wachter/config/properties/HttpClientProperties.java b/src/main/java/dev/vality/wachter/config/properties/HttpClientProperties.java deleted file mode 100644 index be4695f..0000000 --- a/src/main/java/dev/vality/wachter/config/properties/HttpClientProperties.java +++ /dev/null @@ -1,32 +0,0 @@ -package dev.vality.wachter.config.properties; - -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.validation.annotation.Validated; - -@Getter -@Setter -@Validated -@Configuration -@ConfigurationProperties("http-client") -public class HttpClientProperties { - - @NotNull - private int maxTotalPooling; - - @NotNull - private int defaultMaxPerRoute; - - @NotNull - private int socketTimeout; - - @NotNull - private int connectionRequestTimeout; - - @NotNull - private int connectTimeout; - -} diff --git a/src/main/java/dev/vality/wachter/config/properties/WachterProperties.java b/src/main/java/dev/vality/wachter/config/properties/WachterProperties.java index 80af146..6aece91 100644 --- a/src/main/java/dev/vality/wachter/config/properties/WachterProperties.java +++ b/src/main/java/dev/vality/wachter/config/properties/WachterProperties.java @@ -27,5 +27,4 @@ public static class Service { private String url; } - } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6136103..de40fc7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,7 +7,7 @@ management: port: ${management.port} metrics: tags: - application: '@project.name@' + application: ${project.name} endpoint: health: show-details: always @@ -26,7 +26,7 @@ management: spring: application: - name: '@project.name@' + name: ${project.name} output: ansi: enabled: always @@ -39,17 +39,6 @@ spring: issuer-uri: > ${spring.security.oauth2.resourceserver.url}/auth/realms/ ${spring.security.oauth2.resourceserver.jwt.realm} -info: - version: '@project.version@' - stage: dev - -woody-http-bridge: - tracing: - endpoints: - - path: /wachter - port: ${server.port} - request-header-mode: WOODY_OR_X_WOODY - response-header-mode: OFF wachter: auth: @@ -135,20 +124,21 @@ wachter: name: DMTClient url: http://dmt.default:8022/v1/domain/repository_client +auth: + enabled: true -http-client: - connectTimeout: 10000 - connectionRequestTimeout: 10000 - socketTimeout: 10000 - maxTotalPooling: 200 - defaultMaxPerRoute: 200 - -auth.enabled: true +woody-http-bridge: + tracing: + endpoints: + - path: /wachter + port: ${server.port} + request-header-mode: WOODY_OR_X_WOODY + response-header-mode: OFF otel: + enabled: true resource: http://localhost:4318/v1/traces timeout: 60000 - enabled: true http: requestTimeout: 60000 From 86dfffedcbcb5125e2101e118fadf0c25e670f0b Mon Sep 17 00:00:00 2001 From: Anatoly Karlov Date: Wed, 15 Oct 2025 16:50:49 +0700 Subject: [PATCH 14/14] refactor woody.http.bridge --- pom.xml | 2 +- src/main/java/dev/vality/wachter/client/WachterClient.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 098eed1..26e8fb9 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ 2.0.12 - 0.0.4 + 0.0.8 UTF-8 UTF-8 21 diff --git a/src/main/java/dev/vality/wachter/client/WachterClient.java b/src/main/java/dev/vality/wachter/client/WachterClient.java index 2aa1499..804bb37 100644 --- a/src/main/java/dev/vality/wachter/client/WachterClient.java +++ b/src/main/java/dev/vality/wachter/client/WachterClient.java @@ -1,6 +1,6 @@ package dev.vality.wachter.client; -import dev.vality.woody.http.bridge.tracing.TraceContextHeadersExtractor; +import dev.vality.woody.http.bridge.tracing.TraceContextExtractor; import dev.vality.woody.http.bridge.tracing.TraceContextHeadersNormalizer; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -27,7 +27,7 @@ public WachterClientResponse send(HttpServletRequest servletRequest, byte[] cont var httpMethod = resolveMethod(servletRequest); var proxyHeaders = ProxyHeadersExtractor.extractHeaders(servletRequest); - var traceHeaders = TraceContextHeadersExtractor.extractHeaders(); + var traceHeaders = TraceContextExtractor.extractHeaders(); var httpHeaders = new HttpHeaders(); proxyHeaders.forEach(httpHeaders::addAll);