Skip to content

Commit

Permalink
Add androidTests for httpUrlConnection module and create test-common …
Browse files Browse the repository at this point in the history
…module for common test utilities (#452)

* Add androidTests for httpUrlConnection module and pull out common utils in test-common module

* Ensure executor.shutdown also gets called finally.

* Convertion to Kotlin

* Make tests independent allowing parallel run
  • Loading branch information
surbhiia authored Jul 23, 2024
1 parent 3579223 commit fa5eedd
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,11 +23,15 @@

public final class HttpUrlConnectionSingletons {

private static final Instrumenter<URLConnection, Integer> INSTRUMENTER;
private static volatile Instrumenter<URLConnection, Integer> 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<URLConnection, Integer> createInstrumenter(
OpenTelemetry opentelemetry) {

static {
HttpUrlHttpAttributesGetter httpAttributesGetter = new HttpUrlHttpAttributesGetter();

HttpSpanNameExtractorBuilder<URLConnection> httpSpanNameExtractorBuilder =
Expand All @@ -48,9 +53,11 @@ public final class HttpUrlConnectionSingletons {
httpAttributesGetter,
HttpUrlInstrumentationConfig.newPeerServiceResolver());

openTelemetryInstance = (opentelemetry == null) ? GlobalOpenTelemetry.get() : opentelemetry;

InstrumenterBuilder<URLConnection, Integer> builder =
Instrumenter.<URLConnection, Integer>builder(
GlobalOpenTelemetry.get(),
openTelemetryInstance,
INSTRUMENTATION_NAME,
httpSpanNameExtractorBuilder.build())
.setSpanStatusExtractor(
Expand All @@ -65,11 +72,27 @@ public final class HttpUrlConnectionSingletons {
.addOperationMetrics(HttpClientExperimentalMetrics.get());
}

INSTRUMENTER = builder.buildClientInstrumenter(RequestPropertySetter.INSTANCE);
return builder.buildClientInstrumenter(RequestPropertySetter.INSTANCE);
}

public static Instrumenter<URLConnection, Integer> 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() {}
Expand Down
10 changes: 10 additions & 0 deletions instrumentation/httpurlconnection/testing/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"))
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
1 change: 1 addition & 0 deletions instrumentation/okhttp/okhttp-3.0/testing/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ dependencies {
implementation(libs.okhttp)
implementation(libs.opentelemetry.exporter.otlp)
androidTestImplementation(libs.okhttp.mockwebserver)
implementation(project(":test-common"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 =
Expand All @@ -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));
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
}
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
16 changes: 16 additions & 0 deletions test-common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit fa5eedd

Please sign in to comment.