diff --git a/instrumentation/httpurlconnection/library/src/main/java/io/opentelemetry/instrumentation/library/httpurlconnection/HttpUrlReplacements.java b/instrumentation/httpurlconnection/library/src/main/java/io/opentelemetry/instrumentation/library/httpurlconnection/HttpUrlReplacements.java index 3b7305664..7d144a78e 100644 --- a/instrumentation/httpurlconnection/library/src/main/java/io/opentelemetry/instrumentation/library/httpurlconnection/HttpUrlReplacements.java +++ b/instrumentation/httpurlconnection/library/src/main/java/io/opentelemetry/instrumentation/library/httpurlconnection/HttpUrlReplacements.java @@ -6,10 +6,10 @@ package io.opentelemetry.instrumentation.library.httpurlconnection; import static io.opentelemetry.instrumentation.library.httpurlconnection.internal.HttpUrlConnectionSingletons.instrumenter; +import static io.opentelemetry.instrumentation.library.httpurlconnection.internal.HttpUrlConnectionSingletons.openTelemetryInstance; import android.annotation.SuppressLint; import android.os.SystemClock; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.library.httpurlconnection.internal.RequestPropertySetter; import java.io.IOException; @@ -301,7 +301,8 @@ private static void startTracingAtFirstConnection(URLConnection connection) { } private static void injectContextToRequest(URLConnection connection, Context context) { - GlobalOpenTelemetry.getPropagators() + openTelemetryInstance() + .getPropagators() .getTextMapPropagator() .inject(context, connection, RequestPropertySetter.INSTANCE); } diff --git a/instrumentation/httpurlconnection/library/src/main/java/io/opentelemetry/instrumentation/library/httpurlconnection/internal/HttpUrlConnectionSingletons.java b/instrumentation/httpurlconnection/library/src/main/java/io/opentelemetry/instrumentation/library/httpurlconnection/internal/HttpUrlConnectionSingletons.java index adb61b0fb..af9054348 100644 --- a/instrumentation/httpurlconnection/library/src/main/java/io/opentelemetry/instrumentation/library/httpurlconnection/internal/HttpUrlConnectionSingletons.java +++ b/instrumentation/httpurlconnection/library/src/main/java/io/opentelemetry/instrumentation/library/httpurlconnection/internal/HttpUrlConnectionSingletons.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.library.httpurlconnection.internal; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientExperimentalMetrics; import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpClientPeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.http.HttpExperimentalAttributesExtractor; @@ -22,11 +23,15 @@ public final class HttpUrlConnectionSingletons { - private static final Instrumenter INSTRUMENTER; + private static volatile Instrumenter instrumenter; private static final String INSTRUMENTATION_NAME = "io.opentelemetry.android.http-url-connection"; + private static final Object lock = new Object(); + private static OpenTelemetry openTelemetryInstance; + + public static Instrumenter createInstrumenter( + OpenTelemetry opentelemetry) { - static { HttpUrlHttpAttributesGetter httpAttributesGetter = new HttpUrlHttpAttributesGetter(); HttpSpanNameExtractorBuilder httpSpanNameExtractorBuilder = @@ -48,9 +53,11 @@ public final class HttpUrlConnectionSingletons { httpAttributesGetter, HttpUrlInstrumentationConfig.newPeerServiceResolver()); + openTelemetryInstance = (opentelemetry == null) ? GlobalOpenTelemetry.get() : opentelemetry; + InstrumenterBuilder builder = Instrumenter.builder( - GlobalOpenTelemetry.get(), + openTelemetryInstance, INSTRUMENTATION_NAME, httpSpanNameExtractorBuilder.build()) .setSpanStatusExtractor( @@ -65,11 +72,27 @@ public final class HttpUrlConnectionSingletons { .addOperationMetrics(HttpClientExperimentalMetrics.get()); } - INSTRUMENTER = builder.buildClientInstrumenter(RequestPropertySetter.INSTANCE); + return builder.buildClientInstrumenter(RequestPropertySetter.INSTANCE); } public static Instrumenter instrumenter() { - return INSTRUMENTER; + if (instrumenter == null) { + synchronized (lock) { + if (instrumenter == null) { + instrumenter = createInstrumenter(null); + } + } + } + return instrumenter; + } + + public static OpenTelemetry openTelemetryInstance() { + return openTelemetryInstance; + } + + // Used for setting the instrumenter for testing purposes only. + public static void setInstrumenterForTesting(OpenTelemetry opentelemetry) { + instrumenter = createInstrumenter(opentelemetry); } private HttpUrlConnectionSingletons() {} diff --git a/instrumentation/httpurlconnection/testing/build.gradle.kts b/instrumentation/httpurlconnection/testing/build.gradle.kts new file mode 100644 index 000000000..42c52ee01 --- /dev/null +++ b/instrumentation/httpurlconnection/testing/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.android-app-conventions") + id("net.bytebuddy.byte-buddy-gradle-plugin") +} + +dependencies { + byteBuddy(project(":instrumentation:httpurlconnection:agent")) + implementation(project(":instrumentation:httpurlconnection:library")) + implementation(project(":test-common")) +} diff --git a/instrumentation/httpurlconnection/testing/src/androidTest/kotlin/io/opentelemetry/instrumentation/library/httpurlconnection/InstrumentationTest.kt b/instrumentation/httpurlconnection/testing/src/androidTest/kotlin/io/opentelemetry/instrumentation/library/httpurlconnection/InstrumentationTest.kt new file mode 100644 index 000000000..0cfa4ab09 --- /dev/null +++ b/instrumentation/httpurlconnection/testing/src/androidTest/kotlin/io/opentelemetry/instrumentation/library/httpurlconnection/InstrumentationTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.library.httpurlconnection + +import io.opentelemetry.android.test.common.OpenTelemetryTestUtils +import io.opentelemetry.instrumentation.library.httpurlconnection.HttpUrlConnectionTestUtil.executeGet +import io.opentelemetry.instrumentation.library.httpurlconnection.HttpUrlConnectionTestUtil.post +import io.opentelemetry.instrumentation.library.httpurlconnection.internal.HttpUrlConnectionSingletons +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter +import org.junit.Assert +import org.junit.Test +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +class InstrumentationTest { + @Test + fun testHttpUrlConnectionGetRequest_ShouldBeTraced() { + val inMemorySpanExporter = InMemorySpanExporter.create() + HttpUrlConnectionSingletons.setInstrumenterForTesting(OpenTelemetryTestUtils.setUpSpanExporter(inMemorySpanExporter)) + executeGet("http://httpbin.org/get") + Assert.assertEquals(1, inMemorySpanExporter.finishedSpanItems.size) + inMemorySpanExporter.shutdown() + } + + @Test + fun testHttpUrlConnectionPostRequest_ShouldBeTraced() { + val inMemorySpanExporter = InMemorySpanExporter.create() + HttpUrlConnectionSingletons.setInstrumenterForTesting(OpenTelemetryTestUtils.setUpSpanExporter(inMemorySpanExporter)) + post("http://httpbin.org/post") + Assert.assertEquals(1, inMemorySpanExporter.finishedSpanItems.size) + inMemorySpanExporter.shutdown() + } + + @Test + fun testHttpUrlConnectionGetRequest_WhenNoStreamFetchedAndNoDisconnectCalled_ShouldNotBeTraced() { + val inMemorySpanExporter = InMemorySpanExporter.create() + HttpUrlConnectionSingletons.setInstrumenterForTesting(OpenTelemetryTestUtils.setUpSpanExporter(inMemorySpanExporter)) + executeGet("http://httpbin.org/get", false, false) + Assert.assertEquals(0, inMemorySpanExporter.finishedSpanItems.size) + inMemorySpanExporter.shutdown() + } + + @Test + fun testHttpUrlConnectionGetRequest_WhenNoStreamFetchedButDisconnectCalled_ShouldBeTraced() { + val inMemorySpanExporter = InMemorySpanExporter.create() + HttpUrlConnectionSingletons.setInstrumenterForTesting(OpenTelemetryTestUtils.setUpSpanExporter(inMemorySpanExporter)) + executeGet("http://httpbin.org/get", false) + Assert.assertEquals(1, inMemorySpanExporter.finishedSpanItems.size) + inMemorySpanExporter.shutdown() + } + + @Test + fun testHttpUrlConnectionGetRequest_WhenFourConcurrentRequestsAreMade_AllShouldBeTraced() { + val inMemorySpanExporter = InMemorySpanExporter.create() + HttpUrlConnectionSingletons.setInstrumenterForTesting(OpenTelemetryTestUtils.setUpSpanExporter(inMemorySpanExporter)) + val executor = Executors.newFixedThreadPool(4) + try { + executor.submit { executeGet("http://httpbin.org/get") } + executor.submit { executeGet("http://google.com") } + executor.submit { executeGet("http://android.com") } + executor.submit { executeGet("http://httpbin.org/headers") } + + executor.shutdown() + // Wait for all tasks to finish execution or timeout + if (executor.awaitTermination(2, TimeUnit.SECONDS)) { + // if all tasks finish before timeout + Assert.assertEquals(4, inMemorySpanExporter.finishedSpanItems.size) + } else { + // if all tasks don't finish before timeout + Assert.fail( + "Test could not be completed as tasks did not complete within the 2s timeout period.", + ) + } + } catch (e: InterruptedException) { + // print stack trace to decipher lines that threw InterruptedException as it can be + // possibly thrown by multiple calls above. + e.printStackTrace() + Assert.fail("Test could not be completed due to an interrupted exception.") + } finally { + if (!executor.isShutdown) { + executor.shutdownNow() + } + inMemorySpanExporter.shutdown() + } + } + + @Test + fun testHttpUrlConnectionRequest_ContextPropagationHappensAsExpected() { + val inMemorySpanExporter = InMemorySpanExporter.create() + HttpUrlConnectionSingletons.setInstrumenterForTesting(OpenTelemetryTestUtils.setUpSpanExporter(inMemorySpanExporter)) + val parentSpan = OpenTelemetryTestUtils.getSpan() + + parentSpan.makeCurrent().use { + executeGet("http://httpbin.org/get") + val spanDataList = inMemorySpanExporter.finishedSpanItems + if (spanDataList.isNotEmpty()) { + val currentSpanData = spanDataList[0] + Assert.assertEquals( + parentSpan.spanContext.traceId, + currentSpanData.traceId, + ) + } + } + parentSpan.end() + + Assert.assertEquals(2, inMemorySpanExporter.finishedSpanItems.size) + inMemorySpanExporter.shutdown() + } +} diff --git a/instrumentation/httpurlconnection/testing/src/main/AndroidManifest.xml b/instrumentation/httpurlconnection/testing/src/main/AndroidManifest.xml new file mode 100644 index 000000000..7feacd62c --- /dev/null +++ b/instrumentation/httpurlconnection/testing/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/instrumentation/httpurlconnection/testing/src/main/kotlin/io/opentelemetry/instrumentation/library/httpurlconnection/HttpUrlConnectionTestUtil.kt b/instrumentation/httpurlconnection/testing/src/main/kotlin/io/opentelemetry/instrumentation/library/httpurlconnection/HttpUrlConnectionTestUtil.kt new file mode 100644 index 000000000..0540ba73e --- /dev/null +++ b/instrumentation/httpurlconnection/testing/src/main/kotlin/io/opentelemetry/instrumentation/library/httpurlconnection/HttpUrlConnectionTestUtil.kt @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.library.httpurlconnection + +import android.util.Log +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import java.nio.charset.StandardCharsets + +object HttpUrlConnectionTestUtil { + private const val TAG = "HttpUrlConnectionTest" + + fun executeGet( + inputUrl: String, + getInputStream: Boolean = true, + disconnect: Boolean = true, + ) { + var connection: HttpURLConnection? = null + try { + connection = URL(inputUrl).openConnection() as HttpURLConnection + + // always call one API that reads from the connection + val responseCode = connection.responseCode + + val readInput = if (getInputStream) connection.inputStream.bufferedReader(StandardCharsets.UTF_8).use { it.readText() } else "" + + Log.d(TAG, "response code: $responseCode ,input Stream: $readInput") + } catch (e: IOException) { + Log.e(TAG, "Exception occurred while executing GET request", e) + } finally { + connection?.takeIf { disconnect }?.disconnect() + } + } + + fun post(inputUrl: String) { + var connection: HttpURLConnection? = null + try { + connection = URL(inputUrl).openConnection() as HttpURLConnection + connection.doOutput = true + connection.requestMethod = "POST" + + connection.outputStream.bufferedWriter(StandardCharsets.UTF_8).use { out -> out.write("Writing content to output stream!") } + + // always call one API that reads from the connection + val readInput = connection.inputStream.bufferedReader(StandardCharsets.UTF_8).use { it.readText() } + + Log.d(TAG, "InputStream: $readInput") + } catch (e: IOException) { + Log.e(TAG, "Exception occurred while executing post", e) + } finally { + connection?.disconnect() + } + } +} diff --git a/instrumentation/okhttp/okhttp-3.0/testing/build.gradle.kts b/instrumentation/okhttp/okhttp-3.0/testing/build.gradle.kts index 09df2d971..bc4c2c45f 100644 --- a/instrumentation/okhttp/okhttp-3.0/testing/build.gradle.kts +++ b/instrumentation/okhttp/okhttp-3.0/testing/build.gradle.kts @@ -9,4 +9,5 @@ dependencies { implementation(libs.okhttp) implementation(libs.opentelemetry.exporter.otlp) androidTestImplementation(libs.okhttp.mockwebserver) + implementation(project(":test-common")) } diff --git a/instrumentation/okhttp/okhttp-3.0/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.java b/instrumentation/okhttp/okhttp-3.0/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.java index 38857b82e..d50d015a4 100644 --- a/instrumentation/okhttp/okhttp-3.0/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.java +++ b/instrumentation/okhttp/okhttp-3.0/testing/src/androidTest/java/io/opentelemetry/instrumentation/library/okhttp/v3_0/InstrumentationTest.java @@ -8,16 +8,12 @@ import static org.junit.Assert.assertEquals; import androidx.annotation.NonNull; -import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.android.test.common.OpenTelemetryTestUtils; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.Scope; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; -import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; -import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.IOException; import java.util.concurrent.CountDownLatch; import okhttp3.Call; @@ -49,10 +45,10 @@ public void tearDown() throws IOException { @Test public void okhttpTraces() throws IOException { - setUpSpanExporter(inMemorySpanExporter); + OpenTelemetryTestUtils.setUpSpanExporter(inMemorySpanExporter); server.enqueue(new MockResponse().setResponseCode(200)); - Span span = getSpan(); + Span span = OpenTelemetryTestUtils.getSpan(); try (Scope ignored = span.makeCurrent()) { OkHttpClient client = @@ -76,9 +72,9 @@ public void okhttpTraces() throws IOException { @Test public void okhttpTraces_with_callback() throws InterruptedException { - setUpSpanExporter(inMemorySpanExporter); + OpenTelemetryTestUtils.setUpSpanExporter(inMemorySpanExporter); CountDownLatch lock = new CountDownLatch(1); - Span span = getSpan(); + Span span = OpenTelemetryTestUtils.getSpan(); try (Scope ignored = span.makeCurrent()) { server.enqueue(new MockResponse().setResponseCode(200)); @@ -125,13 +121,13 @@ public void avoidCreatingSpansForInternalOkhttpRequests() throws InterruptedExce // so it should be run isolated to actually get it to fail when it's expected to fail. OtlpHttpSpanExporter exporter = OtlpHttpSpanExporter.builder().setEndpoint(server.url("").toString()).build(); - setUpSpanExporter(exporter); + OpenTelemetryTestUtils.setUpSpanExporter(exporter); server.enqueue(new MockResponse().setResponseCode(200)); // This span should trigger 1 export okhttp call, which is the only okhttp call expected // for this test case. - getSpan().end(); + OpenTelemetryTestUtils.getSpan().end(); // Wait for unwanted extra okhttp requests. int loop = 0; @@ -147,28 +143,8 @@ public void avoidCreatingSpansForInternalOkhttpRequests() throws InterruptedExce assertEquals(1, server.getRequestCount()); } - private static Span getSpan() { - return GlobalOpenTelemetry.getTracer("TestTracer").spanBuilder("A Span").startSpan(); - } - - private void setUpSpanExporter(SpanExporter spanExporter) { - OpenTelemetrySdk openTelemetry = - OpenTelemetrySdk.builder() - .setTracerProvider(getSimpleTracerProvider(spanExporter)) - .build(); - GlobalOpenTelemetry.resetForTest(); - GlobalOpenTelemetry.set(openTelemetry); - } - private Call createCall(OkHttpClient client, String urlPath) { Request request = new Request.Builder().url(server.url(urlPath)).build(); return client.newCall(request); } - - @NonNull - private SdkTracerProvider getSimpleTracerProvider(SpanExporter spanExporter) { - return SdkTracerProvider.builder() - .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) - .build(); - } } diff --git a/settings.gradle.kts b/settings.gradle.kts index acf4849fc..1837c7041 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,3 +16,5 @@ include(":instrumentation:startup") include(":instrumentation:volley:library") include(":instrumentation:httpurlconnection:agent") include(":instrumentation:httpurlconnection:library") +include(":instrumentation:httpurlconnection:testing") +include(":test-common") diff --git a/test-common/build.gradle.kts b/test-common/build.gradle.kts new file mode 100644 index 000000000..dd41ad0aa --- /dev/null +++ b/test-common/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("otel.android-library-conventions") +} + +description = "OpenTelemetry Android common test utils" + +android { + namespace = "io.opentelemetry.android.test.common" +} + +dependencies { + api(platform(libs.opentelemetry.platform)) + api(libs.opentelemetry.sdk) + api(libs.opentelemetry.api) + implementation(libs.androidx.core) +} diff --git a/test-common/src/main/kotlin/io/opentelemetry/android/test/common/OpenTelemetryTestUtils.kt b/test-common/src/main/kotlin/io/opentelemetry/android/test/common/OpenTelemetryTestUtils.kt new file mode 100644 index 000000000..6d95cd49b --- /dev/null +++ b/test-common/src/main/kotlin/io/opentelemetry/android/test/common/OpenTelemetryTestUtils.kt @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.test.common + +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.trace.Span +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor +import io.opentelemetry.sdk.trace.export.SpanExporter + +object OpenTelemetryTestUtils { + lateinit var openTelemetry: OpenTelemetry + + @JvmStatic + fun getSpan(): Span { + return openTelemetry.getTracer("TestTracer").spanBuilder("A Span").startSpan() + } + + @JvmStatic + fun setUpSpanExporter(spanExporter: SpanExporter): OpenTelemetry { + openTelemetry = + OpenTelemetrySdk.builder() + .setTracerProvider(getSimpleTracerProvider(spanExporter)) + .build() + + // TODO: Remove the bottom two lines after making okhttp3 androidTests parallel too. + GlobalOpenTelemetry.resetForTest() + GlobalOpenTelemetry.set(openTelemetry) + + return openTelemetry + } + + private fun getSimpleTracerProvider(spanExporter: SpanExporter): SdkTracerProvider { + return SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build() + } +}