diff --git a/rest-client-base/pom.xml b/rest-client-base/pom.xml index 8314aa9..c62c94b 100644 --- a/rest-client-base/pom.xml +++ b/rest-client-base/pom.xml @@ -61,6 +61,12 @@ netty-http-authenticator 1.5 + + + io.opentelemetry + opentelemetry-api + + diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java index e89b7e0..1a1741a 100644 --- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java +++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java @@ -212,6 +212,7 @@ private void initializeWebClient() throws RestClientException { }); }); } + builder.filter(TraceparentFilterFunction.handleTraceparentContext()); final ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); webClient = builder.baseUrl(config.getBaseUrl()).clientConnector(connector).build(); diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/TraceparentFilterFunction.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/TraceparentFilterFunction.java new file mode 100644 index 0000000..46a501e --- /dev/null +++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/TraceparentFilterFunction.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.wultra.core.rest.client.base; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import reactor.core.publisher.Mono; + +/** + * An ExchangeFilterFunction for adding traceparent header to WebClient requests. + * It also propagates trace ID through Reactor context and MDC for logging purposes. + * + * @author Jan Dusil, jan.dusil@wultra.com + */ +public class TraceparentFilterFunction implements ExchangeFilterFunction { + + private static final String TRACEPARENT_HEADER_KEY = "traceparent"; + + /** + * Applies the filter to the given ClientRequest and ExchangeFunction. + * + * @param request The client request. + * @param next The next exchange function in the chain. + * @return a Mono after applying the filter. + */ + @Override + public Mono filter(final ClientRequest request, final ExchangeFunction next) { + return next.exchange(addTraceparentHeader(request)); + } + + /** + * Adds a traceparent header to the ClientRequest if a current span context is available. + * + * @param request The client request. + * @return a modified ClientRequest with the traceparent header added. + */ + private ClientRequest addTraceparentHeader(final ClientRequest request) { + final Span currentSpan = Span.current(); + if (currentSpan != null) { + final SpanContext spanContext = currentSpan.getSpanContext(); + if (spanContext != null) { + final String traceId = spanContext.getTraceId(); + /* The parentId of the next server Span will be the current spanId */ + final String parentId = spanContext.getSpanId(); + final TraceFlags traceFlags = spanContext.getTraceFlags(); + if (traceId != null && parentId != null && traceFlags != null) { + final String headerValue = String.format("00-%s-%s-%s", + traceId, + parentId, + traceFlags.asHex()); + return ClientRequest.from(request) + .headers(headers -> headers.set(TRACEPARENT_HEADER_KEY, headerValue)) + .build(); + } + } + } + return request; + } + + /** + * Factory method to create an instance of TraceparentFilterFunction. + * + * @return a new instance of TraceparentFilterFunction. + */ + public static ExchangeFilterFunction handleTraceparentContext() { + return new TraceparentFilterFunction(); + } +}