diff --git a/android-agent/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java b/android-agent/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java index a59c74075..19eebad9a 100644 --- a/android-agent/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java +++ b/android-agent/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java @@ -83,10 +83,11 @@ public final class OpenTelemetryRumBuilder { private final OtelRumConfig config; private final VisibleScreenTracker visibleScreenTracker = new VisibleScreenTracker(); - private Function spanExporterCustomizer = a -> a; private final List> instrumentationInstallers = new ArrayList<>(); + private final List> otelSdkReadyListeners = new ArrayList<>(); + private Function spanExporterCustomizer = a -> a; private Function propagatorCustomizer = (a) -> a; @@ -325,6 +326,7 @@ OpenTelemetryRum build(ServiceManager serviceManager) { Log.e(RumConstants.OTEL_RUM_LOG_TAG, "Could not initialize disk exporters.", e); } } + initializationEvents.spanExporterInitialized(spanExporter); OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() @@ -377,14 +379,30 @@ private void scheduleDiskTelemetryReader( } } + /** + * Adds a callback to be invoked after the OpenTelemetry SDK has been initialized. This can be + * used to defer some early lifecycle functionality until the working SDK is ready. + * + * @param callback - A callback that receives the OpenTelemetry SDK instance. + * @return this + */ + public OpenTelemetryRumBuilder addOtelSdkReadyListener(Consumer callback) { + otelSdkReadyListeners.add(callback); + return this; + } + /** Leverage the configuration to wire up various instrumentation components. */ private void applyConfiguration() { if (config.shouldGenerateSdkInitializationEvents()) { if (initializationEvents == InitializationEvents.NO_OP) { - initializationEvents = new SdkInitializationEvents(); + SdkInitializationEvents sdkInitEvents = new SdkInitializationEvents(); + addOtelSdkReadyListener(sdkInitEvents::finish); + initializationEvents = sdkInitEvents; } Map configMap = new HashMap<>(); // TODO: Convert config to map + // breedx-splk: Left incomplete for now, because I think Cesar is making changes around + // this initializationEvents.recordConfiguration(configMap); } initializationEvents.sdkInitializationStarted(); @@ -491,7 +509,6 @@ private SdkTracerProvider buildTracerProvider( .setResource(resource) .addSpanProcessor(new SessionIdSpanAppender(sessionId)); - initializationEvents.spanExporterInitialized(spanExporter); BatchSpanProcessor batchSpanProcessor = BatchSpanProcessor.builder(spanExporter).build(); tracerProviderBuilder.addSpanProcessor(batchSpanProcessor); diff --git a/common/src/main/java/io/opentelemetry/android/common/RumConstants.java b/common/src/main/java/io/opentelemetry/android/common/RumConstants.java index cefaa8cff..70fa5c078 100644 --- a/common/src/main/java/io/opentelemetry/android/common/RumConstants.java +++ b/common/src/main/java/io/opentelemetry/android/common/RumConstants.java @@ -34,5 +34,18 @@ public class RumConstants { public static final String APP_START_SPAN_NAME = "AppStart"; + public static final class Events { + public static final String INIT_EVENT_STARTED = "rum.sdk.init.started"; + public static final String INIT_EVENT_CONFIG = "rum.sdk.init.config"; + public static final String INIT_EVENT_NET_PROVIDER = "rum.sdk.init.net.provider"; + public static final String INIT_EVENT_NET_MONITOR = "rum.sdk.init.net.monitor"; + public static final String INIT_EVENT_ANR_MONITOR = "rum.sdk.init.anr_monitor"; + public static final String INIT_EVENT_JANK_MONITOR = "rum.sdk.init.jank_monitor"; + public static final String INIT_EVENT_CRASH_REPORTER = "rum.sdk.init.crash.reporter"; + public static final String INIT_EVENT_SPAN_EXPORTER = "rum.sdk.init.span.exporter"; + + private Events() {} + } + private RumConstants() {} } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b4ec2f28..3895c5b2e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] opentelemetry = "1.38.0" +opentelemetry-alpha = "1.38.0-alpha" opentelemetry-instrumentation = "2.4.0" opentelemetry-instrumentation-alpha = "2.4.0-alpha" opentelemetry-semconv = "1.25.0-alpha" @@ -25,6 +26,8 @@ opentelemetry-instrumentation-okhttp = { module = "io.opentelemetry.instrumentat opentelemetry-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", version.ref = "opentelemetry-semconv" } opentelemetry-semconv-incubating = { module = "io.opentelemetry.semconv:opentelemetry-semconv-incubating", version.ref = "opentelemetry-semconv" } opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api" } +opentelemetry-api-incubator = { module = "io.opentelemetry:opentelemetry-api-incubator" } +opentelemetry-sdk-extension-incubator = { module = "io.opentelemetry:opentelemetry-sdk-extension-incubator", version.ref = "opentelemetry-alpha" } opentelemetry-sdk = { module = "io.opentelemetry:opentelemetry-sdk" } opentelemetry-exporter-logging = { module = "io.opentelemetry:opentelemetry-exporter-logging" } opentelemetry-diskBuffering = { module = "io.opentelemetry.contrib:opentelemetry-disk-buffering", version.ref = "opentelemetry-contrib" } diff --git a/instrumentation/startup/build.gradle.kts b/instrumentation/startup/build.gradle.kts index b5da36b79..06e98f8d6 100644 --- a/instrumentation/startup/build.gradle.kts +++ b/instrumentation/startup/build.gradle.kts @@ -21,5 +21,7 @@ dependencies { implementation(libs.androidx.core) implementation(libs.opentelemetry.semconv) implementation(libs.opentelemetry.sdk) + implementation(libs.opentelemetry.api.incubator) + implementation(libs.opentelemetry.sdk.extension.incubator) implementation(libs.opentelemetry.instrumentation.api) } diff --git a/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java b/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java deleted file mode 100644 index b12122608..000000000 --- a/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.android.instrumentation.startup; - -import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.util.Map; - -public interface InitializationEvents { - - void sdkInitializationStarted(); - - void recordConfiguration(Map config); - - void currentNetworkProviderInitialized(); - - void networkMonitorInitialized(); - - void anrMonitorInitialized(); - - void slowRenderingDetectorInitialized(); - - void crashReportingInitialized(); - - void spanExporterInitialized(SpanExporter spanExporter); - - InitializationEvents NO_OP = - new InitializationEvents() { - @Override - public void sdkInitializationStarted() {} - - @Override - public void recordConfiguration(Map config) {} - - @Override - public void currentNetworkProviderInitialized() {} - - @Override - public void networkMonitorInitialized() {} - - @Override - public void anrMonitorInitialized() {} - - @Override - public void slowRenderingDetectorInitialized() {} - - @Override - public void crashReportingInitialized() {} - - @Override - public void spanExporterInitialized(SpanExporter spanExporter) {} - }; -} diff --git a/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.kt b/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.kt new file mode 100644 index 000000000..c0e00104b --- /dev/null +++ b/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.kt @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation.startup + +import io.opentelemetry.sdk.trace.export.SpanExporter + +interface InitializationEvents { + fun sdkInitializationStarted() + + fun recordConfiguration(config: Map) + + fun currentNetworkProviderInitialized() + + fun networkMonitorInitialized() + + fun anrMonitorInitialized() + + fun slowRenderingDetectorInitialized() + + fun crashReportingInitialized() + + fun spanExporterInitialized(spanExporter: SpanExporter) + + companion object { + @JvmField + val NO_OP: InitializationEvents = + object : InitializationEvents { + override fun sdkInitializationStarted() {} + + override fun recordConfiguration(config: Map) {} + + override fun currentNetworkProviderInitialized() {} + + override fun networkMonitorInitialized() {} + + override fun anrMonitorInitialized() {} + + override fun slowRenderingDetectorInitialized() {} + + override fun crashReportingInitialized() {} + + override fun spanExporterInitialized(spanExporter: SpanExporter) {} + } + } +} diff --git a/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java b/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java deleted file mode 100644 index be82d0e31..000000000 --- a/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.android.instrumentation.startup; - -import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.util.Map; - -public class SdkInitializationEvents implements InitializationEvents { - - @Override - public void sdkInitializationStarted() { - // TODO: Build me - } - - @Override - public void recordConfiguration(Map config) { - // TODO: Build me (create event containing the config params for the sdk) - } - - @Override - public void currentNetworkProviderInitialized() { - // TODO: Build me - } - - @Override - public void networkMonitorInitialized() { - // TODO: Build me "networkMonitorInitialized" - } - - @Override - public void anrMonitorInitialized() { - // TODO: Build me "anrMonitorInitialized" - } - - @Override - public void slowRenderingDetectorInitialized() { - // TODO: Build me "slowRenderingDetectorInitialized" - } - - @Override - public void crashReportingInitialized() { - // TODO: Build me "crashReportingInitialized" - } - - @Override - public void spanExporterInitialized(SpanExporter spanExporter) { - // TODO: Build me "spanExporterInitialized" - } -} diff --git a/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.kt b/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.kt new file mode 100644 index 000000000..00de72f6a --- /dev/null +++ b/instrumentation/startup/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.kt @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation.startup + +import io.opentelemetry.android.common.RumConstants +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.incubator.logs.AnyValue +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider +import io.opentelemetry.sdk.trace.export.SpanExporter +import java.time.Instant +import java.util.function.Consumer +import java.util.function.Supplier + +class SdkInitializationEvents(private val clock: Supplier = Supplier { Instant.now() }) : InitializationEvents { + private val events = mutableListOf() + + override fun sdkInitializationStarted() { + addEvent(RumConstants.Events.INIT_EVENT_STARTED) + } + + override fun recordConfiguration(config: Map) { + val map = mutableMapOf>() + config.entries.forEach( + Consumer { e: Map.Entry -> + map[e.key] = AnyValue.of(e.value) + }, + ) + val body = AnyValue.of(map) + addEvent(RumConstants.Events.INIT_EVENT_CONFIG, body = body) + } + + override fun currentNetworkProviderInitialized() { + addEvent(RumConstants.Events.INIT_EVENT_NET_PROVIDER) + } + + override fun networkMonitorInitialized() { + addEvent(RumConstants.Events.INIT_EVENT_NET_MONITOR) + } + + override fun anrMonitorInitialized() { + addEvent(RumConstants.Events.INIT_EVENT_ANR_MONITOR) + } + + override fun slowRenderingDetectorInitialized() { + addEvent(RumConstants.Events.INIT_EVENT_JANK_MONITOR) + } + + override fun crashReportingInitialized() { + addEvent(RumConstants.Events.INIT_EVENT_CRASH_REPORTER) + } + + override fun spanExporterInitialized(spanExporter: SpanExporter) { + val attributes = + Attributes.of(AttributeKey.stringKey("span.exporter"), spanExporter.toString()) + addEvent(RumConstants.Events.INIT_EVENT_SPAN_EXPORTER, attr = attributes) + } + + fun finish(sdk: OpenTelemetrySdk) { + val loggerProvider = sdk.sdkLoggerProvider + val eventLogger = + SdkEventLoggerProvider.create(loggerProvider).get("otel.initialization.events") + events.forEach { event: Event -> + val eventBuilder = + eventLogger.builder(event.name) + .setTimestamp(event.timestamp) + .setAttributes(event.attributes) + if (event.body != null) { + // TODO: Config is technically correct because config is the only startup event + // with a body, but this is ultimately clunky/fragile. + eventBuilder.put("config", event.body) + } + eventBuilder.emit() + } + } + + private fun addEvent( + name: String, + attr: Attributes? = null, + body: AnyValue<*>? = null, + ) { + events.add(Event(clock.get(), name, attr, body)) + } + + private data class Event( + val timestamp: Instant, + val name: String, + val attributes: Attributes?, + val body: AnyValue<*>? = null, + ) +} diff --git a/instrumentation/startup/src/test/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEventsTest.kt b/instrumentation/startup/src/test/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEventsTest.kt new file mode 100644 index 000000000..68473f4d0 --- /dev/null +++ b/instrumentation/startup/src/test/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEventsTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation.startup + +import io.mockk.called +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import io.opentelemetry.android.common.RumConstants +import io.opentelemetry.api.common.AttributeKey.stringKey +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.incubator.logs.AnyValue +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.logs.LogRecordProcessor +import io.opentelemetry.sdk.logs.ReadWriteLogRecord +import io.opentelemetry.sdk.logs.SdkLoggerProvider +import io.opentelemetry.sdk.logs.data.Body +import io.opentelemetry.sdk.logs.internal.AnyValueBody +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat +import io.opentelemetry.sdk.trace.export.SpanExporter +import org.junit.jupiter.api.Test +import java.time.Instant +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong +import java.util.function.Consumer +import java.util.function.Supplier + +class SdkInitializationEventsTest { + @Test + fun `test all events`() { + val now = System.currentTimeMillis() + val fakeTime = AtomicLong(now) + val clock: Supplier = + Supplier { + Instant.ofEpochMilli(fakeTime.getAndAdd(50)) + } + val config = mapOf(Pair("foo", "bar"), Pair("bar", "baz")) + val seen: MutableList = ArrayList() + val expectedConfig = + AnyValueBody.create(AnyValue.of(config.mapValues { AnyValue.of(it.value) })) + + val exporter = mockk() + val processor = mockk() + val loggerProvider = + SdkLoggerProvider.builder() + .addLogRecordProcessor(processor) + .build() + val sdk = + OpenTelemetrySdk.builder() + .setLoggerProvider(loggerProvider) + .build() + every { processor.onEmit(any(), any()) }.answers { + seen.add(it.invocation.args[1] as ReadWriteLogRecord) + } + every { exporter.toString() }.returns("com.cool.Exporter") + + val events = SdkInitializationEvents(clock) + + events.sdkInitializationStarted() + events.anrMonitorInitialized() + events.crashReportingInitialized() + events.currentNetworkProviderInitialized() + events.networkMonitorInitialized() + events.recordConfiguration(config) + events.slowRenderingDetectorInitialized() + events.spanExporterInitialized(exporter) + + verify { listOf(processor) wasNot called } + verify(exactly = 0) { exporter.export(any()) } + + events.finish(sdk) + + assertThat(seen).satisfiesExactly( + time(now).named(RumConstants.Events.INIT_EVENT_STARTED), + time(now + 50).named(RumConstants.Events.INIT_EVENT_ANR_MONITOR), + time(now + 100).named(RumConstants.Events.INIT_EVENT_CRASH_REPORTER), + time(now + 150).named(RumConstants.Events.INIT_EVENT_NET_PROVIDER), + time(now + 200).named(RumConstants.Events.INIT_EVENT_NET_MONITOR), + time(now + 250).named(RumConstants.Events.INIT_EVENT_CONFIG).withBody(expectedConfig), + time(now + 300).named(RumConstants.Events.INIT_EVENT_JANK_MONITOR), + time(now + 350).named(RumConstants.Events.INIT_EVENT_SPAN_EXPORTER).withAttributes( + "span.exporter", + "com.cool.Exporter", + ), + ) + } + + fun time(timeMs: Long): EventAssert { + return EventAssert(TimeUnit.MILLISECONDS.toNanos(timeMs)) + } + + class EventAssert(val timeNs: Long) : Consumer { + lateinit var name: String + var body: Body? = null + var attrs: Attributes? = null + + override fun accept(log: ReadWriteLogRecord) { + val logData = log.toLogRecordData() + assertThat(logData.timestampEpochNanos).isEqualTo(timeNs) + assertThat(logData.attributes.get(stringKey("event.name"))).isEqualTo(name) + if (body == null) { + assertThat(logData.body.type).isEqualTo(Body.Type.EMPTY) + } else { + assertThat(logData.body.type).isNotEqualTo(Body.Type.EMPTY) + } + if (attrs != null) { + assertThat(logData.attributes).isEqualTo(attrs) + } + } + + fun named(name: String): EventAssert { + this.name = name + return this + } + + fun withBody(body: Body): EventAssert { + this.body = body + return this + } + + fun withAttributes( + key: String, + value: String, + ): EventAssert { + attrs = Attributes.of(stringKey("event.name"), name, stringKey(key), value) + return this + } + } +}