diff --git a/gradle.properties b/gradle.properties index 787c7252d1..31bc451773 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,6 +54,7 @@ jacksonVersion=2.14.3 openTracingVersion=0.33.0 zipkinReporterVersion=2.16.4 opentelemetryVersion=1.28.0 +opentelemetryApiVersion=1.28.0-alpha # gRPC protobufGradlePluginVersion=0.9.4 @@ -73,7 +74,6 @@ assertJCoreVersion=3.24.2 hamcrestVersion=2.2 mockitoCoreVersion=4.11.0 spotbugsPluginVersion=5.0.13 -opentelemetryInstrumentationVersion=1.9.2-alpha apacheDirectoryServerVersion=1.5.7 commonsLangVersion=2.6 diff --git a/servicetalk-opentelemetry-http/build.gradle b/servicetalk-opentelemetry-http/build.gradle index c131c41eb7..cc77bb4a7d 100755 --- a/servicetalk-opentelemetry-http/build.gradle +++ b/servicetalk-opentelemetry-http/build.gradle @@ -24,6 +24,7 @@ dependencies { api project(":servicetalk-http-api") api "io.opentelemetry:opentelemetry-api:$opentelemetryVersion" + implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv:$opentelemetryApiVersion") implementation project(":servicetalk-annotations") implementation project(":servicetalk-http-utils") @@ -36,7 +37,8 @@ dependencies { testImplementation project(":servicetalk-http-netty") testImplementation project(":servicetalk-test-resources") testImplementation "io.opentelemetry:opentelemetry-sdk-testing:$opentelemetryVersion" - testImplementation "io.opentelemetry.instrumentation:opentelemetry-log4j-2.13.2:$opentelemetryInstrumentationVersion" + testRuntimeOnly("io.opentelemetry.instrumentation:opentelemetry-log4j-context-data-2.17-autoconfigure:" + + "$opentelemetryApiVersion") testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.assertj:assertj-core:$assertJCoreVersion" testImplementation "org.mockito:mockito-core:$mockitoCoreVersion" diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpRequestFilter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpRequestFilter.java index c95aae4f9d..99029467d1 100755 --- a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpRequestFilter.java +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpRequestFilter.java @@ -20,6 +20,8 @@ import io.servicetalk.concurrent.api.Single; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.FilterableStreamingHttpConnection; +import io.servicetalk.http.api.HttpRequestMetaData; +import io.servicetalk.http.api.HttpResponseMetaData; import io.servicetalk.http.api.StreamingHttpClientFilter; import io.servicetalk.http.api.StreamingHttpClientFilterFactory; import io.servicetalk.http.api.StreamingHttpConnectionFilter; @@ -27,15 +29,22 @@ import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpRequester; import io.servicetalk.http.api.StreamingHttpResponse; -import io.servicetalk.transport.api.HostAndPort; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.util.function.UnaryOperator; @@ -54,17 +63,42 @@ public final class OpenTelemetryHttpRequestFilter extends AbstractOpenTelemetryFilter implements StreamingHttpClientFilterFactory, StreamingHttpConnectionFilterFactory { - private final String componentName; + private final Instrumenter instrumenter; /** * Create a new instance. * - * @param openTelemetry the {@link OpenTelemetry}. - * @param componentName The component name used during building new spans. + * @param openTelemetry the {@link OpenTelemetry}. + * @param componentName The component name used during building new spans. + * @param opentelemetryOptions extra options to create the opentelemetry filter. */ - public OpenTelemetryHttpRequestFilter(final OpenTelemetry openTelemetry, String componentName) { + public OpenTelemetryHttpRequestFilter(final OpenTelemetry openTelemetry, String componentName, + OpentelemetryOptions opentelemetryOptions) { super(openTelemetry); - this.componentName = componentName.trim(); + SpanNameExtractor serverSpanNameExtractor = + HttpSpanNameExtractor.create(ServicetalkHttpClientCommonAttributesGetter.INSTANCE); + InstrumenterBuilder clientInstrumenterBuilder = + Instrumenter.builder(openTelemetry, INSTRUMENTATION_SCOPE_NAME, serverSpanNameExtractor); + clientInstrumenterBuilder.setSpanStatusExtractor(ServicetalkSpanStatusExtractor.INSTANCE); + + clientInstrumenterBuilder + .addAttributesExtractor(HttpClientAttributesExtractor + .builder(ServicetalkHttpClientCommonAttributesGetter.INSTANCE, + ServicetalkNetClientAttributesGetter.INSTANCE) + .setCapturedRequestHeaders(opentelemetryOptions.getCaptureRequestHeaders()) + .setCapturedResponseHeaders(opentelemetryOptions.getCaptureResponseHeaders()) + .build()) + .addAttributesExtractor( + NetClientAttributesExtractor.create(ServicetalkNetClientAttributesGetter.INSTANCE)); + if (opentelemetryOptions.isEnableMetrics()) { + clientInstrumenterBuilder.addOperationMetrics(HttpClientMetrics.get()); + } + if (!componentName.trim().isEmpty()) { + clientInstrumenterBuilder.addAttributesExtractor( + AttributesExtractor.constant(SemanticAttributes.PEER_SERVICE, componentName)); + } + instrumenter = + clientInstrumenterBuilder.buildClientInstrumenter(RequestHeadersPropagatorSetter.INSTANCE); } /** @@ -73,7 +107,27 @@ public OpenTelemetryHttpRequestFilter(final OpenTelemetry openTelemetry, String * @param componentName The component name used during building new spans. */ public OpenTelemetryHttpRequestFilter(String componentName) { - this(GlobalOpenTelemetry.get(), componentName); + this(GlobalOpenTelemetry.get(), componentName, OpentelemetryOptions.newBuilder().build()); + } + + /** + * Create a new instance, searching for any instance of an opentelemetry available. + * + * @param openTelemetry the {@link OpenTelemetry}. + * @param componentName The component name used during building new spans. + */ + public OpenTelemetryHttpRequestFilter(final OpenTelemetry openTelemetry, String componentName) { + this(openTelemetry, componentName, OpentelemetryOptions.newBuilder().build()); + } + + /** + * Create a new instance, searching for any instance of an opentelemetry available. + * + * @param componentName The component name used during building new spans. + * @param opentelemetryOptions extra options to create the opentelemetry filter + */ + public OpenTelemetryHttpRequestFilter(String componentName, OpentelemetryOptions opentelemetryOptions) { + this(GlobalOpenTelemetry.get(), componentName, opentelemetryOptions); } /** @@ -81,7 +135,7 @@ public OpenTelemetryHttpRequestFilter(String componentName) { * using the hostname as the component name. */ public OpenTelemetryHttpRequestFilter() { - this(GlobalOpenTelemetry.get(), ""); + this(GlobalOpenTelemetry.get(), "", OpentelemetryOptions.newBuilder().build()); } @Override @@ -108,18 +162,13 @@ public Single request(final StreamingHttpRequest request) private Single trackRequest(final StreamingHttpRequester delegate, final StreamingHttpRequest request) { - Context context = Context.current(); - final Span span = RequestTagExtractor.reportTagsAndStart(tracer - .spanBuilder(getSpanName(request)) - .setParent(context) - .setSpanKind(SpanKind.CLIENT), request); - - final Scope scope = span.makeCurrent(); - final ScopeTracker tracker = new ScopeTracker(scope, span); + final Context parentContext = Context.current(); + Context context = instrumenter.start(parentContext, request); + + final Scope scope = context.makeCurrent(); + final ScopeTracker tracker = new ScopeTracker(scope, context, request, instrumenter); Single response; try { - propagators.getTextMapPropagator().inject(Context.current(), request.headers(), - HeadersPropagatorSetter.INSTANCE); response = delegate.request(request); } catch (Throwable t) { tracker.onError(t); @@ -127,15 +176,4 @@ private Single trackRequest(final StreamingHttpRequester } return tracker.track(response); } - - private String getSpanName(StreamingHttpRequest request) { - if (!componentName.isEmpty()) { - return componentName; - } - HostAndPort hostAndPort = request.effectiveHostAndPort(); - if (hostAndPort != null) { - return hostAndPort.hostName(); - } - return request.requestTarget(); - } } diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpServerFilter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpServerFilter.java index a425be810f..b97ccda49d 100755 --- a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpServerFilter.java +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpServerFilter.java @@ -19,6 +19,7 @@ import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; import io.servicetalk.http.api.HttpRequestMetaData; +import io.servicetalk.http.api.HttpResponseMetaData; import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; @@ -30,10 +31,16 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesExtractor; import java.util.function.UnaryOperator; @@ -52,21 +59,62 @@ */ public final class OpenTelemetryHttpServerFilter extends AbstractOpenTelemetryFilter implements StreamingHttpServiceFilterFactory { + private final Instrumenter instrumenter; /** * Create a new instance. * - * @param openTelemetry the {@link OpenTelemetry}. + * @param openTelemetry the {@link OpenTelemetry}. + * @param opentelemetryOptions extra options to create the opentelemetry filter. */ - public OpenTelemetryHttpServerFilter(final OpenTelemetry openTelemetry) { + public OpenTelemetryHttpServerFilter(final OpenTelemetry openTelemetry, OpentelemetryOptions opentelemetryOptions) { super(openTelemetry); + SpanNameExtractor serverSpanNameExtractor = + HttpSpanNameExtractor.create(ServicetalkHttpServerCommonAttributesGetter.INSTANCE); + InstrumenterBuilder serverInstrumenterBuilder = + Instrumenter.builder(openTelemetry, INSTRUMENTATION_SCOPE_NAME, serverSpanNameExtractor); + serverInstrumenterBuilder.setSpanStatusExtractor(ServicetalkSpanStatusExtractor.INSTANCE); + + serverInstrumenterBuilder + .addAttributesExtractor(HttpServerAttributesExtractor + .builder(ServicetalkHttpServerCommonAttributesGetter.INSTANCE, + ServicetalkNetServerAttributesGetter.INSTANCE) + .setCapturedRequestHeaders(opentelemetryOptions.getCaptureRequestHeaders()) + .setCapturedResponseHeaders(opentelemetryOptions.getCaptureResponseHeaders()) + .build()) + .addAttributesExtractor( + NetServerAttributesExtractor.create(ServicetalkNetServerAttributesGetter.INSTANCE)); + if (opentelemetryOptions.isEnableMetrics()) { + serverInstrumenterBuilder.addOperationMetrics(HttpServerMetrics.get()); + } + + instrumenter = + serverInstrumenterBuilder.buildServerInstrumenter(RequestHeadersPropagatorGetter.INSTANCE); } /** * Create a new Instance, searching for any instance of an opentelemetry available. */ public OpenTelemetryHttpServerFilter() { - this(GlobalOpenTelemetry.get()); + this(GlobalOpenTelemetry.get(), OpentelemetryOptions.newBuilder().build()); + } + + /** + * Create a new instance. + * + * @param opentelemetryOptions extra options to create the opentelemetry filter + */ + public OpenTelemetryHttpServerFilter(OpentelemetryOptions opentelemetryOptions) { + this(GlobalOpenTelemetry.get(), opentelemetryOptions); + } + + /** + * Create a new instance. + * + * @param openTelemetry the {@link OpenTelemetry}. + */ + public OpenTelemetryHttpServerFilter(final OpenTelemetry openTelemetry) { + this(openTelemetry, OpentelemetryOptions.newBuilder().build()); } @Override @@ -85,26 +133,15 @@ private Single trackRequest(final StreamingHttpService de final HttpServiceContext ctx, final StreamingHttpRequest request, final StreamingHttpResponseFactory responseFactory) { - final Context context = Context.root(); - io.opentelemetry.context.Context tracingContext = - propagators.getTextMapPropagator().extract(context, request.headers(), HeadersPropagatorGetter.INSTANCE); - final Span span = RequestTagExtractor.reportTagsAndStart(tracer - .spanBuilder(getOperationName(request)) - .setParent(tracingContext) - .setSpanKind(SpanKind.SERVER), request); + final Context parentContext = Context.current(); + if (!instrumenter.shouldStart(parentContext, request)) { + return delegate.handle(ctx, request, responseFactory); + } + Context context = instrumenter.start(parentContext, request); - final Scope scope = span.makeCurrent(); - final ScopeTracker tracker = new ScopeTracker(scope, span) { - @Override - protected void tagStatusCode() { - super.tagStatusCode(); - if (metaData != null) { - propagators.getTextMapPropagator().inject(Context.current(), metaData.headers(), - HeadersPropagatorSetter.INSTANCE); - } - } - }; + final Scope scope = context.makeCurrent(); + final ScopeTracker tracker = new ScopeTracker(scope, context, request, instrumenter); Single response; try { response = delegate.handle(ctx, request, responseFactory); @@ -114,14 +151,4 @@ protected void tagStatusCode() { } return tracker.track(response); } - - /** - * Get the operation name to build the span with. - * - * @param metaData The {@link HttpRequestMetaData}. - * @return the operation name to build the span with. - */ - private static String getOperationName(HttpRequestMetaData metaData) { - return metaData.method().name() + ' ' + metaData.path(); - } } diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpentelemetryOptions.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpentelemetryOptions.java new file mode 100644 index 0000000000..5ae3245f5a --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpentelemetryOptions.java @@ -0,0 +1,97 @@ +/* + * Copyright © 2023 Apple Inc. and the ServiceTalk project authors + * + * 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 io.servicetalk.opentelemetry.http; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A set of options for creating the opentelemetry filter. + */ +public class OpentelemetryOptions { + + private final List captureRequestHeaders; + private final List captureResponseHeaders; + private final boolean enableMetrics; + + OpentelemetryOptions(List captureRequestHeaders, List captureResponseHeaders, + boolean enableMetrics) { + this.captureRequestHeaders = captureRequestHeaders; + this.captureResponseHeaders = captureResponseHeaders; + this.enableMetrics = enableMetrics; + } + + static Builder newBuilder() { + return new Builder(); + } + + public List getCaptureRequestHeaders() { + return Collections.unmodifiableList(captureRequestHeaders); + } + + public List getCaptureResponseHeaders() { + return Collections.unmodifiableList(captureResponseHeaders); + } + + public boolean isEnableMetrics() { + return enableMetrics; + } + + /** + * a Builder of {@link OpentelemetryOptions}. + */ + public static class Builder { + private List captureRequestHeaders = new ArrayList<>(); + private List captureResponseHeaders = new ArrayList<>(); + private boolean enableMetrics; + + /** + * set the headers to be captured as extra tags. + * @param captureRequestHeaders extra headers to be captured in client/server requests and added as tags. + * @return an instance of itself + */ + public OpentelemetryOptions.Builder setCaptureRequestHeaders(List captureRequestHeaders) { + this.captureRequestHeaders.addAll(captureRequestHeaders); + return this; + } + + /** + * set the headers to be captured as extra tags. + * @param captureResponseHeaders extra headers to be captured in client/server response and added as tags. + * @return an instance of itself + */ + public OpentelemetryOptions.Builder setCaptureResponseHeaders(List captureResponseHeaders) { + this.captureResponseHeaders.addAll(captureResponseHeaders); + return this; + } + + /** + * whether to enable span metrics. + * @param enableMetrics whether to enable opentelemetry metrics. + * @return an instance of itself + */ + public OpentelemetryOptions.Builder setEnableMetrics(boolean enableMetrics) { + this.enableMetrics = enableMetrics; + return this; + } + + public OpentelemetryOptions build() { + return new OpentelemetryOptions(captureRequestHeaders, captureResponseHeaders, enableMetrics); + } + } +} diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestHeadersPropagatorGetter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestHeadersPropagatorGetter.java new file mode 100755 index 0000000000..1ee1ed7195 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestHeadersPropagatorGetter.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project authors + * + * 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 io.servicetalk.opentelemetry.http; + +import io.servicetalk.http.api.HttpRequestMetaData; + +import io.opentelemetry.context.propagation.TextMapGetter; + +import javax.annotation.Nullable; + +final class RequestHeadersPropagatorGetter implements TextMapGetter { + + static final TextMapGetter INSTANCE = new RequestHeadersPropagatorGetter(); + + private RequestHeadersPropagatorGetter() { + } + + @Override + public Iterable keys(final HttpRequestMetaData carrier) { + return HeadersPropagatorGetter.INSTANCE.keys(carrier.headers()); + } + + @Override + @Nullable + public String get(@Nullable HttpRequestMetaData carrier, final String key) { + if (carrier == null) { + return null; + } + return HeadersPropagatorGetter.INSTANCE.get(carrier.headers(), key); + } +} diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ResponseTagExtractor.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestHeadersPropagatorSetter.java old mode 100644 new mode 100755 similarity index 52% rename from servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ResponseTagExtractor.java rename to servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestHeadersPropagatorSetter.java index f2a4ca7d08..165de1a602 --- a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ResponseTagExtractor.java +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestHeadersPropagatorSetter.java @@ -16,15 +16,23 @@ package io.servicetalk.opentelemetry.http; -import io.servicetalk.http.api.HttpResponseMetaData; +import io.servicetalk.http.api.HttpRequestMetaData; -import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.propagation.TextMapSetter; -final class ResponseTagExtractor { +import javax.annotation.Nullable; - public static final ResponseTagExtractor INSTANCE = new ResponseTagExtractor(); +final class RequestHeadersPropagatorSetter implements TextMapSetter { - void extract(HttpResponseMetaData responseMetaData, Span span) { - span.setAttribute("http.status_code", responseMetaData.status().code()); + static final TextMapSetter INSTANCE = new RequestHeadersPropagatorSetter(); + + private RequestHeadersPropagatorSetter() { + } + + @Override + public void set(@Nullable final HttpRequestMetaData headers, final String key, final String value) { + if (headers != null) { + HeadersPropagatorSetter.INSTANCE.set(headers.headers(), key, value); + } } } diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestTagExtractor.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestTagExtractor.java deleted file mode 100644 index d2c3612cf0..0000000000 --- a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestTagExtractor.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright © 2022 Apple Inc. and the ServiceTalk project authors - * - * 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 io.servicetalk.opentelemetry.http; - -import io.servicetalk.http.api.HttpProtocolVersion; -import io.servicetalk.http.api.HttpRequestMetaData; -import io.servicetalk.transport.api.HostAndPort; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanBuilder; - -import static io.servicetalk.http.api.HttpHeaderNames.USER_AGENT; - -final class RequestTagExtractor { - - private RequestTagExtractor() { - // empty private constructor - } - - private static String getRequestMethod(HttpRequestMetaData req) { - return req.method().name(); - } - - private static String getHttpUrl(HttpRequestMetaData req) { - return req.path() - + (req.rawQuery() == null ? "" : '?' + req.rawQuery()); - } - - static Span reportTagsAndStart(SpanBuilder span, HttpRequestMetaData httpRequestMetaData) { - span.setAttribute("http.url", getHttpUrl(httpRequestMetaData)); - span.setAttribute("http.method", getRequestMethod(httpRequestMetaData)); - span.setAttribute("http.target", getHttpUrl(httpRequestMetaData)); - span.setAttribute("http.route", httpRequestMetaData.rawPath()); - span.setAttribute("http.flavor", getFlavor(httpRequestMetaData.version())); - CharSequence userAgent = httpRequestMetaData.headers().get(USER_AGENT); - if (userAgent != null) { - span.setAttribute("http.user_agent", userAgent.toString()); - } - String scheme = httpRequestMetaData.scheme(); - if (scheme != null) { - span.setAttribute("http.scheme", scheme); - } - HostAndPort hostAndPort = httpRequestMetaData.effectiveHostAndPort(); - if (hostAndPort != null) { - span.setAttribute("net.host.name", hostAndPort.hostName()); - span.setAttribute("net.host.port", hostAndPort.port()); - } - return span.startSpan(); - } - - private static String getFlavor(final HttpProtocolVersion version) { - if (version.major() == 1) { - if (version.minor() == 1) { - return "1.1"; - } - if (version.minor() == 0) { - return "1.0"; - } - } else if (version.major() == 2 && version.minor() == 0) { - return "2.0"; - } - return version.major() + "." + version.minor(); - } -} diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ScopeTracker.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ScopeTracker.java index 1171898343..67a61c391b 100755 --- a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ScopeTracker.java +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ScopeTracker.java @@ -18,30 +18,36 @@ import io.servicetalk.concurrent.api.Single; import io.servicetalk.concurrent.api.TerminalSignalConsumer; +import io.servicetalk.http.api.HttpRequestMetaData; import io.servicetalk.http.api.HttpResponseMetaData; +import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; import io.servicetalk.http.utils.BeforeFinallyHttpOperator; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import javax.annotation.Nullable; -import static io.servicetalk.http.api.HttpResponseStatus.StatusClass.SERVER_ERROR_5XX; import static java.util.Objects.requireNonNull; class ScopeTracker implements TerminalSignalConsumer { private final Scope currentScope; - private final Span span; + private final Context context; + private final StreamingHttpRequest request; + private final Instrumenter instrumenter; @Nullable protected HttpResponseMetaData metaData; - ScopeTracker(Scope currentScope, final Span span) { + ScopeTracker(Scope currentScope, Context context, StreamingHttpRequest request, + Instrumenter instrumenter) { this.currentScope = requireNonNull(currentScope); - this.span = requireNonNull(span); + this.context = requireNonNull(context); + this.request = requireNonNull(request); + this.instrumenter = requireNonNull(instrumenter); } void onResponseMeta(final HttpResponseMetaData metaData) { @@ -51,11 +57,8 @@ void onResponseMeta(final HttpResponseMetaData metaData) { @Override public void onComplete() { assert metaData != null : "can't have succeeded without capturing metadata first"; - tagStatusCode(); try { - if (isError(metaData)) { - span.setStatus(StatusCode.ERROR); - } + instrumenter.end(context, request, metaData, null); } finally { closeAll(); } @@ -64,8 +67,7 @@ public void onComplete() { @Override public void onError(final Throwable throwable) { try { - tagStatusCode(); - span.setStatus(StatusCode.ERROR); + instrumenter.end(context, request, metaData, throwable); } finally { closeAll(); } @@ -74,23 +76,12 @@ public void onError(final Throwable throwable) { @Override public void cancel() { try { - tagStatusCode(); - span.setStatus(StatusCode.ERROR); + instrumenter.end(context, request, metaData, null); } finally { closeAll(); } } - /** - * Determine if a {@link HttpResponseMetaData} should be considered an error from a tracing perspective. - * - * @param metaData The {@link HttpResponseMetaData} to test. - * @return {@code true} if the {@link HttpResponseMetaData} should be considered an error for tracing. - */ - private static boolean isError(final HttpResponseMetaData metaData) { - return metaData.status().statusClass() == SERVER_ERROR_5XX; - } - Single track(Single responseSingle) { return responseSingle.liftSync(new BeforeFinallyHttpOperator(this)) // BeforeFinallyHttpOperator conditionally outputs a Single with a failed @@ -100,17 +91,7 @@ Single track(Single responseSingle .beforeOnSuccess(this::onResponseMeta); } - void tagStatusCode() { - if (metaData != null) { - ResponseTagExtractor.INSTANCE.extract(metaData, span); - } - } - private void closeAll() { - try { - currentScope.close(); - } finally { - span.end(); - } + currentScope.close(); } } diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkHttpClientCommonAttributesGetter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkHttpClientCommonAttributesGetter.java new file mode 100644 index 0000000000..b9575dd3e5 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkHttpClientCommonAttributesGetter.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2023 Apple Inc. and the ServiceTalk project authors + * + * 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 io.servicetalk.opentelemetry.http; + +import io.servicetalk.http.api.HttpRequestMetaData; +import io.servicetalk.http.api.HttpResponseMetaData; +import io.servicetalk.transport.api.HostAndPort; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesGetter; + +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +final class ServicetalkHttpClientCommonAttributesGetter + implements HttpClientAttributesGetter { + + static final ServicetalkHttpClientCommonAttributesGetter INSTANCE = + new ServicetalkHttpClientCommonAttributesGetter(); + + private ServicetalkHttpClientCommonAttributesGetter() { + } + + @Override + public String getHttpRequestMethod(HttpRequestMetaData httpRequestMetaData) { + return httpRequestMetaData.method().name(); + } + + @Override + public List getHttpRequestHeader(HttpRequestMetaData httpRequestMetaData, String name) { + CharSequence value = httpRequestMetaData.headers().get(name); + if (value != null) { + return Collections.singletonList(value.toString()); + } + return Collections.emptyList(); + } + + @Override + public Integer getHttpResponseStatusCode(HttpRequestMetaData httpRequestMetaData, + HttpResponseMetaData httpResponseMetaData, + @Nullable Throwable error) { + return httpResponseMetaData.status().code(); + } + + @Override + public List getHttpResponseHeader(HttpRequestMetaData httpRequestMetaData, + HttpResponseMetaData httpResponseMetaData, + String name) { + CharSequence value = httpResponseMetaData.headers().get(name); + if (value != null) { + return Collections.singletonList(value.toString()); + } + return Collections.emptyList(); + } + + @Nullable + @Override + public String getUrlFull(HttpRequestMetaData request) { + HostAndPort effectiveHostAndPort = request.effectiveHostAndPort(); + String requestScheme = request.scheme() != null ? request.scheme() + : "http"; + CharSequence hostAndPort = request.headers().contains("host") ? request.headers().get("host") + : effectiveHostAndPort != null ? + String.format("%s:%d", effectiveHostAndPort.hostName(), effectiveHostAndPort.port()) : null; + return hostAndPort != null ? String.format("%s://%s%s", requestScheme, hostAndPort, request.path()) : + request.path(); + } +} diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkHttpServerCommonAttributesGetter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkHttpServerCommonAttributesGetter.java new file mode 100644 index 0000000000..66c915f197 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkHttpServerCommonAttributesGetter.java @@ -0,0 +1,94 @@ +/* + * Copyright © 2023 Apple Inc. and the ServiceTalk project authors + * + * 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 io.servicetalk.opentelemetry.http; + +import io.servicetalk.http.api.HttpRequestMetaData; +import io.servicetalk.http.api.HttpResponseMetaData; + +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; + +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +final class ServicetalkHttpServerCommonAttributesGetter + implements HttpServerAttributesGetter { + + static final ServicetalkHttpServerCommonAttributesGetter INSTANCE = + new ServicetalkHttpServerCommonAttributesGetter(); + + private ServicetalkHttpServerCommonAttributesGetter() { + } + + @Nullable + @Override + public String getUrlScheme(HttpRequestMetaData httpRequestMetaData) { + return httpRequestMetaData.scheme(); + } + + @Nullable + @Override + public String getUrlPath(HttpRequestMetaData httpRequestMetaData) { + return httpRequestMetaData.path(); + } + + @Nullable + @Override + public String getUrlQuery(HttpRequestMetaData httpRequestMetaData) { + return httpRequestMetaData.query(); + } + + @Nullable + @Override + public String getHttpRequestMethod(HttpRequestMetaData httpRequestMetaData) { + return httpRequestMetaData.method().name(); + } + + @Override + public List getHttpRequestHeader(HttpRequestMetaData httpRequestMetaData, String name) { + CharSequence value = httpRequestMetaData.headers().get(name); + if (value != null) { + return Collections.singletonList(value.toString()); + } + return Collections.emptyList(); + } + + @Nullable + @Override + public Integer getHttpResponseStatusCode(HttpRequestMetaData httpRequestMetaData, + HttpResponseMetaData httpResponseMetaData, + @Nullable Throwable error) { + return httpResponseMetaData.status().code(); + } + + @Override + public List getHttpResponseHeader(HttpRequestMetaData httpRequestMetaData, + HttpResponseMetaData httpResponseMetaData, + String name) { + CharSequence value = httpResponseMetaData.headers().get(name); + if (value != null) { + return Collections.singletonList(value.toString()); + } + return Collections.emptyList(); + } + + @Nullable + @Override + public String getHttpRoute(HttpRequestMetaData httpRequestMetaData) { + return httpRequestMetaData.path(); + } +} diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkNetClientAttributesGetter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkNetClientAttributesGetter.java new file mode 100644 index 0000000000..a26b1efd39 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkNetClientAttributesGetter.java @@ -0,0 +1,81 @@ +/* + * Copyright © 2023 Apple Inc. and the ServiceTalk project authors + * + * 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 io.servicetalk.opentelemetry.http; + +import io.servicetalk.http.api.HttpProtocolVersion; +import io.servicetalk.http.api.HttpRequestMetaData; +import io.servicetalk.http.api.HttpResponseMetaData; +import io.servicetalk.transport.api.HostAndPort; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; + +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +final class ServicetalkNetClientAttributesGetter + implements NetClientAttributesGetter { + static final ServicetalkNetClientAttributesGetter INSTANCE = new ServicetalkNetClientAttributesGetter(); + + private ServicetalkNetClientAttributesGetter() { + } + + @Nullable + @Override + public String getNetworkProtocolName(HttpRequestMetaData request, @Nullable HttpResponseMetaData response) { + if (response == null) { + return null; + } + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(HttpRequestMetaData request, + @Nullable HttpResponseMetaData response) { + if (response == null) { + return null; + } + HttpProtocolVersion version = response.version(); + if (version.major() == 1) { + if (version.minor() == 1) { + return "1.1"; + } + if (version.minor() == 0) { + return "1.0"; + } + } else if (version.major() == 2 && version.minor() == 0) { + return "2.0"; + } + return version.major() + "." + version.minor(); + } + + @Override + @Nullable + public String getServerAddress(HttpRequestMetaData request) { + HostAndPort effectiveHostAndPort = request.effectiveHostAndPort(); + return effectiveHostAndPort != null ? effectiveHostAndPort.hostName() : null; + } + + @Override + public Integer getServerPort(HttpRequestMetaData request) { + HostAndPort effectiveHostAndPort = request.effectiveHostAndPort(); + return effectiveHostAndPort != null ? effectiveHostAndPort.port() : null; + } +} diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkNetServerAttributesGetter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkNetServerAttributesGetter.java new file mode 100644 index 0000000000..bbcb1fc58a --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkNetServerAttributesGetter.java @@ -0,0 +1,80 @@ +/* + * Copyright © 2023 Apple Inc. and the ServiceTalk project authors + * + * 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 io.servicetalk.opentelemetry.http; + +import io.servicetalk.http.api.HttpProtocolVersion; +import io.servicetalk.http.api.HttpRequestMetaData; +import io.servicetalk.http.api.HttpResponseMetaData; +import io.servicetalk.transport.api.HostAndPort; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; + +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +final class ServicetalkNetServerAttributesGetter + implements NetServerAttributesGetter { + + static final ServicetalkNetServerAttributesGetter INSTANCE = new ServicetalkNetServerAttributesGetter(); + + private ServicetalkNetServerAttributesGetter() { + } + + @Nullable + @Override + public String getNetworkProtocolName(HttpRequestMetaData request, @Nullable HttpResponseMetaData response) { + + if (response == null) { + return null; + } + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion(HttpRequestMetaData request, + @Nullable HttpResponseMetaData response) { + HttpProtocolVersion version = request.version(); + if (version.major() == 1) { + if (version.minor() == 1) { + return "1.1"; + } + if (version.minor() == 0) { + return "1.0"; + } + } else if (version.major() == 2 && version.minor() == 0) { + return "2.0"; + } + return version.major() + "." + version.minor(); + } + + @Override + @Nullable + public String getServerAddress(HttpRequestMetaData request) { + HostAndPort effectiveHostAndPort = request.effectiveHostAndPort(); + return effectiveHostAndPort != null ? effectiveHostAndPort.hostName() : null; + } + + @Override + public Integer getServerPort(HttpRequestMetaData request) { + HostAndPort effectiveHostAndPort = request.effectiveHostAndPort(); + return effectiveHostAndPort != null ? effectiveHostAndPort.port() : null; + } +} diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkSpanStatusExtractor.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkSpanStatusExtractor.java new file mode 100644 index 0000000000..3d1b9b2706 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServicetalkSpanStatusExtractor.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2023 Apple Inc. and the ServiceTalk project authors + * + * 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 io.servicetalk.opentelemetry.http; + +import io.servicetalk.http.api.HttpRequestMetaData; +import io.servicetalk.http.api.HttpResponseMetaData; + +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor; + +import javax.annotation.Nullable; + +final class ServicetalkSpanStatusExtractor implements SpanStatusExtractor { + + static final ServicetalkSpanStatusExtractor INSTANCE = new ServicetalkSpanStatusExtractor(); + + private ServicetalkSpanStatusExtractor() { + } + + @Override + public void extract( + SpanStatusBuilder spanStatusBuilder, + HttpRequestMetaData request, + @Nullable HttpResponseMetaData status, + @Nullable Throwable error) { + if (error != null) { + spanStatusBuilder.setStatus(StatusCode.ERROR); + } else if (status != null) { + switch (status.status().statusClass()) { + case INFORMATIONAL_1XX: + case SUCCESSFUL_2XX: + case REDIRECTION_3XX: + spanStatusBuilder.setStatus(StatusCode.OK); + break; + default: + spanStatusBuilder.setStatus(StatusCode.ERROR); + } + } else { + SpanStatusExtractor.getDefault().extract(spanStatusBuilder, request, null, null); + } + } +} diff --git a/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpRequestFilterTest.java b/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpRequestFilterTest.java index 57eafa9b5d..bc1e3dc961 100755 --- a/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpRequestFilterTest.java +++ b/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpRequestFilterTest.java @@ -43,6 +43,7 @@ import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; +import java.util.Collections; import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.http.netty.HttpClients.forSingleAddress; @@ -128,9 +129,27 @@ void testInjectWithAParent() throws Exception { ta.hasTraceId(serverSpanState.getTraceId())); otelTesting.assertTraces() - .hasTracesSatisfyingExactly(ta -> - assertThat(ta.getSpan(0).getAttributes().get(SemanticAttributes.HTTP_URL)) - .isEqualTo("/path")); + .hasTracesSatisfyingExactly(ta -> { + SpanData span = ta.getSpan(0); + assertThat(span.getAttributes().get(SemanticAttributes.HTTP_URL)) + .isEqualTo("/path"); + assertThat(span.getAttributes().get(SemanticAttributes.HTTP_METHOD)) + .isEqualTo("GET"); + assertThat(span.getAttributes().get(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH)) + .isGreaterThan(0); + assertThat(span.getAttributes().get(SemanticAttributes.NET_PROTOCOL_VERSION)) + .isEqualTo("1.1"); + assertThat(span.getAttributes().get(SemanticAttributes.NET_PROTOCOL_NAME)) + .isEqualTo("http"); + assertThat(span.getAttributes().get(SemanticAttributes.PEER_SERVICE)) + .isEqualTo("testClient"); + assertThat(span.getAttributes() + .get(AttributeKey.stringArrayKey("http.response.header.my_header"))) + .isNull(); + assertThat(span.getAttributes() + .get(AttributeKey.stringArrayKey("http.request.header.some_request_header"))) + .isNull(); + }); } } } @@ -186,6 +205,47 @@ void testInjectWithAParentCreated() throws Exception { } } + @Test + void testCaptureHeader() throws Exception { + final String requestUrl = "/"; + OpenTelemetry openTelemetry = otelTesting.getOpenTelemetry(); + try (ServerContext context = buildServer(openTelemetry, false)) { + try (HttpClient client = forSingleAddress(serverHostAndPort(context)) + .appendClientFilter(new OpenTelemetryHttpRequestFilter(openTelemetry, "testClient", + OpentelemetryOptions.newBuilder() + .setCaptureResponseHeaders(Collections.singletonList("my-header")) + .setCaptureRequestHeaders(Collections.singletonList("some-request-header")) + .build())) + .appendClientFilter(new TestTracingClientLoggerFilter(TRACING_TEST_LOG_LINE_PREFIX)).build()) { + HttpResponse response = client.request(client.get(requestUrl) + .addHeader("some-request-header", "request-header-value")).toFuture().get(); + TestSpanState serverSpanState = response.payloadBody(SPAN_STATE_SERIALIZER); + + verifyTraceIdPresentInLogs(stableAccumulated(1000), requestUrl, + serverSpanState.getTraceId(), serverSpanState.getSpanId(), + TRACING_TEST_LOG_LINE_PREFIX); + assertThat(otelTesting.getSpans()).hasSize(1); + assertThat(otelTesting.getSpans()).extracting("traceId") + .containsExactly(serverSpanState.getTraceId()); + assertThat(otelTesting.getSpans()).extracting("spanId") + .containsAnyOf(serverSpanState.getSpanId()); + otelTesting.assertTraces() + .hasTracesSatisfyingExactly(ta -> ta.hasTraceId(serverSpanState.getTraceId())); + + otelTesting.assertTraces() + .hasTracesSatisfyingExactly(ta -> { + SpanData span = ta.getSpan(0); + assertThat(span.getAttributes() + .get(AttributeKey.stringArrayKey("http.response.header.my_header"))) + .isEqualTo(Collections.singletonList("header-value")); + assertThat(span.getAttributes() + .get(AttributeKey.stringArrayKey("http.request.header.some_request_header"))) + .isEqualTo(Collections.singletonList("request-header-value")); + }); + } + } + } + private static ServerContext buildServer(OpenTelemetry givenOpentelemetry, boolean addFilter) throws Exception { HttpServerBuilder httpServerBuilder = HttpServers.forAddress(localAddress(0)); if (addFilter) { @@ -200,7 +260,8 @@ private static ServerContext buildServer(OpenTelemetry givenOpentelemetry, boole .extract(context, request.headers(), HeadersPropagatorGetter.INSTANCE); Span span = Span.fromContext(tracingContext); return succeeded( - responseFactory.ok().payloadBody(new TestSpanState(span.getSpanContext()), SPAN_STATE_SERIALIZER)); + responseFactory.ok().addHeader("my-header", "header-value") + .payloadBody(new TestSpanState(span.getSpanContext()), SPAN_STATE_SERIALIZER)); }); } diff --git a/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpServerFilterTest.java b/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpServerFilterTest.java index c9b866affb..22cfcd5d5d 100644 --- a/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpServerFilterTest.java +++ b/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/OpenTelemetryHttpServerFilterTest.java @@ -43,6 +43,7 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; +import java.util.Collections; import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.http.netty.HttpClients.forSingleAddress; @@ -91,17 +92,23 @@ void testInjectWithNoParent() throws Exception { otelTesting.assertTraces() .hasTracesSatisfyingExactly(ta -> { SpanData span = ta.getSpan(0); - assertThat(span.getAttributes().get(SemanticAttributes.HTTP_URL)) - .isEqualTo("/path"); + assertThat(span.getAttributes().get(SemanticAttributes.HTTP_STATUS_CODE)) + .isEqualTo(200); assertThat(span.getAttributes().get(SemanticAttributes.HTTP_TARGET)) .isEqualTo("/path"); - assertThat(span.getAttributes().get(SemanticAttributes.HTTP_ROUTE)) - .isEqualTo("/path"); - assertThat(span.getAttributes().get(SemanticAttributes.HTTP_FLAVOR)) + assertThat(span.getAttributes().get(SemanticAttributes.NET_PROTOCOL_NAME)) + .isEqualTo("http"); + assertThat(span.getAttributes().get(SemanticAttributes.NET_PROTOCOL_VERSION)) .isEqualTo("1.1"); assertThat(span.getAttributes().get(SemanticAttributes.HTTP_METHOD)) .isEqualTo("GET"); assertThat(span.getName()).isEqualTo("GET /path"); + assertThat(span.getAttributes() + .get(AttributeKey.stringArrayKey("http.response.header.my_header"))) + .isNull(); + assertThat(span.getAttributes() + .get(AttributeKey.stringArrayKey("http.request.header.some_request_header"))) + .isNull(); }); } } @@ -133,7 +140,7 @@ void testInjectWithAParent() throws Exception { .hasTracesSatisfyingExactly(ta -> { assertThat(ta.getSpan(0).getAttributes().get(SemanticAttributes.HTTP_URL)) .isEqualTo("/path"); - assertThat(ta.getSpan(0).getAttributes().get(SemanticAttributes.HTTP_FLAVOR)) + assertThat(ta.getSpan(0).getAttributes().get(SemanticAttributes.NET_PROTOCOL_VERSION)) .isEqualTo("1.1"); }); } @@ -169,28 +176,68 @@ void testInjectWithNewTrace() throws Exception { } finally { span.end(); } - verifyTraceIdPresentInLogs(stableAccumulated(1000), "/", - serverSpanState.getTraceId(), serverSpanState.getSpanId(), - TRACING_TEST_LOG_LINE_PREFIX); - assertThat(otelTesting.getSpans()).hasSize(2); - assertThat(otelTesting.getSpans()).extracting("traceId") - .containsExactly(serverSpanState.getTraceId(), serverSpanState.getTraceId()); + verifyTraceIdPresentInLogs(stableAccumulated(1000), "/", + serverSpanState.getTraceId(), serverSpanState.getSpanId(), + TRACING_TEST_LOG_LINE_PREFIX); + assertThat(otelTesting.getSpans()).hasSize(2); + assertThat(otelTesting.getSpans()).extracting("traceId") + .containsExactly(serverSpanState.getTraceId(), serverSpanState.getTraceId()); otelTesting.assertTraces() .hasTracesSatisfyingExactly(ta -> { assertThat(ta.getSpan(0).getAttributes().get(SemanticAttributes.HTTP_URL)) - .startsWith(url.toString()); - assertThat(ta.getSpan(1).getAttributes().get(SemanticAttributes.HTTP_URL)) - .isEqualTo("/path?query=this&foo=bar"); + .endsWith(url.toString()); + assertThat(ta.getSpan(1).getAttributes().get(SemanticAttributes.HTTP_METHOD)) + .isEqualTo("GET"); assertThat(ta.getSpan(0).getAttributes().get(AttributeKey.stringKey("component"))) .isEqualTo("serviceTalk"); }); } } - private static ServerContext buildServer(OpenTelemetry givenOpentelemetry) throws Exception { + @Test + void testCaptureHeaders() throws Exception { + final String requestUrl = "/path"; + try (ServerContext context = buildServer(otelTesting.getOpenTelemetry(), + OpentelemetryOptions.newBuilder() + .setCaptureResponseHeaders(Collections.singletonList("my-header")) + .setCaptureRequestHeaders(Collections.singletonList("some-request-header")) + .build())) { + try (HttpClient client = forSingleAddress(serverHostAndPort(context)).build()) { + HttpResponse response = client.request(client.get(requestUrl) + .addHeader("some-request-header", "request-header-value")) + .toFuture().get(); + TestSpanState serverSpanState = response.payloadBody(SPAN_STATE_SERIALIZER); + + verifyTraceIdPresentInLogs(stableAccumulated(1000), requestUrl, + serverSpanState.getTraceId(), serverSpanState.getSpanId(), + TRACING_TEST_LOG_LINE_PREFIX); + assertThat(otelTesting.getSpans()).hasSize(1); + assertThat(otelTesting.getSpans()).extracting("traceId") + .containsExactly(serverSpanState.getTraceId()); + assertThat(otelTesting.getSpans()).extracting("spanId") + .containsAnyOf(serverSpanState.getSpanId()); + otelTesting.assertTraces() + .hasTracesSatisfyingExactly(ta -> ta.hasTraceId(serverSpanState.getTraceId())); + + otelTesting.assertTraces() + .hasTracesSatisfyingExactly(ta -> { + SpanData span = ta.getSpan(0); + assertThat( + span.getAttributes().get(AttributeKey.stringArrayKey("http.response.header.my_header"))) + .isEqualTo(Collections.singletonList("header-value")); + assertThat(span.getAttributes() + .get(AttributeKey.stringArrayKey("http.request.header.some_request_header"))) + .isEqualTo(Collections.singletonList("request-header-value")); + }); + } + } + } + + private static ServerContext buildServer(OpenTelemetry givenOpentelemetry, + OpentelemetryOptions opentelemetryOptions) throws Exception { return HttpServers.forAddress(localAddress(0)) - .appendServiceFilter(new OpenTelemetryHttpServerFilter(givenOpentelemetry)) + .appendServiceFilter(new OpenTelemetryHttpServerFilter(givenOpentelemetry, opentelemetryOptions)) .appendServiceFilter(new TestTracingServerLoggerFilter(TRACING_TEST_LOG_LINE_PREFIX)) .listenAndAwait((ctx, request, responseFactory) -> { final ContextPropagators propagators = givenOpentelemetry.getPropagators(); @@ -202,7 +249,13 @@ private static ServerContext buildServer(OpenTelemetry givenOpentelemetry) throw span = Span.fromContext(tracingContext); } return succeeded( - responseFactory.ok().payloadBody(new TestSpanState(span.getSpanContext()), SPAN_STATE_SERIALIZER)); + responseFactory.ok() + .addHeader("my-header", "header-value") + .payloadBody(new TestSpanState(span.getSpanContext()), SPAN_STATE_SERIALIZER)); }); } + + private static ServerContext buildServer(OpenTelemetry givenOpentelemetry) throws Exception { + return buildServer(givenOpentelemetry, OpentelemetryOptions.newBuilder().build()); + } }