diff --git a/android-agent/api/android-agent.api b/android-agent/api/android-agent.api index 082e6f709..b58777706 100644 --- a/android-agent/api/android-agent.api +++ b/android-agent/api/android-agent.api @@ -46,11 +46,18 @@ public final class io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration public final fun globalAttributes (Lkotlin/jvm/functions/Function0;)V public final fun httpExport (Lkotlin/jvm/functions/Function1;)V public final fun instrumentations (Lkotlin/jvm/functions/Function1;)V + public final fun otelSdkCustomizations (Lkotlin/jvm/functions/Function1;)V public final fun resource (Lkotlin/jvm/functions/Function1;)V public final fun session (Lkotlin/jvm/functions/Function1;)V public final fun setClock (Lio/opentelemetry/sdk/common/Clock;)V } +public final class io/opentelemetry/android/agent/dsl/OtelSdkCustomizationsSpec { + public final fun customizeLoggerProvider (Lkotlin/jvm/functions/Function1;)V + public final fun customizeMeterProvider (Lkotlin/jvm/functions/Function1;)V + public final fun customizeTracerProvider (Lkotlin/jvm/functions/Function1;)V +} + public final class io/opentelemetry/android/agent/dsl/SessionConfiguration { public final fun getBackgroundInactivityTimeout-UwyO8pc ()J public final fun getMaxLifetime-UwyO8pc ()J diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt index e6d7ac1ab..80a2705ef 100644 --- a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt @@ -59,33 +59,44 @@ object OpenTelemetryRumInitializer { cfg.resourceAction(resourceBuilder) val resource = resourceBuilder.build() - return RumBuilder - .builder(ctx, cfg.rumConfig) - .setSessionProvider(createSessionProvider(ctx, cfg)) - .setResource(resource) - .setClock(cfg.clock) - .addSpanExporterCustomizer { - OtlpHttpSpanExporter - .builder() - .setEndpoint(spansEndpoint.getUrl()) - .setHeaders(spansEndpoint::getHeaders) - .setCompression(spansEndpoint.getCompression().getUpstreamName()) - .build() - }.addLogRecordExporterCustomizer { - OtlpHttpLogRecordExporter - .builder() - .setEndpoint(logsEndpoints.getUrl()) - .setHeaders(logsEndpoints::getHeaders) - .setCompression(logsEndpoints.getCompression().getUpstreamName()) - .build() - }.addMetricExporterCustomizer { - OtlpHttpMetricExporter - .builder() - .setEndpoint(metricsEndpoint.getUrl()) - .setHeaders(metricsEndpoint::getHeaders) - .setCompression(metricsEndpoint.getCompression().getUpstreamName()) - .build() - }.build() + val builder = + RumBuilder + .builder(ctx, cfg.rumConfig) + .setSessionProvider(createSessionProvider(ctx, cfg)) + .setResource(resource) + .setClock(cfg.clock) + .addSpanExporterCustomizer { + OtlpHttpSpanExporter + .builder() + .setEndpoint(spansEndpoint.getUrl()) + .setHeaders(spansEndpoint::getHeaders) + .setCompression(spansEndpoint.getCompression().getUpstreamName()) + .build() + }.addLogRecordExporterCustomizer { + OtlpHttpLogRecordExporter + .builder() + .setEndpoint(logsEndpoints.getUrl()) + .setHeaders(logsEndpoints::getHeaders) + .setCompression(logsEndpoints.getCompression().getUpstreamName()) + .build() + }.addMetricExporterCustomizer { + OtlpHttpMetricExporter + .builder() + .setEndpoint(metricsEndpoint.getUrl()) + .setHeaders(metricsEndpoint::getHeaders) + .setCompression(metricsEndpoint.getCompression().getUpstreamName()) + .build() + } + cfg.sdkCustomizations.tracerProviderCustomizers.forEach { + builder.addTracerProviderCustomizer { builder, _ -> it.invoke(builder) } + } + cfg.sdkCustomizations.loggerProviderCustomizers.forEach { + builder.addLoggerProviderCustomizer { builder, _ -> it.invoke(builder) } + } + cfg.sdkCustomizations.meterProviderCustomizers.forEach { + builder.addMeterProviderCustomizer { builder, _ -> it.invoke(builder) } + } + return builder.build() } private fun Compression.getUpstreamName(): String = diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration.kt index 1c71402c7..d8f8db1ce 100644 --- a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration.kt +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration.kt @@ -25,6 +25,7 @@ class OpenTelemetryConfiguration internal constructor( * Configures the [Clock] used for capturing telemetry. */ var clock: Clock = OtelAndroidClock(), + internal val sdkCustomizations: OtelSdkCustomizationsSpec = OtelSdkCustomizationsSpec(), ) { internal val exportConfig = HttpExportConfiguration() internal val sessionConfig = SessionConfiguration() @@ -59,6 +60,14 @@ class OpenTelemetryConfiguration internal constructor( rumConfig.setGlobalAttributes(action()) } + /** + * You can use this to customize additional nonstandard aspects of the OpenTelemetry SDK. + * Most users should not need to provide any customizations here. + */ + fun otelSdkCustomizations(action: OtelSdkCustomizationsSpec.() -> Unit) { + sdkCustomizations.action() + } + /** * Configures disk buffering behavior of exported telemetry. */ diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OtelSdkCustomizationsSpec.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OtelSdkCustomizationsSpec.kt new file mode 100644 index 000000000..3c8afffc9 --- /dev/null +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OtelSdkCustomizationsSpec.kt @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.dsl + +import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder + +typealias TracerProviderCustomizer = Function1 +typealias LoggerProviderCustomizer = Function1 +typealias MeterProviderCustomizer = Function1 + +@OpenTelemetryDslMarker +class OtelSdkCustomizationsSpec internal constructor() { + internal val tracerProviderCustomizers: MutableList = mutableListOf() + internal val loggerProviderCustomizers: MutableList = mutableListOf() + internal val meterProviderCustomizers: MutableList = mutableListOf() + + /** + * Modify the creation of the OpenTelemetry TracerProvider by providing + * your own customizer here. The customizer is a function + * that receives an instance of [SdkTracerProviderBuilder] and returns an + * instance of [SdkTracerProviderBuilder], which should almost always be + * the same instance. If a new instance is returned, the operation can be + * destructive. + */ + fun customizeTracerProvider(customizer: TracerProviderCustomizer) { + tracerProviderCustomizers.add(customizer) + } + + /** + * Modify the creation of the OpenTelemetry LoggerProvider by providing + * your own customizer here. The customizer is a function + * that receives an instance of [SdkLoggerProviderBuilder] and returns an + * instance of [SdkLoggerProviderBuilder], which should almost always be + * the same instance. If a new instance is returned, the operation can be + * destructive. + */ + fun customizeLoggerProvider(customizer: LoggerProviderCustomizer) { + loggerProviderCustomizers.add(customizer) + } + + /** + * Modify the creation of the OpenTelemetry MeterProvider by providing + * your own customizer here. The customizer is a function + * that receives an instance of [SdkMeterProviderBuilder] and returns an + * instance of [SdkMeterProviderBuilder], which should almost always be + * the same instance. If a new instance is returned, the operation can be + * destructive. + */ + fun customizeMeterProvider(customizer: MeterProviderCustomizer) { + meterProviderCustomizers.add(customizer) + } +} diff --git a/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/OtelSdkCustomizationsSpecTest.kt b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/OtelSdkCustomizationsSpecTest.kt new file mode 100644 index 000000000..4e6790fd0 --- /dev/null +++ b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/OtelSdkCustomizationsSpecTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.dsl + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.opentelemetry.android.agent.OpenTelemetryRumInitializer +import io.opentelemetry.sdk.trace.IdGenerator +import io.opentelemetry.sdk.trace.SdkTracerProvider +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RuntimeEnvironment +import java.util.concurrent.atomic.AtomicBoolean + +@RunWith(AndroidJUnit4::class) +class OtelSdkCustomizationsSpecTest { + @Test + fun `can configure tracer provider`() { + val traceId = "b9a654847cb3514b6e5bd80cb168ed1c" + val spanId = "0666666666666666" + val idGen = + object : IdGenerator { + override fun generateSpanId(): String? = spanId + + override fun generateTraceId(): String? = traceId + } + val agent = + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + configuration = { + otelSdkCustomizations { + customizeTracerProvider { _ -> + SdkTracerProvider.builder().setIdGenerator(idGen) + } + } + }, + ) + val tracer = + agent.openTelemetry.tracerProvider + .tracerBuilder("test") + .build() + val span = tracer.spanBuilder("test").startSpan() + assertThat(span.spanContext.spanId).isEqualTo(spanId) + } + + @Test + fun `can configure logger provider`() { + val seen = AtomicBoolean(false) + val agent = + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + configuration = { + otelSdkCustomizations { + customizeLoggerProvider { builder -> + builder.addLogRecordProcessor { _, _ -> + run { + seen.set(true) + } + } + } + } + }, + ) + val logger = + agent.openTelemetry.logsBridge + .loggerBuilder("test") + .build() + logger.logRecordBuilder().setBody("howdy").emit() + assertThat(seen.get()).isTrue + } + + @Test + fun `can configure meter provider`() { + val seen = AtomicBoolean(false) + val agent = + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + configuration = { + otelSdkCustomizations { + customizeMeterProvider { builder -> + run { + seen.set(true) + builder + } + } + } + }, + ) + val counter = + agent.openTelemetry.meterProvider + .get("test") + .counterBuilder("test") + .build() + counter.add(1) + assertThat(seen.get()).isTrue + } +} diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt index 3d22f98dd..dc8306b6c 100644 --- a/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt @@ -17,6 +17,11 @@ import io.opentelemetry.api.logs.LogRecordBuilder import io.opentelemetry.api.logs.LoggerProvider import io.opentelemetry.api.metrics.LongCounter import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.context.Context +import io.opentelemetry.sdk.trace.ReadWriteSpan +import io.opentelemetry.sdk.trace.ReadableSpan +import io.opentelemetry.sdk.trace.SpanProcessor +import kotlin.random.Random const val TAG = "otel.demo" @@ -40,6 +45,18 @@ class OtelDemoApplication : Application() { globalAttributes { Attributes.of(stringKey("toolkit"), "jetpack compose") } + otelSdkCustomizations { + customizeTracerProvider { builder -> + builder.addSpanProcessor(OtelDemoCustomSpanProcessor()) + } + customizeLoggerProvider { builder -> + builder.addLogRecordProcessor { _, logRecord -> + // onEmit() + logRecord.setAttribute(stringKey("awesome"), "sauce-" + Random.nextInt()) + } + } + customizeMeterProvider { builder -> builder /* customize as you like! */ } + } } ) Log.d(TAG, "RUM session started: " + rum?.getRumSessionId()) @@ -69,3 +86,20 @@ class OtelDemoApplication : Application() { } } } + +class OtelDemoCustomSpanProcessor: SpanProcessor { + override fun onStart( + parentContext: Context, + span: ReadWriteSpan + ) { + span.setAttribute("my.custom.attribute", Random.nextLong()) + } + + override fun isStartRequired(): Boolean = true + + override fun isEndRequired(): Boolean = false + + override fun onEnd(span: ReadableSpan) { + //no-op + } +}