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

Instrumentation API - Part 1 #396

Merged
merged 16 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
@@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -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 <T : AndroidInstrumentation> get(type: Class<out T>): T?

/**
* Provides all registered instrumentations.
*
* @return All registered instrumentations.
*/
fun getAll(): Collection<AndroidInstrumentation>

/**
* 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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.instrumentation

import java.util.Collections
import java.util.ServiceLoader

internal class AndroidInstrumentationRegistryImpl : AndroidInstrumentationRegistry {
private val instrumentations: MutableMap<Class<out AndroidInstrumentation>, AndroidInstrumentation> by lazy {
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
ServiceLoader.load(AndroidInstrumentation::class.java).associateBy { it.javaClass }
.toMutableMap()
breedx-splk marked this conversation as resolved.
Show resolved Hide resolved
}

@Suppress("UNCHECKED_CAST")
override fun <T : AndroidInstrumentation> get(type: Class<out T>): T? {
return instrumentations[type] as? T
}

override fun getAll(): Collection<AndroidInstrumentation> {
return Collections.unmodifiableCollection(instrumentations.values)
}
Copy link
Member

Choose a reason for hiding this comment

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

Any reason to use the Java Collection types instead of the Kotlin ones?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean kotlin.collections.Collection?

Copy link
Member

Choose a reason for hiding this comment

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

yep, the kotlin.collections package in general.
The less we depend on java.* the easier it'll be to be KMP compatible in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I was looking for a Collections.unmodifiableCollection equivalent, though it seems like toList should do. I've just made the changes. Cheers!


@Throws(IllegalStateException::class)
override fun register(instrumentation: AndroidInstrumentation) {
LikeTheSalad marked this conversation as resolved.
Show resolved Hide resolved
if (instrumentation::class.java in instrumentations) {
throw IllegalStateException("Instrumentation with type '${instrumentation::class.java}' already exists.")
}
instrumentations[instrumentation.javaClass] = instrumentation
}
}
Original file line number Diff line number Diff line change
@@ -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,
) {
}
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.opentelemetry.android.instrumentation.TestAndroidInstrumentation