Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OkHttp migration to instrumentaion API #505

Merged
merged 16 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion instrumentation/okhttp/okhttp-3.0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,12 @@ automatically.
### Configuration

You can configure the automatic instrumentation by using the setters
in [OkHttpInstrumentationConfig](library/src/main/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/OkHttpInstrumentationConfig.java)).
from
the [OkHttpInstrumentation](library/src/main/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/OkHttpInstrumentation.java))
instance provided via the AndroidInstrumentationRegistry as shown below:

```java
OkHttpInstrumentation instrumentation = AndroidInstrumentationRegistry.get().get(OkHttpInstrumentation.class);

// Call `instrumentation` setters.
```
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ android {
dependencies {
compileOnly(libs.okhttp)
api(libs.opentelemetry.instrumentation.okhttp)
implementation(project(":core"))
implementation(libs.opentelemetry.instrumentation.apiSemconv)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

package io.opentelemetry.instrumentation.library.okhttp.v3_0;

import android.app.Application;
import com.google.auto.service.AutoService;
import io.opentelemetry.android.OpenTelemetryRum;
import io.opentelemetry.android.instrumentation.AndroidInstrumentation;
import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceResolver;
import io.opentelemetry.instrumentation.api.internal.HttpConstants;
import java.util.ArrayList;
Expand All @@ -13,16 +17,17 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.NotNull;

/** Configuration for automatic instrumentation of okhttp requests. */
public final class OkHttpInstrumentationConfig {
private static List<String> capturedRequestHeaders = new ArrayList<>();
private static List<String> capturedResponseHeaders = new ArrayList<>();
private static Set<String> knownMethods = HttpConstants.KNOWN_METHODS;
private static Map<String, String> peerServiceMapping = new HashMap<>();
private static boolean emitExperimentalHttpClientMetrics;

private OkHttpInstrumentationConfig() {}
/** Instrumentation for okhttp requests. */
@AutoService(AndroidInstrumentation.class)
public class OkHttpInstrumentation implements AndroidInstrumentation {
private List<String> capturedRequestHeaders = new ArrayList<>();
private List<String> capturedResponseHeaders = new ArrayList<>();
private Set<String> knownMethods = HttpConstants.KNOWN_METHODS;
private Map<String, String> peerServiceMapping = new HashMap<>();
private boolean emitExperimentalHttpClientMetrics;
private OpenTelemetryRum openTelemetryRum = OpenTelemetryRum.noop();

/**
* Configures the HTTP request headers that will be captured as span attributes as described in
Expand All @@ -36,11 +41,11 @@ private OkHttpInstrumentationConfig() {}
*
* @param requestHeaders A list of HTTP header names.
*/
public static void setCapturedRequestHeaders(List<String> requestHeaders) {
OkHttpInstrumentationConfig.capturedRequestHeaders = new ArrayList<>(requestHeaders);
public void setCapturedRequestHeaders(List<String> requestHeaders) {
capturedRequestHeaders = new ArrayList<>(requestHeaders);
}

public static List<String> getCapturedRequestHeaders() {
public List<String> getCapturedRequestHeaders() {
return capturedRequestHeaders;
}

Expand All @@ -56,11 +61,11 @@ public static List<String> getCapturedRequestHeaders() {
*
* @param responseHeaders A list of HTTP header names.
*/
public static void setCapturedResponseHeaders(List<String> responseHeaders) {
OkHttpInstrumentationConfig.capturedResponseHeaders = new ArrayList<>(responseHeaders);
public void setCapturedResponseHeaders(List<String> responseHeaders) {
capturedResponseHeaders = new ArrayList<>(responseHeaders);
}

public static List<String> getCapturedResponseHeaders() {
public List<String> getCapturedResponseHeaders() {
return capturedResponseHeaders;
}

Expand All @@ -79,11 +84,11 @@ public static List<String> getCapturedResponseHeaders() {
*
* @param knownMethods A set of recognized HTTP request methods.
*/
public static void setKnownMethods(Set<String> knownMethods) {
OkHttpInstrumentationConfig.knownMethods = new HashSet<>(knownMethods);
public void setKnownMethods(Set<String> knownMethods) {
this.knownMethods = new HashSet<>(knownMethods);
}

public static Set<String> getKnownMethods() {
public Set<String> getKnownMethods() {
return knownMethods;
}

Expand All @@ -92,11 +97,11 @@ public static Set<String> getKnownMethods() {
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/span-general.md#general-remote-service-attributes">the
* specification</a>.
*/
public static void setPeerServiceMapping(Map<String, String> peerServiceMapping) {
OkHttpInstrumentationConfig.peerServiceMapping = new HashMap<>(peerServiceMapping);
public void setPeerServiceMapping(Map<String, String> peerServiceMapping) {
this.peerServiceMapping = new HashMap<>(peerServiceMapping);
}

public static PeerServiceResolver newPeerServiceResolver() {
public PeerServiceResolver newPeerServiceResolver() {
return PeerServiceResolver.create(peerServiceMapping);
}

Expand All @@ -109,13 +114,21 @@ public static PeerServiceResolver newPeerServiceResolver() {
* href="https://github.com/open-telemetry/semantic-conventions/blob/main/specification/metrics/semantic_conventions/http-metrics.md#metric-httpclientresponsesize">
* the response size</a>.
*/
public static void setEmitExperimentalHttpClientMetrics(
boolean emitExperimentalHttpClientMetrics) {
OkHttpInstrumentationConfig.emitExperimentalHttpClientMetrics =
emitExperimentalHttpClientMetrics;
public void setEmitExperimentalHttpClientMetrics(boolean emitExperimentalHttpClientMetrics) {
this.emitExperimentalHttpClientMetrics = emitExperimentalHttpClientMetrics;
}

public static boolean emitExperimentalHttpClientMetrics() {
public boolean emitExperimentalHttpClientMetrics() {
return emitExperimentalHttpClientMetrics;
}

public OpenTelemetryRum getOpenTelemetryRum() {
return openTelemetryRum;
}

@Override
public void install(
@NotNull Application application, @NotNull OpenTelemetryRum openTelemetryRum) {
this.openTelemetryRum = openTelemetryRum;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm, this feels weird to me. Installing does nothing but hold onto the instance, so that we can add a get() method to this instrumentation so that the lazy singletons can get at the instance. Shouldn't the install() method actually help do the creational work?

The lifecycle is further complicated / obfuscated by the lazy caching stuff in the singletons class.

Here's an idea: What if the install() method just passed the needed otel instance to a static factory method in the singletons class, and that is responsible for setting up the instrumenter and the dependent interceptors?

I made a demonstration PR to this branch to show you more what I'm talking about: LikeTheSalad#1

Note that in addition to the lifecycle being somewhat clearer, it also allows us to remove the otelRum instance, remove the lazy interceptor, and the cached supplier.

Copy link
Contributor Author

@LikeTheSalad LikeTheSalad Aug 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks! I took a quick look and your approach looks a lot cleaner, although I'm concerned about removing the laziness of the singletons due to the timing between the instantiation of OkHttpClient objects and the OTel rum initialization. I remember it was a key issue when using GlobalOpenTelemetry, though maybe it's no longer the case. I'd like to discuss it further in the SIG just to make sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I've applied the changes suggested here taking into account what was discussed in the SIG meeting as well 👍

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

package io.opentelemetry.instrumentation.library.okhttp.v3_0.internal;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.android.instrumentation.AndroidInstrumentationRegistry;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.semconv.http.HttpClientRequestResendCount;
import io.opentelemetry.instrumentation.api.semconv.http.HttpSpanNameExtractor;
import io.opentelemetry.instrumentation.library.okhttp.v3_0.OkHttpInstrumentationConfig;
import io.opentelemetry.instrumentation.library.okhttp.v3_0.OkHttpInstrumentation;
import io.opentelemetry.instrumentation.okhttp.v3_0.internal.ConnectionErrorSpanInterceptor;
import io.opentelemetry.instrumentation.okhttp.v3_0.internal.OkHttpAttributesGetter;
import io.opentelemetry.instrumentation.okhttp.v3_0.internal.OkHttpClientInstrumenterBuilderFactory;
Expand All @@ -30,34 +30,33 @@ public final class OkHttp3Singletons {

private static final Supplier<Instrumenter<Request, Response>> INSTRUMENTER =
CachedSupplier.create(
() ->
OkHttpClientInstrumenterBuilderFactory.create(GlobalOpenTelemetry.get())
.setCapturedRequestHeaders(
OkHttpInstrumentationConfig.getCapturedRequestHeaders())
.setCapturedResponseHeaders(
OkHttpInstrumentationConfig
.getCapturedResponseHeaders())
.setKnownMethods(OkHttpInstrumentationConfig.getKnownMethods())
// TODO: Do we really need to set the known methods on the span
// name
// extractor as well?
.setSpanNameExtractor(
x ->
HttpSpanNameExtractor.builder(
OkHttpAttributesGetter.INSTANCE)
.setKnownMethods(
OkHttpInstrumentationConfig
.getKnownMethods())
.build())
.addAttributeExtractor(
PeerServiceAttributesExtractor.create(
OkHttpAttributesGetter.INSTANCE,
OkHttpInstrumentationConfig
.newPeerServiceResolver()))
.setEmitExperimentalHttpClientMetrics(
OkHttpInstrumentationConfig
.emitExperimentalHttpClientMetrics())
.build());
() -> {
OkHttpInstrumentation instrumentation = getInstrumentation();
return OkHttpClientInstrumenterBuilderFactory.create(
instrumentation.getOpenTelemetryRum().getOpenTelemetry())
.setCapturedRequestHeaders(
instrumentation.getCapturedRequestHeaders())
.setCapturedResponseHeaders(
instrumentation.getCapturedResponseHeaders())
.setKnownMethods(instrumentation.getKnownMethods())
// TODO: Do we really need to set the known methods on the span
// name
// extractor as well?
.setSpanNameExtractor(
x ->
HttpSpanNameExtractor.builder(
OkHttpAttributesGetter.INSTANCE)
.setKnownMethods(
instrumentation.getKnownMethods())
.build())
.addAttributeExtractor(
PeerServiceAttributesExtractor.create(
OkHttpAttributesGetter.INSTANCE,
instrumentation.newPeerServiceResolver()))
.setEmitExperimentalHttpClientMetrics(
instrumentation.emitExperimentalHttpClientMetrics())
.build();
});

public static final Interceptor CALLBACK_CONTEXT_INTERCEPTOR =
chain -> {
Expand Down Expand Up @@ -89,10 +88,24 @@ public final class OkHttp3Singletons {
public static final Interceptor TRACING_INTERCEPTOR =
new LazyInterceptor<>(
CachedSupplier.create(
() ->
new TracingInterceptor(
INSTRUMENTER.get(),
GlobalOpenTelemetry.getPropagators())));
() -> {
OkHttpInstrumentation instrumentation = getInstrumentation();
return new TracingInterceptor(
INSTRUMENTER.get(),
instrumentation
.getOpenTelemetryRum()
.getOpenTelemetry()
.getPropagators());
}));

private OkHttp3Singletons() {}

private static OkHttpInstrumentation getInstrumentation() {
OkHttpInstrumentation instrumentation =
AndroidInstrumentationRegistry.get().get(OkHttpInstrumentation.class);
if (instrumentation == null) {
throw new IllegalStateException("OkHttpInstrumentation not found.");
}
return instrumentation;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.library.okhttp.v3_0;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

import io.opentelemetry.android.OpenTelemetryRum;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class OkHttpInstrumentationTest {
private OkHttpInstrumentation instrumentation;

@BeforeEach
void setUp() {
instrumentation = new OkHttpInstrumentation();
}

@Test
void validateDefaultHttpMethods() {
assertThat(instrumentation.getKnownMethods())
.containsExactlyInAnyOrder(
"CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT",
"TRACE");
}

@Test
void validateDefaultRumInstance() {
assertThat(instrumentation.getOpenTelemetryRum()).isEqualTo(OpenTelemetryRum.noop());
}

@Test
void validateInstall() {
OpenTelemetryRum rum = mock();

instrumentation.install(mock(), rum);

assertThat(instrumentation.getOpenTelemetryRum()).isEqualTo(rum);
}
}