Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.opentelemetry.android.common.RumConstants
import io.opentelemetry.android.config.OtelRumConfig
import io.opentelemetry.android.instrumentation.AndroidInstrumentation
import io.opentelemetry.android.instrumentation.AndroidInstrumentationLoader
import io.opentelemetry.android.instrumentation.InstrumentationConfigurators
import io.opentelemetry.android.session.SessionProvider
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.common.Clock
Expand Down Expand Up @@ -75,8 +76,10 @@ class SdkPreconfiguredRumBuilder internal constructor(
onShutdown.run()
}

val configurator = InstrumentationConfigurators.create()
// Install instrumentations
for (instrumentation in enabledInstrumentations) {
configurator.configure(instrumentation)
instrumentation.install(context, openTelemetryRum)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.opentelemetry.android.instrumentation

import java.util.ServiceLoader

private typealias Konfigurator = InstrumentationConfigurator<*>

internal class InstrumentationConfigurators
private constructor(private val configurators: Map<String, List<Konfigurator>>) {

companion object {
fun create(): InstrumentationConfigurators {
return create { s -> ServiceLoader.load(s) }
}

// Exists for testing
fun create(loader: Loader): InstrumentationConfigurators {
val configurators = loader.load(Konfigurator::class.java)
.groupBy { it.instrumentationName }
return InstrumentationConfigurators(configurators)
}
}

@Suppress("UNCHECKED_CAST")
fun configure(instrumentation: AndroidInstrumentation) {
configurators[instrumentation.name]
?.forEach { (it as InstrumentationConfigurator<AndroidInstrumentation>).configure(instrumentation) }
}
}

/**
* Exists for testing
*/
internal fun interface Loader {
fun load(service: Class<Konfigurator>): Iterable<Konfigurator>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.opentelemetry.android.instrumentation

import android.content.Context
import io.opentelemetry.android.OpenTelemetryRum
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class InstrumentationConfiguratorsTest {

@Test
fun testCreateAndConfigure(){
val config1 = object: InstrumentationConfigurator<TestInstrumentation1> {
override val instrumentationName: String = "inst1"

override fun configure(instrumentation: TestInstrumentation1) {
instrumentation.configured = true
}

}
val config2 = object: InstrumentationConfigurator<TestInstrumentation2> {
override val instrumentationName: String = "inst2"

override fun configure(instrumentation: TestInstrumentation2) {
instrumentation.configured = true
}

}
val inst1 = TestInstrumentation1()
val inst2 = TestInstrumentation2()
val inst3 = TestInstrumentation1()
inst3.name = "not going to find this but that's ok"

val instrumentations = listOf<InstrumentationConfigurator<*>>(config1, config2)

val loader: (Class<InstrumentationConfigurator<*>>) -> Iterable<InstrumentationConfigurator<*>> =
{ _ -> instrumentations }

val configurators = InstrumentationConfigurators.create(loader)
configurators.configure(inst1)
configurators.configure(inst2)
configurators.configure(inst3)

assertThat(inst1.configured).isTrue
assertThat(inst2.configured).isTrue

}
}

class TestInstrumentation1: AndroidInstrumentation {
override var name: String = "inst1"
var configured = false

override fun install(
context: Context,
openTelemetryRum: OpenTelemetryRum
) {
}
}

class TestInstrumentation2: AndroidInstrumentation {
override var name: String = "inst2"
var configured = false

override fun install(
context: Context,
openTelemetryRum: OpenTelemetryRum
) {
}
}
28 changes: 28 additions & 0 deletions docs/WRITING_INSTRUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,34 @@ Configuration must happen before `OpenTelemetryRum` is built. The loader discove
instrumentation instance from the classpath, and the builder installs it during
RUM initialization. So the configuration is read once per RUM initialization.

#### Service-Loaded Configuration

When you need to configure autoloaded `AndroidInstrumentation` classes before
they are installed, you can build custom instances of
[`InstrumentationConfigurator`](../instrumentation/android-instrumentation/src/main/java/io/opentelemetry/android/instrumentation/InstrumentationConfigurator.kt).

These will be automatically found on the classpath at runtime and will be
invoked prior to installation. A configurator is matched to an instrumentation by `instrumentationName`,
which must equal the target instrumentation's `AndroidInstrumentation.name`. There can be multiple
configurators for any `AndroidInstrumentation`.

Configurators must be annotated with `@AutoService`:

```kotlin
@AutoService(InstrumentationConfigurator::class)
class MyFeatureConfigurator : InstrumentationConfigurator<SomeInstrumentation> {
override val instrumentationName: String = "some-instrumentation"

override fun configure(instrumentation: SomeInstrumentation) {
instrumentation.someOption = true
}
}
```

The builder discovers configurators with `ServiceLoader` and calls `configure()` after
instrumentation discovery, but before `AndroidInstrumentation.install()`. This lets a
configurator set properties or invoke setup methods before telemetry hooks are installed.

### Telemetry

Follow OpenTelemetry semantic conventions where they exist. Keep instrumentation scope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ public abstract interface class io/opentelemetry/android/instrumentation/Android
public fun uninstall (Landroid/content/Context;Lio/opentelemetry/android/OpenTelemetryRum;)V
}

public abstract interface class io/opentelemetry/android/instrumentation/InstrumentationConfigurator {
public abstract fun configure (Lio/opentelemetry/android/instrumentation/AndroidInstrumentation;)V
public abstract fun getInstrumentationName ()Ljava/lang/String;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.opentelemetry.android.instrumentation

/**
* Configures a discovered [AndroidInstrumentation] before it is installed.
*
* @param T The instrumentation type this configurator can configure.
*/
interface InstrumentationConfigurator<in T : AndroidInstrumentation> {

/**
* The [AndroidInstrumentation.name] value of the instrumentation this configurator applies to.
* This name must exactly match the name returned by the AndroidInstrumentation.name.
*/
val instrumentationName: String

/**
* Called to configure an [instrumentation] before [AndroidInstrumentation.install] is called.
*/
fun configure(instrumentation: T)
}
4 changes: 4 additions & 0 deletions instrumentation/httpurlconnection/testing/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("otel.android-app-conventions")
id("net.bytebuddy.byte-buddy-gradle-plugin")
id("com.google.devtools.ksp")
}

android {
Expand All @@ -12,6 +13,9 @@ dependencies {
implementation(project(":instrumentation:android-instrumentation"))
implementation(project(":instrumentation:httpurlconnection:library"))
implementation(project(":test-common"))
implementation(libs.opentelemetry.instrumentation.api)
implementation(libs.auto.service.annotations)
ksp(libs.auto.service.processor)
androidTestImplementation(project(":core"))
androidTestImplementation(libs.assertj.core)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
package io.opentelemetry.instrumentation.library.httpurlconnection

import io.opentelemetry.android.instrumentation.AndroidInstrumentation
import java.util.ServiceLoader
import io.opentelemetry.android.test.common.OpenTelemetryRumRule
import io.opentelemetry.api.common.AttributeKey.booleanKey
import io.opentelemetry.instrumentation.library.httpurlconnection.HttpUrlConnectionTestUtil.executeGet
import io.opentelemetry.instrumentation.library.httpurlconnection.HttpUrlConnectionTestUtil.post
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import java.util.ServiceLoader
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -99,7 +100,7 @@ class InstrumentationTest {
.filterIsInstance<HttpUrlInstrumentation>()
.firstOrNull(),
)
// setting a -1ms connection inactivity timeout for testing to ensure harvester sees it as 1ms elapsed
// setting a -1ms connection inactivity timeout for testing to ensure harvester sees it as 1ms elapsed,
// and we don't have to include any wait timers in the test. 0ms does not work as the time difference
// between last connection activity and harvester time elapsed check is much lesser than 1ms due to
// our high speed modern CPUs.
Expand All @@ -111,4 +112,12 @@ class InstrumentationTest {
// span created with harvester thread
assertThat(openTelemetryRumRule.inMemorySpanExporter.finishedSpanItems.size).isEqualTo(1)
}

@Test
fun extraAttributesCanBeExtracted() {
executeGet("http://httpbin.org/get")
val spans = openTelemetryRumRule.inMemorySpanExporter.finishedSpanItems
assertThat(spans[0].attributes.get(booleanKey("extractor.on.start"))).isEqualTo(true)
assertThat(spans[0].attributes.get(booleanKey("extractor.on.end"))).isEqualTo(true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.opentelemetry.instrumentation.library.httpurlconnection

import com.google.auto.service.AutoService
import io.opentelemetry.android.instrumentation.InstrumentationConfigurator
import io.opentelemetry.api.common.AttributesBuilder
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor
import java.net.URLConnection

@AutoService(InstrumentationConfigurator::class)
class AddExtractor : InstrumentationConfigurator<HttpUrlInstrumentation> {
override val instrumentationName: String = "httpurlconnection"

override fun configure(instrumentation: HttpUrlInstrumentation) {
val extractor = object : AttributesExtractor<URLConnection, Int> {
override fun onStart(
builder: AttributesBuilder,
ctx: Context,
urlConnection: URLConnection,
) {
builder.put("extractor.on.start", true)
}

override fun onEnd(
builder: AttributesBuilder,
ctx: Context,
urlConnection: URLConnection,
response: Int?,
err: Throwable?,
) {
builder.put("extractor.on.end", true)
}
}
instrumentation.addAttributesExtractor(extractor)
}
}