diff --git a/android-agent/src/main/java/io/opentelemetry/android/instrumentation/AndroidInstrumentation.kt b/android-agent/src/main/java/io/opentelemetry/android/instrumentation/AndroidInstrumentation.kt new file mode 100644 index 000000000..3f4c41e07 --- /dev/null +++ b/android-agent/src/main/java/io/opentelemetry/android/instrumentation/AndroidInstrumentation.kt @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation + +import android.app.Application +import io.opentelemetry.android.OpenTelemetryRum + +/** + * This interface defines a tool that automatically generates telemetry for a specific use-case, + * without the need for end users to directly interact with the OpenTelemetry SDK to create telemetry manually. + * + * Implementations of this interface should be focused on a single use-case and should attach themselves automatically + * to the tool that they are supposed to generate telemetry for. For example, an implementation that tracks + * Fragment lifecycle methods by generating OTel events in key places of a Fragment's lifecycle, should + * come with its own "FragmentLifecycleCallbacks" implementation (or similar callback mechanism that notifies when a fragment lifecycle state has changed) + * and should find a way to register its callback into all of the Fragments of the host app to automatically + * track their lifecycle without end users having to modify their project's code to make it work. + * + * Even though users shouldn't have to write code to make an AndroidInstrumentation implementation work, + * implementations should expose configurable options whenever possible to allow users to customize relevant + * options depending on the use-case. + * + * Access to an implementation, either to configure it or to install it, must be made through + * [AndroidInstrumentationRegistry.get] or [AndroidInstrumentationRegistry.getAll]. + */ +interface AndroidInstrumentation { + /** + * This is the entry point of the instrumentation, it must be called once per implementation and it should + * only be called from [OpenTelemetryRum]'s builder once the [OpenTelemetryRum] instance is initialized and ready + * to use for generating telemetry. + * + * @param application The Android application being instrumented. + * @param openTelemetryRum The [OpenTelemetryRum] instance to use for creating signals. + */ + fun install( + application: Application, + openTelemetryRum: OpenTelemetryRum, + ) +} diff --git a/android-agent/src/main/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistry.kt b/android-agent/src/main/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistry.kt new file mode 100644 index 000000000..408ea8157 --- /dev/null +++ b/android-agent/src/main/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistry.kt @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation + +/** + * Stores and provides all the available instrumentations. + */ +interface AndroidInstrumentationRegistry { + /** + * Provides a single instrumentation if available. + * + * @param type The type of the instrumentation to retrieve. + * @return The instrumentation instance if available, null otherwise. + */ + fun get(type: Class): T? + + /** + * Provides all registered instrumentations. + * + * @return All registered instrumentations. + */ + fun getAll(): Collection + + /** + * Stores an instrumentation as long as there is not other instrumentation already registered with the same + * type. + * + * @param instrumentation The instrumentation to register. + * @throws IllegalStateException If the instrumentation couldn't be registered. + */ + fun register(instrumentation: AndroidInstrumentation) + + companion object { + private val instance: AndroidInstrumentationRegistry by lazy { + AndroidInstrumentationRegistryImpl() + } + + @JvmStatic + fun get(): AndroidInstrumentationRegistry { + return instance + } + } +} diff --git a/android-agent/src/main/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistryImpl.kt b/android-agent/src/main/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistryImpl.kt new file mode 100644 index 000000000..e265f46e7 --- /dev/null +++ b/android-agent/src/main/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistryImpl.kt @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation + +import java.util.ServiceLoader + +internal class AndroidInstrumentationRegistryImpl : AndroidInstrumentationRegistry { + private val instrumentations: MutableMap, AndroidInstrumentation> by lazy { + ServiceLoader.load(AndroidInstrumentation::class.java).associateBy { it.javaClass } + .toMutableMap() + } + + @Suppress("UNCHECKED_CAST") + override fun get(type: Class): T? { + return instrumentations[type] as? T + } + + override fun getAll(): Collection { + return instrumentations.values.toList() + } + + @Throws(IllegalStateException::class) + override fun register(instrumentation: AndroidInstrumentation) { + if (instrumentation::class.java in instrumentations) { + throw IllegalStateException("Instrumentation with type '${instrumentation::class.java}' already exists.") + } + instrumentations[instrumentation.javaClass] = instrumentation + } +} diff --git a/android-agent/src/test/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistryImplTest.kt b/android-agent/src/test/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistryImplTest.kt new file mode 100644 index 000000000..b42aa60a3 --- /dev/null +++ b/android-agent/src/test/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistryImplTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation + +import android.app.Application +import io.mockk.mockk +import io.opentelemetry.android.OpenTelemetryRum +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class AndroidInstrumentationRegistryImplTest { + private lateinit var registry: AndroidInstrumentationRegistryImpl + + @BeforeEach + fun setUp() { + registry = AndroidInstrumentationRegistryImpl() + } + + @Test + fun `Find and register implementations available in the classpath when querying an instrumentation`() { + val instrumentation = registry.get(TestAndroidInstrumentation::class.java)!! + + assertThat(instrumentation.installed).isFalse() + + instrumentation.install(mockk(), mockk()) + + assertThat(registry.get(TestAndroidInstrumentation::class.java)!!.installed).isTrue() + } + + @Test + fun `Find and register implementations available in the classpath when querying all instrumentations`() { + val instrumentations = registry.getAll() + + assertThat(instrumentations).hasSize(1) + assertThat(instrumentations.first()).isInstanceOf(TestAndroidInstrumentation::class.java) + } + + @Test + fun `Register instrumentations`() { + val instrumentation = DummyInstrumentation("test") + + registry.register(instrumentation) + + assertThat(registry.get(DummyInstrumentation::class.java)!!.name).isEqualTo("test") + } + + @Test + fun `Register only one instrumentation per type`() { + val instrumentation = DummyInstrumentation("test") + val instrumentation2 = DummyInstrumentation("test2") + + registry.register(instrumentation) + + assertThatThrownBy { + registry.register(instrumentation2) + }.isInstanceOf(IllegalStateException::class.java) + .hasMessage("Instrumentation with type '${DummyInstrumentation::class.java}' already exists.") + } + + private class DummyInstrumentation(val name: String) : AndroidInstrumentation { + override fun install( + application: Application, + openTelemetryRum: OpenTelemetryRum, + ) { + } + } +} diff --git a/android-agent/src/test/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistryTest.kt b/android-agent/src/test/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistryTest.kt new file mode 100644 index 000000000..2a17be5c2 --- /dev/null +++ b/android-agent/src/test/java/io/opentelemetry/android/instrumentation/AndroidInstrumentationRegistryTest.kt @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class AndroidInstrumentationRegistryTest { + @Test + fun `Verify singleton`() { + val registry = AndroidInstrumentationRegistry.get() + + assertThat(registry).isEqualTo(AndroidInstrumentationRegistry.get()) + } +} diff --git a/android-agent/src/test/java/io/opentelemetry/android/instrumentation/TestAndroidInstrumentation.kt b/android-agent/src/test/java/io/opentelemetry/android/instrumentation/TestAndroidInstrumentation.kt new file mode 100644 index 000000000..27b32a376 --- /dev/null +++ b/android-agent/src/test/java/io/opentelemetry/android/instrumentation/TestAndroidInstrumentation.kt @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation + +import android.app.Application +import io.opentelemetry.android.OpenTelemetryRum + +class TestAndroidInstrumentation : AndroidInstrumentation { + var installed = false + private set + + override fun install( + application: Application, + openTelemetryRum: OpenTelemetryRum, + ) { + installed = true + } +} diff --git a/android-agent/src/test/resources/META-INF/services/io.opentelemetry.android.instrumentation.AndroidInstrumentation b/android-agent/src/test/resources/META-INF/services/io.opentelemetry.android.instrumentation.AndroidInstrumentation new file mode 100644 index 000000000..ddf212907 --- /dev/null +++ b/android-agent/src/test/resources/META-INF/services/io.opentelemetry.android.instrumentation.AndroidInstrumentation @@ -0,0 +1 @@ +io.opentelemetry.android.instrumentation.TestAndroidInstrumentation \ No newline at end of file