diff --git a/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRum.java b/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRum.java index c43e53a70..e06546a41 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRum.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRum.java @@ -21,15 +21,32 @@ public interface OpenTelemetryRum { /** - * Returns a new {@link OpenTelemetryRumBuilder} for {@link OpenTelemetryRum}. Use this version - * if you would like to configure individual aspects of the OpenTelemetry SDK but would still - * prefer to allow OpenTelemetry RUM to create the SDK for you. If you would like to "bring your - * own" SDK, call the two-argument version. + * Returns a new {@link OpenTelemetryRumBuilder} for {@link OpenTelemetryRum} with a default + * configuration. Use this version if you would like to configure individual aspects of the + * OpenTelemetry SDK but would still prefer to allow OpenTelemetry RUM to create the SDK for + * you. For additional configuration, call the two-argument version of build and pass it your + * {@link OtelRumConfig} instance. If you would like to "bring your own" SDK, call the + * two-argument version that takes the SDK as a parameter. * * @param application The {@link Application} that is being instrumented. */ static OpenTelemetryRumBuilder builder(Application application) { - return new OpenTelemetryRumBuilder(application); + return builder(application, new OtelRumConfig()); + } + + /** + * Returns a new {@link OpenTelemetryRumBuilder} for {@link OpenTelemetryRum} with the given + * configuration. Use this version if you would like to configure individual aspects of the + * OpenTelemetry SDK but would still prefer to allow OpenTelemetry RUM to create the SDK for + * you. If you would like to "bring your own" SDK, call the two-argument version that takes the + * SDK as a parameter. + * + * @param application + * @param config + * @return + */ + static OpenTelemetryRumBuilder builder(Application application, OtelRumConfig config) { + return new OpenTelemetryRumBuilder(application, config); } /** diff --git a/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java b/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java index 359fbda0b..034760707 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java @@ -9,6 +9,11 @@ import android.app.Application; import io.opentelemetry.android.instrumentation.InstrumentedApplication; +import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker; +import io.opentelemetry.android.instrumentation.network.CurrentNetworkProvider; +import io.opentelemetry.android.instrumentation.network.NetworkAttributesSpanAppender; +import io.opentelemetry.android.instrumentation.startup.InitializationEvents; +import io.opentelemetry.android.instrumentation.startup.SdkInitializationEvents; import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; @@ -22,6 +27,7 @@ import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.util.ArrayList; @@ -47,6 +53,8 @@ public final class OpenTelemetryRumBuilder { meterProviderCustomizers = new ArrayList<>(); private final List> loggerProviderCustomizers = new ArrayList<>(); + private final OtelRumConfig config; + private final VisibleScreenTracker visibleScreenTracker = new VisibleScreenTracker(); private Function spanExporterCustomizer = a -> a; private final List> instrumentationInstallers = @@ -56,17 +64,19 @@ public final class OpenTelemetryRumBuilder { (a) -> a; private Resource resource; + private InitializationEvents initializationEvents = InitializationEvents.NO_OP; private static TextMapPropagator buildDefaultPropagator() { return TextMapPropagator.composite( W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()); } - OpenTelemetryRumBuilder(Application application) { + OpenTelemetryRumBuilder(Application application, OtelRumConfig config) { this.application = application; SessionIdTimeoutHandler timeoutHandler = new SessionIdTimeoutHandler(); this.sessionId = new SessionId(timeoutHandler); this.resource = AndroidResource.createDefault(application); + this.config = config; } /** @@ -216,6 +226,9 @@ public SessionId getSessionId() { * @return A new {@link OpenTelemetryRum} instance. */ public OpenTelemetryRum build() { + + applyConfiguration(); + OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() .setTracerProvider(buildTracerProvider(sessionId, application)) @@ -230,6 +243,50 @@ public OpenTelemetryRum build() { return delegate.build(); } + /** Leverage the configuration to wire up various instrumentation components. */ + private void applyConfiguration() { + if (config.shouldGenerateSdkInitializationEvents()) { + initializationEvents = new SdkInitializationEvents(); + initializationEvents.recordConfiguration(config); + } + initializationEvents.sdkInitializationStarted(); + + // Global attributes + if (config.hasGlobalAttributes()) { + // Add span processor that appends global attributes. + GlobalAttributesSpanAppender appender = + GlobalAttributesSpanAppender.create(config.getGlobalAttributes()); + addTracerProviderCustomizer( + (tracerProviderBuilder, app) -> + tracerProviderBuilder.addSpanProcessor(appender)); + } + + // Network specific attributes + if (config.shouldIncludeNetworkAttributes()) { + // Add span processor that appends network attributes. + CurrentNetworkProvider currentNetworkProvider = + CurrentNetworkProvider.createAndStart(application); + addTracerProviderCustomizer( + (tracerProviderBuilder, app) -> { + SpanProcessor networkAttributesSpanAppender = + NetworkAttributesSpanAppender.create(currentNetworkProvider); + return tracerProviderBuilder.addSpanProcessor( + networkAttributesSpanAppender); + }); + initializationEvents.currentNetworkProviderInitialized(); + } + + // Add span processor that appends screen attribute(s) + if (config.shouldIncludeScreenAttributes()) { + addTracerProviderCustomizer( + (tracerProviderBuilder, app) -> { + SpanProcessor screenAttributesAppender = + new ScreenAttributesSpanProcessor(visibleScreenTracker); + return tracerProviderBuilder.addSpanProcessor(screenAttributesAppender); + }); + } + } + private SdkTracerProvider buildTracerProvider(SessionId sessionId, Application application) { SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder() diff --git a/instrumentation/src/main/java/io/opentelemetry/android/OtelRumConfig.java b/instrumentation/src/main/java/io/opentelemetry/android/OtelRumConfig.java new file mode 100644 index 000000000..e8f7a1add --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/OtelRumConfig.java @@ -0,0 +1,88 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android; + +import io.opentelemetry.android.instrumentation.network.CurrentNetworkProvider; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; + +/** + * Configuration object for OpenTelemetry Android. The configuration items in this class will be + * used in the OpenTelemetryRumBuilder to wire up and enable/disable various mobile instrumentation + * components. + */ +public class OtelRumConfig { + + private AttributesBuilder globalAttributes = Attributes.builder(); + private boolean includeNetworkAttributes = true; + private boolean generateSdkInitializationEvents = true; + private boolean includeScreenAttributes = true; + + /** + * Configures the set of global attributes to emit with every span and event. Any existing + * configured attributes will be dropped. Default = none. + */ + public OtelRumConfig setGlobalAttributes(Attributes attributes) { + globalAttributes = attributes.toBuilder(); + return this; + } + + boolean hasGlobalAttributes() { + return !globalAttributes.build().isEmpty(); + } + + Attributes getGlobalAttributes() { + return globalAttributes.build(); + } + + /** + * Disables the collection of runtime network attributes. See {@link CurrentNetworkProvider} for + * more information. Default = true. + * + * @return this + */ + public OtelRumConfig disableNetworkAttributes() { + includeNetworkAttributes = false; + return this; + } + + /** Returns true if runtime network attributes are enabled, false otherwise. */ + public boolean shouldIncludeNetworkAttributes() { + return includeNetworkAttributes; + } + + /** + * Disables the collection of events related to the initialization of the OTel Android SDK + * itself. Default = true. + * + * @return this + */ + public OtelRumConfig disableSdkInitializationEvents() { + this.generateSdkInitializationEvents = false; + return this; + } + + /** Returns true if the SDK is configured to generate initialization events, false otherwise. */ + public boolean shouldGenerateSdkInitializationEvents() { + return generateSdkInitializationEvents; + } + + /** + * Call this to disable the collection of screen attributes. See {@link + * ScreenAttributesSpanProcessor} for more information. Default = true. + * + * @return this + */ + public OtelRumConfig disableScreenAttributes() { + this.includeScreenAttributes = false; + return this; + } + + /** Return true if the SDK should be configured to report screen attributes. */ + public boolean shouldIncludeScreenAttributes() { + return includeScreenAttributes; + } +} diff --git a/instrumentation/src/main/java/io/opentelemetry/android/ScreenAttributesSpanProcessor.java b/instrumentation/src/main/java/io/opentelemetry/android/ScreenAttributesSpanProcessor.java new file mode 100644 index 000000000..57c0d411c --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/ScreenAttributesSpanProcessor.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android; + +import static io.opentelemetry.android.RumConstants.SCREEN_NAME_KEY; + +import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +class ScreenAttributesSpanProcessor implements SpanProcessor { + + private final VisibleScreenTracker visibleScreenTracker; + + public ScreenAttributesSpanProcessor(VisibleScreenTracker visibleScreenTracker) { + this.visibleScreenTracker = visibleScreenTracker; + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + String currentScreen = visibleScreenTracker.getCurrentlyVisibleScreen(); + span.setAttribute(SCREEN_NAME_KEY, currentScreen); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) { + // nop + } + + @Override + public boolean isEndRequired() { + return false; + } +} diff --git a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java new file mode 100644 index 000000000..5fae104ca --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation.startup; + +import io.opentelemetry.android.OtelRumConfig; + +public interface InitializationEvents { + + void sdkInitializationStarted(); + + void recordConfiguration(OtelRumConfig config); + + void currentNetworkProviderInitialized(); + + InitializationEvents NO_OP = + new InitializationEvents() { + @Override + public void sdkInitializationStarted() {} + + @Override + public void recordConfiguration(OtelRumConfig config) {} + + @Override + public void currentNetworkProviderInitialized() {} + }; +} diff --git a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java new file mode 100644 index 000000000..ed20a7fc8 --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation.startup; + +import io.opentelemetry.android.OtelRumConfig; + +public class SdkInitializationEvents implements InitializationEvents { + + @Override + public void sdkInitializationStarted() { + // TODO: Build me + } + + @Override + public void recordConfiguration(OtelRumConfig config) { + // TODO: Build me (create event containing the config params for the sdk) + } + + @Override + public void currentNetworkProviderInitialized() { + // TODO: Build me + } +} diff --git a/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index 68c38ac6a..3e6aa556f 100644 --- a/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java +++ b/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java @@ -5,6 +5,7 @@ package io.opentelemetry.android; +import static io.opentelemetry.android.RumConstants.SCREEN_NAME_KEY; import static io.opentelemetry.android.RumConstants.SESSION_ID_KEY; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; @@ -17,6 +18,7 @@ import android.app.Activity; import android.app.Application; +import androidx.annotation.NonNull; import io.opentelemetry.android.instrumentation.ApplicationStateListener; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; @@ -53,7 +55,7 @@ class OpenTelemetryRumBuilderTest { @Test void shouldRegisterApplicationStateWatcher() { - OpenTelemetryRum.builder(application).build(); + makeBuilder().build(); verify(application).registerActivityLifecycleCallbacks(isA(ApplicationStateWatcher.class)); } @@ -61,7 +63,7 @@ void shouldRegisterApplicationStateWatcher() { @Test void shouldBuildTracerProvider() { OpenTelemetryRum openTelemetryRum = - OpenTelemetryRum.builder(application) + makeBuilder() .setResource(resource) .addTracerProviderCustomizer( (tracerProviderBuilder, app) -> @@ -82,12 +84,13 @@ void shouldBuildTracerProvider() { assertThat(spans.get(0)) .hasName("test span") .hasResource(resource) - .hasAttributesSatisfyingExactly(equalTo(SESSION_ID_KEY, sessionId)); + .hasAttributesSatisfyingExactly( + equalTo(SESSION_ID_KEY, sessionId), equalTo(SCREEN_NAME_KEY, "unknown")); } @Test void shouldInstallInstrumentation() { - OpenTelemetryRum.builder(application) + OpenTelemetryRum.builder(application, buildConfig()) .addInstrumentation( instrumentedApplication -> { assertThat(instrumentedApplication.getApplication()) @@ -105,6 +108,10 @@ void shouldInstallInstrumentation() { verify(listener).onApplicationBackgrounded(); } + private OtelRumConfig buildConfig() { + return new OtelRumConfig().disableNetworkAttributes(); + } + @Test void canAddPropagator() { Context context = Context.root(); @@ -116,10 +123,7 @@ void canAddPropagator() { when(customPropagator.extract(context, carrier, getter)).thenReturn(expected); - OpenTelemetryRum rum = - OpenTelemetryRum.builder(application) - .addPropagatorCustomizer(x -> customPropagator) - .build(); + OpenTelemetryRum rum = makeBuilder().addPropagatorCustomizer(x -> customPropagator).build(); Context result = rum.getOpenTelemetry() .getPropagators() @@ -132,10 +136,7 @@ void canAddPropagator() { void canSetPropagator() { TextMapPropagator customPropagator = mock(TextMapPropagator.class); - OpenTelemetryRum rum = - OpenTelemetryRum.builder(application) - .addPropagatorCustomizer(x -> customPropagator) - .build(); + OpenTelemetryRum rum = makeBuilder().addPropagatorCustomizer(x -> customPropagator).build(); TextMapPropagator result = rum.getOpenTelemetry().getPropagators().getTextMapPropagator(); assertThat(result).isSameAs(customPropagator); } @@ -144,8 +145,7 @@ void canSetPropagator() { void setSpanExporterCustomizer() { SpanExporter exporter = mock(SpanExporter.class); Function customizer = x -> exporter; - OpenTelemetryRum rum = - OpenTelemetryRum.builder(application).addSpanExporterCustomizer(customizer).build(); + OpenTelemetryRum rum = makeBuilder().addSpanExporterCustomizer(customizer).build(); Span span = rum.getOpenTelemetry().getTracer("test").spanBuilder("foo").startSpan(); try (Scope scope = span.makeCurrent()) { // no-op @@ -156,4 +156,9 @@ void setSpanExporterCustomizer() { await().atMost(Duration.ofSeconds(30)) .untilAsserted(() -> verify(exporter).export(anyCollection())); } + + @NonNull + private OpenTelemetryRumBuilder makeBuilder() { + return OpenTelemetryRum.builder(application, buildConfig()); + } } diff --git a/instrumentation/src/test/java/io/opentelemetry/android/ScreenAttributesSpanProcessorTest.java b/instrumentation/src/test/java/io/opentelemetry/android/ScreenAttributesSpanProcessorTest.java new file mode 100644 index 000000000..fe96da9e6 --- /dev/null +++ b/instrumentation/src/test/java/io/opentelemetry/android/ScreenAttributesSpanProcessorTest.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android; + +import static io.opentelemetry.android.RumConstants.SCREEN_NAME_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import org.junit.jupiter.api.Test; + +class ScreenAttributesSpanProcessorTest { + + @Test + void append() { + String screenName = "my cool screen"; + VisibleScreenTracker visibleScreenTracker = mock(VisibleScreenTracker.class); + Context contenxt = mock(Context.class); + ReadWriteSpan span = mock(ReadWriteSpan.class); + + when(visibleScreenTracker.getCurrentlyVisibleScreen()).thenReturn(screenName); + + ScreenAttributesSpanProcessor testClass = + new ScreenAttributesSpanProcessor(visibleScreenTracker); + assertThat(testClass.isStartRequired()).isTrue(); + assertThat(testClass.isEndRequired()).isFalse(); + assertThatCode(() -> testClass.onEnd(mock(ReadableSpan.class))).doesNotThrowAnyException(); + + testClass.onStart(contenxt, span); + verify(span).setAttribute(SCREEN_NAME_KEY, screenName); + } +}