diff --git a/instrumentation/okhttp/okhttp-3.0/library/build.gradle.kts b/instrumentation/okhttp/okhttp-3.0/library/build.gradle.kts index 9251deeffa1e..00bf4670b04a 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/build.gradle.kts +++ b/instrumentation/okhttp/okhttp-3.0/library/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } dependencies { - library("com.squareup.okhttp3:okhttp:3.0.0") + library("com.squareup.okhttp3:okhttp:3.11.0") testImplementation(project(":instrumentation:okhttp:okhttp-3.0:testing")) } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/http2Test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java b/instrumentation/okhttp/okhttp-3.0/library/src/http2Test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java index 1ce2679d818a..71e24283a707 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/http2Test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/http2Test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Http2Test.java @@ -6,7 +6,9 @@ package io.opentelemetry.instrumentation.okhttp.v3_0; import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; @@ -14,6 +16,7 @@ import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Protocol; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; class OkHttp3Http2Test extends AbstractOkHttp3Test { @@ -39,4 +42,41 @@ protected void configure(HttpClientTestOptions.Builder optionsBuilder) { optionsBuilder.disableTestHttps(); optionsBuilder.setHttpProtocolVersion(uri -> "2"); } + + public Call.Factory createCallFactoryWithNetworkTiming(OkHttpClient.Builder clientBuilder) { + clientBuilder.protocols(singletonList(Protocol.H2_PRIOR_KNOWLEDGE)); + return OkHttpTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedRequestHeaders(singletonList(AbstractHttpClientTest.TEST_REQUEST_HEADER)) + .setCapturedResponseHeaders(singletonList(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) + .build() + .newCallFactoryWithNetworkTiming(clientBuilder.build()); + } + + @Test + void networkTimingClient() throws Exception { + okhttp3.Request request = + new okhttp3.Request.Builder().url(resolveAddress("/success").toString()).build(); + okhttp3.Response response = + createCallFactoryWithNetworkTiming(new OkHttpClient.Builder()).newCall(request).execute(); + assertThat(response.code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + assertClientSpan(span, resolveAddress("/success"), "GET", 200, null) + .hasNoParent() + .hasAttributesSatisfying( + attrs -> { + boolean hasTiming = + attrs.asMap().keySet().stream() + .map(AttributeKey::getKey) + .anyMatch( + k -> k.endsWith(".start_time") || k.endsWith(".end_time")); + assertThat(hasTiming).isTrue(); + }); + }, + span -> assertServerSpan(span).hasParent(trace.getSpan(0))); + }); + } } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetry.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetry.java index 6bb85070e31b..5886e0eb7ad2 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetry.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttpTelemetry.java @@ -9,6 +9,7 @@ import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.okhttp.v3_0.internal.ConnectionErrorSpanInterceptor; +import io.opentelemetry.instrumentation.okhttp.v3_0.internal.NetworkTimingEventListener; import io.opentelemetry.instrumentation.okhttp.v3_0.internal.TracingInterceptor; import okhttp3.Call; import okhttp3.Callback; @@ -59,4 +60,27 @@ public Call.Factory newCallFactory(OkHttpClient baseClient) { OkHttpClient tracingClient = builder.build(); return new TracingCallFactory(tracingClient); } + + /** + * Construct a new OpenTelemetry tracing-enabled {@link okhttp3.Call.Factory} using the provided + * {@link OkHttpClient} instance, with a NetworkTimingEventListener added to capture timing + * attributes. + * + *

Using this method will result in proper propagation and span parenting, for both {@linkplain + * Call#execute() synchronous} and {@linkplain Call#enqueue(Callback) asynchronous} usages. + * + * @param baseClient An instance of OkHttpClient configured as desired. + * @return a {@link Call.Factory} for creating new {@link Call} instances. + */ + public Call.Factory newCallFactoryWithNetworkTiming(OkHttpClient baseClient) { + OkHttpClient.Builder builder = baseClient.newBuilder(); + // add our interceptors before other interceptors + builder.interceptors().add(0, new ContextInterceptor()); + builder.interceptors().add(1, new ConnectionErrorSpanInterceptor(instrumenter)); + builder.networkInterceptors().add(0, new TracingInterceptor(instrumenter, propagators)); + // Add NetworkTimingEventListener to capture timing attributes + builder.eventListenerFactory(new NetworkTimingEventListener.Factory()); + OkHttpClient tracingClient = builder.build(); + return new TracingCallFactory(tracingClient); + } } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/TracingCallFactory.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/TracingCallFactory.java index 479e107c51ae..768bc5ec009a 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/TracingCallFactory.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/TracingCallFactory.java @@ -76,16 +76,24 @@ public void cancel() { } @Override - public Call clone() throws CloneNotSupportedException { + public Call clone() { if (cloneMethod == null) { - return (Call) super.clone(); + try { + return (Call) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } } try { // we pull the current context here, because the cloning might be happening in a different // context than the original call creation. return new TracingCall((Call) cloneMethod.invoke(delegate), Context.current()); } catch (IllegalAccessException | InvocationTargetException e) { - return (Call) super.clone(); + try { + return (Call) super.clone(); + } catch (CloneNotSupportedException cloneException) { + throw new AssertionError(cloneException); + } } } diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/NetworkTimingEventListener.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/NetworkTimingEventListener.java new file mode 100644 index 000000000000..0f9485208a98 --- /dev/null +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/NetworkTimingEventListener.java @@ -0,0 +1,179 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.okhttp.v3_0.internal; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.List; +import javax.annotation.Nullable; +import okhttp3.Call; +import okhttp3.Connection; +import okhttp3.EventListener; +import okhttp3.Handshake; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class NetworkTimingEventListener extends EventListener { + + // Raw timestamp attribute keys + private static final AttributeKey CALL_START = AttributeKey.longKey("http.call.start_time"); + private static final AttributeKey DNS_START = AttributeKey.longKey("http.dns.start_time"); + private static final AttributeKey DNS_END = AttributeKey.longKey("http.dns.end_time"); + private static final AttributeKey CONNECT_START = + AttributeKey.longKey("http.connect.start_time"); + private static final AttributeKey CONNECT_END = + AttributeKey.longKey("http.connect.end_time"); + private static final AttributeKey SECURE_CONNECT_START = + AttributeKey.longKey("http.secure_connect.start_time"); + private static final AttributeKey SECURE_CONNECT_END = + AttributeKey.longKey("http.secure_connect.end_time"); + private static final AttributeKey REQUEST_HEADERS_START = + AttributeKey.longKey("http.request.headers.start_time"); + private static final AttributeKey REQUEST_HEADERS_END = + AttributeKey.longKey("http.request.headers.end_time"); + private static final AttributeKey REQUEST_BODY_START = + AttributeKey.longKey("http.request.body.start_time"); + private static final AttributeKey REQUEST_BODY_END = + AttributeKey.longKey("http.request.body.end_time"); + private static final AttributeKey RESPONSE_HEADERS_START = + AttributeKey.longKey("http.response.headers.start_time"); + private static final AttributeKey RESPONSE_HEADERS_END = + AttributeKey.longKey("http.response.headers.end_time"); + private static final AttributeKey RESPONSE_BODY_START = + AttributeKey.longKey("http.response.body.start_time"); + private static final AttributeKey RESPONSE_BODY_END = + AttributeKey.longKey("http.response.body.end_time"); + private static final AttributeKey CALL_END = AttributeKey.longKey("http.call.end_time"); + + // Singleton instance of stateless NetworkTimingEventListener + private static final NetworkTimingEventListener INSTANCE = new NetworkTimingEventListener(); + + private NetworkTimingEventListener() {} + + @Override + public void callStart(Call call) { + Span.current().setAttribute(CALL_START, System.currentTimeMillis()); + } + + @Override + public void dnsStart(Call call, String domainName) { + Span.current().setAttribute(DNS_START, System.currentTimeMillis()); + } + + @Override + public void dnsEnd(Call call, String domainName, List inetAddressList) { + Span.current().setAttribute(DNS_END, System.currentTimeMillis()); + } + + @Override + public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) { + Span.current().setAttribute(CONNECT_START, System.currentTimeMillis()); + } + + @Override + public void secureConnectStart(Call call) { + Span.current().setAttribute(SECURE_CONNECT_START, System.currentTimeMillis()); + } + + @Override + public void secureConnectEnd(Call call, @Nullable Handshake handshake) { + Span.current().setAttribute(SECURE_CONNECT_END, System.currentTimeMillis()); + } + + @Override + public void connectEnd( + Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol) { + Span.current().setAttribute(CONNECT_END, System.currentTimeMillis()); + } + + @Override + public void connectFailed( + Call call, + InetSocketAddress inetSocketAddress, + Proxy proxy, + @Nullable Protocol protocol, + IOException ioe) {} + + @Override + public void connectionAcquired(Call call, Connection connection) {} + + @Override + public void connectionReleased(Call call, Connection connection) {} + + @Override + public void requestHeadersStart(Call call) { + Span.current().setAttribute(REQUEST_HEADERS_START, System.currentTimeMillis()); + } + + @Override + public void requestHeadersEnd(Call call, Request request) { + Span.current().setAttribute(REQUEST_HEADERS_END, System.currentTimeMillis()); + } + + @Override + public void requestBodyStart(Call call) { + Span.current().setAttribute(REQUEST_BODY_START, System.currentTimeMillis()); + } + + @Override + public void requestBodyEnd(Call call, long byteCount) { + Span.current().setAttribute(REQUEST_BODY_END, System.currentTimeMillis()); + } + + @Override + public void responseHeadersStart(Call call) { + Span.current().setAttribute(RESPONSE_HEADERS_START, System.currentTimeMillis()); + } + + @Override + public void responseHeadersEnd(Call call, Response response) { + Span.current().setAttribute(RESPONSE_HEADERS_END, System.currentTimeMillis()); + } + + @Override + public void responseBodyStart(Call call) { + Span.current().setAttribute(RESPONSE_BODY_START, System.currentTimeMillis()); + } + + @Override + public void responseBodyEnd(Call call, long byteCount) { + Span.current().setAttribute(RESPONSE_BODY_END, System.currentTimeMillis()); + } + + @Override + public void callEnd(Call call) { + Span.current().setAttribute(CALL_END, System.currentTimeMillis()); + } + + /** + * Factory for creating NetworkTimingEventListener instances. A singleton instance is returned as + * the listener is stateless and thread-safe. + * + *

NetworkTimingEventListener captures raw network timing timestamps and adds them as + * attributes to the current OpenTelemetry span. + * + *

Works with both synchronous and asynchronous OkHttp calls when used with proper context + * propagation. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ + public static final class Factory implements EventListener.Factory { + @Override + public EventListener create(Call call) { + return INSTANCE; + } + } +} diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpAttributesGetter.java b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpAttributesGetter.java index 0043f7dd52ed..2443a897a8f9 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpAttributesGetter.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/main/java/io/opentelemetry/instrumentation/okhttp/v3_0/internal/OkHttpAttributesGetter.java @@ -62,12 +62,13 @@ public String getNetworkProtocolName(Interceptor.Chain chain, @Nullable Response return "http"; case SPDY_3: return "spdy"; + default: + // added in 3.11.0 + if ("H2_PRIOR_KNOWLEDGE".equals(response.protocol().name())) { + return "http"; + } + return null; } - // added in 3.11.0 - if ("H2_PRIOR_KNOWLEDGE".equals(response.protocol().name())) { - return "http"; - } - return null; } @Nullable @@ -85,12 +86,13 @@ public String getNetworkProtocolVersion(Interceptor.Chain chain, @Nullable Respo return "2"; case SPDY_3: return "3.1"; + default: + // added in 3.11.0 + if ("H2_PRIOR_KNOWLEDGE".equals(response.protocol().name())) { + return "2"; + } + return null; } - // added in 3.11.0 - if ("H2_PRIOR_KNOWLEDGE".equals(response.protocol().name())) { - return "2"; - } - return null; } @Override diff --git a/instrumentation/okhttp/okhttp-3.0/library/src/test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.java b/instrumentation/okhttp/okhttp-3.0/library/src/test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.java index b2f8bbddc636..9334af46d627 100644 --- a/instrumentation/okhttp/okhttp-3.0/library/src/test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.java +++ b/instrumentation/okhttp/okhttp-3.0/library/src/test/java/io/opentelemetry/instrumentation/okhttp/v3_0/OkHttp3Test.java @@ -6,13 +6,16 @@ package io.opentelemetry.instrumentation.okhttp.v3_0; import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Protocol; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; class OkHttp3Test extends AbstractOkHttp3Test { @@ -29,4 +32,41 @@ public Call.Factory createCallFactory(OkHttpClient.Builder clientBuilder) { .build() .newCallFactory(clientBuilder.build()); } + + public Call.Factory createCallFactoryWithNetworkTiming(OkHttpClient.Builder clientBuilder) { + clientBuilder.protocols(singletonList(Protocol.HTTP_1_1)); + return OkHttpTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedRequestHeaders(singletonList(AbstractHttpClientTest.TEST_REQUEST_HEADER)) + .setCapturedResponseHeaders(singletonList(AbstractHttpClientTest.TEST_RESPONSE_HEADER)) + .build() + .newCallFactoryWithNetworkTiming(clientBuilder.build()); + } + + @Test + void networkTimingClient() throws Exception { + okhttp3.Request request = + new okhttp3.Request.Builder().url(resolveAddress("/success").toString()).build(); + okhttp3.Response response = + createCallFactoryWithNetworkTiming(new OkHttpClient.Builder()).newCall(request).execute(); + assertThat(response.code()).isEqualTo(200); + + testing.waitAndAssertTraces( + trace -> { + trace.hasSpansSatisfyingExactly( + span -> { + assertClientSpan(span, resolveAddress("/success"), "GET", 200, null) + .hasNoParent() + .hasAttributesSatisfying( + attrs -> { + boolean hasTiming = + attrs.asMap().keySet().stream() + .map(AttributeKey::getKey) + .anyMatch( + k -> k.endsWith(".start_time") || k.endsWith(".end_time")); + assertThat(hasTiming).isTrue(); + }); + }, + span -> assertServerSpan(span).hasParent(trace.getSpan(0))); + }); + } }