diff --git a/gradle.properties b/gradle.properties index 787c7252d1..6ded5ab9fa 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 +opentelemetryInstrumentationVersion=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-http-api/src/main/java/io/servicetalk/http/api/HttpProtocolVersion.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpProtocolVersion.java index 31b01e9831..893051c4b4 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpProtocolVersion.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpProtocolVersion.java @@ -45,6 +45,7 @@ public final class HttpProtocolVersion implements Protocol, Comparable 9) { @@ -56,8 +57,8 @@ private HttpProtocolVersion(final int major, final int minor) { throw new IllegalArgumentException("Illegal minor version: " + minor + ", expected [0-9]"); } this.minor = minor; - - httpVersion = "HTTP/" + major + '.' + minor; + this.fullVersion = major + "." + minor; + httpVersion = "HTTP/" + fullVersion; encodedAsBuffer = PREFER_HEAP_RO_ALLOCATOR.fromAscii(httpVersion); } @@ -149,6 +150,14 @@ public String name() { return httpVersion; } + /** + * Resolves and return the http version number as a String. e.g. : 1.1 or 2.0. + * @return the http version number string. + */ + public String fullVersion() { + return fullVersion; + } + /** * Determine if the protocol version is {@link #major()} is {@code 1} and trailers are supported. * @param version The version to check. diff --git a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/HttpProtocolVersionTest.java b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/HttpProtocolVersionTest.java index 2eb0608682..963870129e 100644 --- a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/HttpProtocolVersionTest.java +++ b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/HttpProtocolVersionTest.java @@ -39,6 +39,7 @@ class HttpProtocolVersionTest { void testHttp11Constant() { assertEquals(1, HTTP_1_1.major()); assertEquals(1, HTTP_1_1.minor()); + assertEquals("1.1", HTTP_1_1.fullVersion()); assertEquals("HTTP/1.1", HTTP_1_1.toString()); assertWriteToBuffer("HTTP/1.1", HTTP_1_1); } @@ -47,6 +48,7 @@ void testHttp11Constant() { void testHttp10Constant() { assertEquals(1, HTTP_1_0.major()); assertEquals(0, HTTP_1_0.minor()); + assertEquals("1.0", HTTP_1_0.fullVersion()); assertEquals("HTTP/1.0", HTTP_1_0.toString()); assertWriteToBuffer("HTTP/1.0", HTTP_1_0); } @@ -63,6 +65,7 @@ void testCreateNewProtocolVersionFromMajorAndMinor() { assertEquals(9, version98.major()); assertEquals(8, version98.minor()); assertEquals("HTTP/9.8", version98.toString()); + assertEquals("9.8", version98.fullVersion()); assertWriteToBuffer("HTTP/9.8", version98); } diff --git a/servicetalk-opentelemetry-http/build.gradle b/servicetalk-opentelemetry-http/build.gradle index c131c41eb7..276454df21 100755 --- a/servicetalk-opentelemetry-http/build.gradle +++ b/servicetalk-opentelemetry-http/build.gradle @@ -24,6 +24,8 @@ dependencies { api project(":servicetalk-http-api") api "io.opentelemetry:opentelemetry-api:$opentelemetryVersion" + implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-semconv:" + + "$opentelemetryInstrumentationVersion") implementation project(":servicetalk-annotations") implementation project(":servicetalk-http-utils") @@ -36,7 +38,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:" + + "$opentelemetryInstrumentationVersion") 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/gradle/spotbugs/main-exclusions.xml b/servicetalk-opentelemetry-http/gradle/spotbugs/main-exclusions.xml new file mode 100644 index 0000000000..7374177c69 --- /dev/null +++ b/servicetalk-opentelemetry-http/gradle/spotbugs/main-exclusions.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/servicetalk-opentelemetry-http/gradle/spotbugs/test-exclusions.xml b/servicetalk-opentelemetry-http/gradle/spotbugs/test-exclusions.xml new file mode 100644 index 0000000000..b566540c05 --- /dev/null +++ b/servicetalk-opentelemetry-http/gradle/spotbugs/test-exclusions.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/AbstractOpenTelemetryFilter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/AbstractOpenTelemetryFilter.java index 323f9013ed..11f58cc700 100644 --- a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/AbstractOpenTelemetryFilter.java +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/AbstractOpenTelemetryFilter.java @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Apple Inc. and the ServiceTalk project authors + * Copyright © 2022-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. @@ -25,6 +25,7 @@ import io.opentelemetry.context.propagation.ContextPropagators; abstract class AbstractOpenTelemetryFilter implements HttpExecutionStrategyInfluencer { + static final OpenTelemetryOptions DEFAULT_OPTIONS = new OpenTelemetryOptions.Builder().build(); static final String INSTRUMENTATION_SCOPE_NAME = "io.servicetalk"; final Tracer tracer; final ContextPropagators propagators; diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/HeadersPropagatorGetter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/HeadersPropagatorGetter.java index b3f27d4940..d7de6a1a6f 100755 --- a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/HeadersPropagatorGetter.java +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/HeadersPropagatorGetter.java @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Apple Inc. and the ServiceTalk project authors + * Copyright © 2022-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. @@ -60,7 +60,7 @@ public void remove() { @Override @Nullable - public String get(@Nullable HttpHeaders carrier, final String key) { + public String get(@Nullable final HttpHeaders carrier, final String key) { if (carrier == null) { return null; } 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..91ac89cd8f 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Apple Inc. and the ServiceTalk project authors + * Copyright © 2022-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. @@ -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,23 @@ 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. + * @deprecated this method is internal, no user should be setting the {@link OpenTelemetry} as it is obtained by + * using {@link GlobalOpenTelemetry#get()} and there should be no other implementations but the one available in + * the classpath, this constructor will be removed in the future releases. + * Use {@link #OpenTelemetryHttpRequestFilter(String, OpenTelemetryOptions)} or + * {@link #OpenTelemetryHttpRequestFilter()} instead. */ + @Deprecated // FIXME: 0.43 - remove deprecated ctor + @SuppressWarnings("DeprecatedIsStillUsed") public OpenTelemetryHttpRequestFilter(final OpenTelemetry openTelemetry, String componentName) { - super(openTelemetry); - this.componentName = componentName.trim(); + this(openTelemetry, componentName, DEFAULT_OPTIONS); } /** @@ -72,8 +87,18 @@ public OpenTelemetryHttpRequestFilter(final OpenTelemetry openTelemetry, String * * @param componentName The component name used during building new spans. */ - public OpenTelemetryHttpRequestFilter(String componentName) { - this(GlobalOpenTelemetry.get(), componentName); + public OpenTelemetryHttpRequestFilter(final String componentName) { + this(componentName, DEFAULT_OPTIONS); + } + + /** + * 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(final String componentName, final OpenTelemetryOptions opentelemetryOptions) { + this(GlobalOpenTelemetry.get(), componentName, opentelemetryOptions); } /** @@ -81,7 +106,42 @@ public OpenTelemetryHttpRequestFilter(String componentName) { * using the hostname as the component name. */ public OpenTelemetryHttpRequestFilter() { - this(GlobalOpenTelemetry.get(), ""); + this(""); + } + + /** + * Create a new instance. + * + * @param openTelemetry the {@link OpenTelemetry}. + * @param componentName The component name used during building new spans. + * @param opentelemetryOptions extra options to create the opentelemetry filter. + */ + OpenTelemetryHttpRequestFilter(final OpenTelemetry openTelemetry, String componentName, + final OpenTelemetryOptions opentelemetryOptions) { + super(openTelemetry); + SpanNameExtractor serverSpanNameExtractor = + HttpSpanNameExtractor.create(ServiceTalkHttpAttributesGetter.INSTANCE); + InstrumenterBuilder clientInstrumenterBuilder = + Instrumenter.builder(openTelemetry, INSTRUMENTATION_SCOPE_NAME, serverSpanNameExtractor); + clientInstrumenterBuilder.setSpanStatusExtractor(ServicetalkSpanStatusExtractor.INSTANCE); + + clientInstrumenterBuilder + .addAttributesExtractor(HttpClientAttributesExtractor + .builder(ServiceTalkHttpAttributesGetter.INSTANCE, ServiceTalkNetAttributesGetter.INSTANCE) + .setCapturedRequestHeaders(opentelemetryOptions.capturedRequestHeaders()) + .setCapturedResponseHeaders(opentelemetryOptions.capturedResponseHeaders()) + .build()) + .addAttributesExtractor(NetClientAttributesExtractor.create(ServiceTalkNetAttributesGetter.INSTANCE)); + if (opentelemetryOptions.enableMetrics()) { + clientInstrumenterBuilder.addOperationMetrics(HttpClientMetrics.get()); + } + componentName = componentName.trim(); + if (!componentName.isEmpty()) { + clientInstrumenterBuilder.addAttributesExtractor( + AttributesExtractor.constant(SemanticAttributes.PEER_SERVICE, componentName)); + } + instrumenter = + clientInstrumenterBuilder.buildClientInstrumenter(RequestHeadersPropagatorSetter.INSTANCE); } @Override @@ -108,18 +168,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(); + final 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 +182,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..bbf1da4be2 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Apple Inc. and the ServiceTalk project authors + * Copyright © 2022-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. @@ -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,61 @@ */ public final class OpenTelemetryHttpServerFilter extends AbstractOpenTelemetryFilter implements StreamingHttpServiceFilterFactory { + private final Instrumenter instrumenter; /** * Create a new instance. * * @param openTelemetry the {@link OpenTelemetry}. + * @deprecated this method is internal, no user should be setting the {@link OpenTelemetry} as it is obtained by + * using {@link GlobalOpenTelemetry#get()} and there should be no other implementations but the one available in + * the classpath, this constructor will be removed in the future releases. + * Use {@link #OpenTelemetryHttpServerFilter(OpenTelemetryOptions)} or {@link #OpenTelemetryHttpServerFilter()} + * instead. */ + @Deprecated // FIXME: 0.43 - remove deprecated ctor + @SuppressWarnings("DeprecatedIsStillUsed") public OpenTelemetryHttpServerFilter(final OpenTelemetry openTelemetry) { - super(openTelemetry); + this(openTelemetry, DEFAULT_OPTIONS); } /** * Create a new Instance, searching for any instance of an opentelemetry available. */ public OpenTelemetryHttpServerFilter() { - this(GlobalOpenTelemetry.get()); + this(DEFAULT_OPTIONS); + } + + /** + * Create a new instance. + * + * @param opentelemetryOptions extra options to create the opentelemetry filter + */ + public OpenTelemetryHttpServerFilter(final OpenTelemetryOptions opentelemetryOptions) { + this(GlobalOpenTelemetry.get(), opentelemetryOptions); + } + + OpenTelemetryHttpServerFilter(final OpenTelemetry openTelemetry, final OpenTelemetryOptions opentelemetryOptions) { + super(openTelemetry); + SpanNameExtractor serverSpanNameExtractor = + HttpSpanNameExtractor.create(ServiceTalkHttpAttributesGetter.INSTANCE); + InstrumenterBuilder serverInstrumenterBuilder = + Instrumenter.builder(openTelemetry, INSTRUMENTATION_SCOPE_NAME, serverSpanNameExtractor); + serverInstrumenterBuilder.setSpanStatusExtractor(ServicetalkSpanStatusExtractor.INSTANCE); + + serverInstrumenterBuilder + .addAttributesExtractor(HttpServerAttributesExtractor + .builder(ServiceTalkHttpAttributesGetter.INSTANCE, ServiceTalkNetAttributesGetter.INSTANCE) + .setCapturedRequestHeaders(opentelemetryOptions.capturedRequestHeaders()) + .setCapturedResponseHeaders(opentelemetryOptions.capturedResponseHeaders()) + .build()) + .addAttributesExtractor(NetServerAttributesExtractor.create(ServiceTalkNetAttributesGetter.INSTANCE)); + if (opentelemetryOptions.enableMetrics()) { + serverInstrumenterBuilder.addOperationMetrics(HttpServerMetrics.get()); + } + + instrumenter = + serverInstrumenterBuilder.buildServerInstrumenter(RequestHeadersPropagatorGetter.INSTANCE); } @Override @@ -85,26 +132,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); + } + final 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 +150,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..e4079c5b34 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/OpenTelemetryOptions.java @@ -0,0 +1,183 @@ +/* + * 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.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpClientMetrics; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesExtractorBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerMetrics; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; + +/** + * A set of options for configuring OpenTelemetry filters. + */ +public final class OpenTelemetryOptions { + + private final List capturedRequestHeaders; + private final List capturedResponseHeaders; + private final boolean enableMetrics; + + OpenTelemetryOptions(final List capturedRequestHeaders, + final List capturedResponseHeaders, + final boolean enableMetrics) { + this.capturedRequestHeaders = capturedRequestHeaders; + this.capturedResponseHeaders = capturedResponseHeaders; + this.enableMetrics = enableMetrics; + } + + /** + * List of request headers to be captured as extra span attributes. + * + * @return List of request headers to be captured as extra span attributes + * @see HttpClientAttributesExtractorBuilder#setCapturedRequestHeaders(List) + * @see HttpServerAttributesExtractorBuilder#setCapturedRequestHeaders(List) + */ + public List capturedRequestHeaders() { + return capturedRequestHeaders; + } + + /** + * List of response headers to be captured as extra span attributes. + * + * @return List of response headers to be captured as extra span attributes. + * @see HttpClientAttributesExtractorBuilder#setCapturedResponseHeaders(List) + * @see HttpServerAttributesExtractorBuilder#setCapturedResponseHeaders(List) + */ + public List capturedResponseHeaders() { + return capturedResponseHeaders; + } + + /** + * Whether to enable operation metrics or not. + * + * @return {@code true} when operation metrics should be enabled, {@code false} otherwise + * @see InstrumenterBuilder#addOperationMetrics(OperationMetrics) + * @see HttpClientMetrics + * @see HttpServerMetrics + */ + public boolean enableMetrics() { + return enableMetrics; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof OpenTelemetryOptions)) { + return false; + } + + final OpenTelemetryOptions that = (OpenTelemetryOptions) o; + if (enableMetrics != that.enableMetrics) { + return false; + } + if (!capturedRequestHeaders.equals(that.capturedRequestHeaders)) { + return false; + } + return capturedResponseHeaders.equals(that.capturedResponseHeaders); + } + + @Override + public int hashCode() { + int result = capturedRequestHeaders.hashCode(); + result = 31 * result + capturedResponseHeaders.hashCode(); + result = 31 * result + (enableMetrics ? 1 : 0); + return result; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "{capturedRequestHeaders=" + capturedRequestHeaders + + ", capturedResponseHeaders=" + capturedResponseHeaders + + ", enableMetrics=" + enableMetrics + + '}'; + } + + /** + * A builder for {@link OpenTelemetryOptions}. + */ + public static final class Builder { + private List capturedRequestHeaders = emptyList(); + private List capturedResponseHeaders = emptyList(); + private boolean enableMetrics; + + /** + * Add the headers to be captured as extra span attributes. + * + * @param capturedRequestHeaders extra headers to be captured in client/server requests and added as extra span + * attributes + * @return an instance of itself + * @see #capturedRequestHeaders() + * @see HttpClientAttributesExtractorBuilder#setCapturedRequestHeaders(List) + * @see HttpServerAttributesExtractorBuilder#setCapturedRequestHeaders(List) + */ + public Builder capturedRequestHeaders(final List capturedRequestHeaders) { + this.capturedRequestHeaders = capturedRequestHeaders.isEmpty() ? emptyList() : + unmodifiableList(new ArrayList<>(capturedRequestHeaders)); + return this; + } + + /** + * Add the headers to be captured as extra span attributes. + * + * @param capturedResponseHeaders extra headers to be captured in client/server response and added as extra span + * attributes + * @return an instance of itself + * @see #capturedResponseHeaders() + * @see HttpClientAttributesExtractorBuilder#setCapturedResponseHeaders(List) + * @see HttpServerAttributesExtractorBuilder#setCapturedResponseHeaders(List) + */ + public Builder capturedResponseHeaders(final List capturedResponseHeaders) { + this.capturedResponseHeaders = capturedResponseHeaders.isEmpty() ? emptyList() : + unmodifiableList(new ArrayList<>(capturedResponseHeaders)); + return this; + } + + /** + * Whether to enable operation metrics or not. + * + * @param enableMetrics whether to enable operation metrics or not + * @return an instance of itself + * @see #enableMetrics() + * @see InstrumenterBuilder#addOperationMetrics(OperationMetrics) + * @see HttpClientMetrics + * @see HttpServerMetrics + */ + public Builder enableMetrics(final boolean enableMetrics) { + this.enableMetrics = enableMetrics; + return this; + } + + /** + * Builds a new {@link OpenTelemetryOptions}. + * + * @return a new {@link OpenTelemetryOptions} + */ + public OpenTelemetryOptions build() { + return new OpenTelemetryOptions(capturedRequestHeaders, capturedResponseHeaders, 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..4448b3ba75 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestHeadersPropagatorGetter.java @@ -0,0 +1,45 @@ +/* + * 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.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/RequestHeadersPropagatorSetter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestHeadersPropagatorSetter.java new file mode 100755 index 0000000000..3115bf56ca --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/RequestHeadersPropagatorSetter.java @@ -0,0 +1,38 @@ +/* + * 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.opentelemetry.context.propagation.TextMapSetter; + +import javax.annotation.Nullable; + +final class RequestHeadersPropagatorSetter implements TextMapSetter { + + static final TextMapSetter INSTANCE = new RequestHeadersPropagatorSetter(); + + private RequestHeadersPropagatorSetter() { + } + + @Override + public void set(@Nullable final HttpRequestMetaData carrier, final String key, final String value) { + if (carrier != null) { + HeadersPropagatorSetter.INSTANCE.set(carrier.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/ResponseTagExtractor.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ResponseTagExtractor.java deleted file mode 100644 index f2a4ca7d08..0000000000 --- a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ResponseTagExtractor.java +++ /dev/null @@ -1,30 +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.HttpResponseMetaData; - -import io.opentelemetry.api.trace.Span; - -final class ResponseTagExtractor { - - public static final ResponseTagExtractor INSTANCE = new ResponseTagExtractor(); - - void extract(HttpResponseMetaData responseMetaData, Span span) { - span.setAttribute("http.status_code", responseMetaData.status().code()); - } -} 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..eb194cb18b 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Apple Inc. and the ServiceTalk project authors + * Copyright © 2022-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. @@ -18,44 +18,47 @@ 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 HttpRequestMetaData requestMetaData; + private final Instrumenter instrumenter; @Nullable - protected HttpResponseMetaData metaData; + private HttpResponseMetaData responseMetaData; - ScopeTracker(Scope currentScope, final Span span) { + ScopeTracker(Scope currentScope, Context context, StreamingHttpRequest requestMetaData, + Instrumenter instrumenter) { this.currentScope = requireNonNull(currentScope); - this.span = requireNonNull(span); + this.context = requireNonNull(context); + this.requestMetaData = requireNonNull(requestMetaData); + this.instrumenter = requireNonNull(instrumenter); } void onResponseMeta(final HttpResponseMetaData metaData) { - this.metaData = metaData; + this.responseMetaData = metaData; } @Override public void onComplete() { - assert metaData != null : "can't have succeeded without capturing metadata first"; - tagStatusCode(); + assert responseMetaData != null : "can't have succeeded without capturing metadata first"; try { - if (isError(metaData)) { - span.setStatus(StatusCode.ERROR); - } + instrumenter.end(context, requestMetaData, responseMetaData, 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, requestMetaData, responseMetaData, 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, requestMetaData, responseMetaData, CancelledRequestException.INSTANCE); } 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,16 @@ Single track(Single responseSingle .beforeOnSuccess(this::onResponseMeta); } - void tagStatusCode() { - if (metaData != null) { - ResponseTagExtractor.INSTANCE.extract(metaData, span); - } + private void closeAll() { + currentScope.close(); } - private void closeAll() { - try { - currentScope.close(); - } finally { - span.end(); + private static final class CancelledRequestException extends Exception { + private static final long serialVersionUID = 6357694797622093267L; + static final CancelledRequestException INSTANCE = new CancelledRequestException(); + + CancelledRequestException() { + super("canceled", null, false, false); } } } diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServiceTalkHttpAttributesGetter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServiceTalkHttpAttributesGetter.java new file mode 100644 index 0000000000..26548bda76 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServiceTalkHttpAttributesGetter.java @@ -0,0 +1,114 @@ +/* + * 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.HttpHeaders; +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 io.opentelemetry.instrumentation.api.instrumenter.http.HttpServerAttributesGetter; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import javax.annotation.Nullable; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; + +final class ServiceTalkHttpAttributesGetter implements + HttpClientAttributesGetter, + HttpServerAttributesGetter { + + static final ServiceTalkHttpAttributesGetter INSTANCE = new ServiceTalkHttpAttributesGetter(); + + private ServiceTalkHttpAttributesGetter() { + } + + @Override + public String getHttpRequestMethod(final HttpRequestMetaData httpRequestMetaData) { + return httpRequestMetaData.method().name(); + } + + @Override + public List getHttpRequestHeader(final HttpRequestMetaData httpRequestMetaData, final String name) { + return getHeaderValues(httpRequestMetaData.headers(), name); + } + + @Override + public Integer getHttpResponseStatusCode(final HttpRequestMetaData httpRequestMetaData, + final HttpResponseMetaData httpResponseMetaData, + @Nullable final Throwable error) { + return httpResponseMetaData.status().code(); + } + + @Override + public List getHttpResponseHeader(final HttpRequestMetaData httpRequestMetaData, + final HttpResponseMetaData httpResponseMetaData, + final String name) { + return getHeaderValues(httpResponseMetaData.headers(), name); + } + + @Nullable + @Override + public String getUrlFull(final HttpRequestMetaData request) { + HostAndPort effectiveHostAndPort = request.effectiveHostAndPort(); + if (effectiveHostAndPort == null) { + return null; + } + String requestScheme = request.scheme() == null ? "http" : request.scheme(); + String hostAndPort = effectiveHostAndPort.hostName() + ':' + effectiveHostAndPort.port(); + return requestScheme + "://" + hostAndPort + '/' + request.requestTarget(); + } + + @Override + public String getUrlScheme(final HttpRequestMetaData httpRequestMetaData) { + final String scheme = httpRequestMetaData.scheme(); + return scheme == null ? "http" : scheme; + } + + @Override + public String getUrlPath(final HttpRequestMetaData httpRequestMetaData) { + return httpRequestMetaData.path(); + } + + @Override + public String getUrlQuery(final HttpRequestMetaData httpRequestMetaData) { + return httpRequestMetaData.query(); + } + + private static List getHeaderValues(final HttpHeaders headers, final String name) { + final Iterator iterator = headers.valuesIterator(name); + if (!iterator.hasNext()) { + return emptyList(); + } + final CharSequence firstValue = iterator.next(); + if (!iterator.hasNext()) { + return singletonList(firstValue.toString()); + } + final List result = new ArrayList<>(2); + result.add(firstValue.toString()); + result.add(iterator.next().toString()); + while (iterator.hasNext()) { + result.add(iterator.next().toString()); + } + return unmodifiableList(result); + } +} diff --git a/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServiceTalkNetAttributesGetter.java b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServiceTalkNetAttributesGetter.java new file mode 100644 index 0000000000..a6d6d13fd6 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/main/java/io/servicetalk/opentelemetry/http/ServiceTalkNetAttributesGetter.java @@ -0,0 +1,72 @@ +/* + * 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.net.NetClientAttributesGetter; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetServerAttributesGetter; + +import javax.annotation.Nullable; + +final class ServiceTalkNetAttributesGetter implements + NetClientAttributesGetter, + NetServerAttributesGetter { + + static final ServiceTalkNetAttributesGetter INSTANCE = new ServiceTalkNetAttributesGetter(); + + private ServiceTalkNetAttributesGetter() { + } + + @Override + public String getNetworkProtocolName(final HttpRequestMetaData request, + @Nullable final HttpResponseMetaData response) { + return "http"; + } + + @Override + public String getNetworkProtocolVersion(final HttpRequestMetaData request, + @Nullable final HttpResponseMetaData response) { + if (response == null) { + return request.version().fullVersion(); + } + return response.version().fullVersion(); + } + + @Override + @Nullable + public String getServerAddress(final HttpRequestMetaData request) { + final HostAndPort effectiveHostAndPort = request.effectiveHostAndPort(); + return effectiveHostAndPort != null ? effectiveHostAndPort.hostName() : null; + } + + @Nullable + @Override + public Integer getServerPort(final HttpRequestMetaData request) { + final HostAndPort effectiveHostAndPort = request.effectiveHostAndPort(); + return effectiveHostAndPort != null ? effectiveHostAndPort.port() : null; + } + + @Nullable + @Override + public String getNetworkType(final HttpRequestMetaData requestMetaData, + @Nullable final HttpResponseMetaData metaData) { + return NetServerAttributesGetter.super.getNetworkType(requestMetaData, metaData); + } +} 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..e63b499d0f --- /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 CLIENT_ERROR_4XX: + case SERVER_ERROR_5XX: + spanStatusBuilder.setStatus(StatusCode.ERROR); + break; + default: + spanStatusBuilder.setStatus(StatusCode.OK); + break; + } + } 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..48199f0c22 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 @@ -53,6 +53,7 @@ import static io.servicetalk.opentelemetry.http.TestUtils.TestTracingClientLoggerFilter; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -62,7 +63,7 @@ class OpenTelemetryHttpRequestFilterTest { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @RegisterExtension - private final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); @BeforeEach public void setup() { @@ -98,8 +99,8 @@ void testInjectWithNoParent() throws Exception { otelTesting.assertTraces() .hasTracesSatisfyingExactly(ta -> - assertThat(ta.getSpan(0).getAttributes().get(SemanticAttributes.HTTP_URL)) - .isEqualTo("/")); + assertThat(ta.getSpan(0).getAttributes().get(SemanticAttributes.NET_PROTOCOL_NAME)) + .isEqualTo("http")); } } } @@ -128,9 +129,25 @@ 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_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(); + }); } } } @@ -171,8 +188,8 @@ void testInjectWithAParentCreated() throws Exception { otelTesting.assertTraces() .hasTracesSatisfyingExactly(ta -> - assertThat(ta.getSpan(1).getAttributes().get(SemanticAttributes.HTTP_URL)) - .isEqualTo("/path/to/resource")); + assertThat(ta.getSpan(1).getAttributes().get(SemanticAttributes.NET_PROTOCOL_NAME)) + .isEqualTo("http")); otelTesting.assertTraces() .hasTracesSatisfyingExactly(ta -> assertThat(ta.getSpan(0).getAttributes().get(AttributeKey.stringKey("component"))) @@ -186,6 +203,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", + new OpenTelemetryOptions.Builder() + .capturedResponseHeaders(singletonList("my-header")) + .capturedRequestHeaders(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(singletonList("header-value")); + assertThat(span.getAttributes() + .get(AttributeKey.stringArrayKey("http.request.header.some_request_header"))) + .isEqualTo(singletonList("request-header-value")); + }); + } + } + } + private static ServerContext buildServer(OpenTelemetry givenOpentelemetry, boolean addFilter) throws Exception { HttpServerBuilder httpServerBuilder = HttpServers.forAddress(localAddress(0)); if (addFilter) { @@ -196,11 +254,12 @@ private static ServerContext buildServer(OpenTelemetry givenOpentelemetry, boole .listenAndAwait((ctx, request, responseFactory) -> { final ContextPropagators propagators = givenOpentelemetry.getPropagators(); final Context context = Context.root(); - io.opentelemetry.context.Context tracingContext = propagators.getTextMapPropagator() + Context tracingContext = propagators.getTextMapPropagator() .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..50eeff4f0f 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 @@ -52,12 +52,13 @@ import static io.servicetalk.opentelemetry.http.TestUtils.TRACING_TEST_LOG_LINE_PREFIX; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; class OpenTelemetryHttpServerFilterTest { @RegisterExtension - final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); @BeforeEach public void setup() { @@ -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.getName()).isEqualTo("GET"); + 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(); }); } } @@ -131,9 +138,9 @@ void testInjectWithAParent() throws Exception { otelTesting.assertTraces() .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_NAME)) + .isEqualTo("http"); + assertThat(ta.getSpan(0).getAttributes().get(SemanticAttributes.NET_PROTOCOL_VERSION)) .isEqualTo("1.1"); }); } @@ -158,7 +165,7 @@ void testInjectWithNewTrace() throws Exception { .setAttribute(SemanticAttributes.HTTP_URL, url.toString()); HttpURLConnection con = (HttpURLConnection) url.openConnection(); - textMapPropagator.inject(io.opentelemetry.context.Context.root().with(span), con, setter); + textMapPropagator.inject(Context.root().with(span), con, setter); con.setRequestMethod("GET"); int responseCode = con.getResponseCode(); @@ -169,40 +176,86 @@ 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(), + new OpenTelemetryOptions.Builder() + .capturedResponseHeaders(singletonList("my-header")) + .capturedRequestHeaders(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(singletonList("header-value")); + assertThat(span.getAttributes() + .get(AttributeKey.stringArrayKey("http.request.header.some_request_header"))) + .isEqualTo(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(); final Context context = Context.root(); - io.opentelemetry.context.Context tracingContext = propagators.getTextMapPropagator() + Context tracingContext = propagators.getTextMapPropagator() .extract(context, request.headers(), HeadersPropagatorGetter.INSTANCE); Span span = Span.current(); if (!span.getSpanContext().isValid()) { 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, new OpenTelemetryOptions.Builder().build()); + } } diff --git a/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/ServicetalkSpanStatusExtractorTest.java b/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/ServicetalkSpanStatusExtractorTest.java new file mode 100644 index 0000000000..654556b144 --- /dev/null +++ b/servicetalk-opentelemetry-http/src/test/java/io/servicetalk/opentelemetry/http/ServicetalkSpanStatusExtractorTest.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.HttpRequestMetaData; +import io.servicetalk.http.api.HttpResponseMetaData; +import io.servicetalk.http.api.HttpResponseStatus; + +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +@ExtendWith(MockitoExtension.class) +class ServicetalkSpanStatusExtractorTest { + + @Mock + private SpanStatusBuilder spanStatusBuilder; + + @Mock + HttpRequestMetaData requestMetaData; + + @Mock + HttpResponseMetaData responseMetaData; + + @Test + void testStatus200To399() { + int executions = 0; + for (int code = 100; code < 400; code++) { + executions++; + when(responseMetaData.status()).thenReturn(HttpResponseStatus.of(code, "any")); + ServicetalkSpanStatusExtractor.INSTANCE.extract(spanStatusBuilder, requestMetaData, responseMetaData, null); + } + verify(spanStatusBuilder, times(executions)).setStatus(StatusCode.OK); + } + + @Test + void testStatus400to599() { + int executions = 0; + for (int code = 400; code < 600; code++) { + executions++; + when(responseMetaData.status()).thenReturn(HttpResponseStatus.of(code, "any")); + ServicetalkSpanStatusExtractor.INSTANCE.extract(spanStatusBuilder, requestMetaData, responseMetaData, null); + } + verify(spanStatusBuilder, times(executions)).setStatus(StatusCode.ERROR); + } + + @Test + void testStatusUnknown() { + when(responseMetaData.status()).thenReturn(HttpResponseStatus.of(600, "any")); + ServicetalkSpanStatusExtractor.INSTANCE.extract(spanStatusBuilder, requestMetaData, responseMetaData, null); + verify(spanStatusBuilder).setStatus(StatusCode.OK); + } + + @Test + void testExceptionError() { + ServicetalkSpanStatusExtractor.INSTANCE.extract(spanStatusBuilder, requestMetaData, responseMetaData, + new RuntimeException()); + verify(spanStatusBuilder).setStatus(StatusCode.ERROR); + } +}