From 3d91ee19546544c23f9bc27909e9fbdd30c04e2f Mon Sep 17 00:00:00 2001 From: Mateusz Rzeszutek Date: Sun, 30 Oct 2022 08:52:12 +0100 Subject: [PATCH] Initial implementation of OpenTelemetryRum (#378) --- .../java/com/splunk/rum/NetworkMonitor.java | 7 +- .../java/com/splunk/rum/NoOpSplunkRum.java | 3 +- .../com/splunk/rum/RumAttributeAppender.java | 9 +- .../java/com/splunk/rum/RumInitializer.java | 277 +++++++++--------- .../rum/SessionIdRatioBasedSampler.java | 16 +- .../main/java/com/splunk/rum/SplunkRum.java | 31 +- .../internal/ApplicationStateWatcher.java} | 25 +- .../internal/InstrumentedApplicationImpl.java | 53 ++++ .../rum/internal/NoopOpenTelemetryRum.java} | 20 +- .../rum/internal/OpenTelemetryRum.java | 51 ++++ .../rum/internal/OpenTelemetryRumBuilder.java | 209 +++++++++++++ .../rum/internal/OpenTelemetryRumImpl.java | 41 +++ .../rum/internal}/SessionId.java | 2 +- .../internal}/SessionIdChangeListener.java | 2 +- .../rum/internal}/SessionIdChangeTracer.java | 2 +- .../rum/internal/SessionIdSpanAppender.java | 55 ++++ .../internal}/SessionIdTimeoutHandler.java | 9 +- .../ApplicationStateListener.java | 35 +++ .../InstrumentedApplication.java | 49 ++++ .../splunk/rum/RumAttributeAppenderTest.java | 14 +- .../com/splunk/rum/RumInitializerTest.java | 2 - .../rum/SessionIdRatioBasedSamplerTest.java | 21 +- .../java/com/splunk/rum/SplunkRumTest.java | 38 +-- .../ApplicationStateWatcherTest.java} | 46 +-- .../GlobalAttributesSpanAppenderTest.java | 6 +- .../internal/OpenTelemetryRumBuilderTest.java | 106 +++++++ .../internal}/SessionIdChangeTracerTest.java | 2 +- .../internal/SessionIdSpanAppenderTest.java | 50 ++++ .../rum/internal}/SessionIdTest.java | 2 +- .../SessionIdTimeoutHandlerTest.java | 8 +- 30 files changed, 926 insertions(+), 265 deletions(-) rename splunk-otel-android/src/main/java/{com/splunk/rum/AppStateWatcher.java => io/opentelemetry/rum/internal/ApplicationStateWatcher.java} (69%) create mode 100644 splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/InstrumentedApplicationImpl.java rename splunk-otel-android/src/main/java/{com/splunk/rum/AppStateListener.java => io/opentelemetry/rum/internal/NoopOpenTelemetryRum.java} (56%) create mode 100644 splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRum.java create mode 100644 splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRumBuilder.java create mode 100644 splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRumImpl.java rename splunk-otel-android/src/main/java/{com/splunk/rum => io/opentelemetry/rum/internal}/SessionId.java (98%) rename splunk-otel-android/src/main/java/{com/splunk/rum => io/opentelemetry/rum/internal}/SessionIdChangeListener.java (95%) rename splunk-otel-android/src/main/java/{com/splunk/rum => io/opentelemetry/rum/internal}/SessionIdChangeTracer.java (97%) create mode 100644 splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdSpanAppender.java rename splunk-otel-android/src/main/java/{com/splunk/rum => io/opentelemetry/rum/internal}/SessionIdTimeoutHandler.java (90%) create mode 100644 splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/ApplicationStateListener.java create mode 100644 splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/InstrumentedApplication.java rename splunk-otel-android/src/test/java/{com/splunk/rum/AppStateWatcherTest.java => io/opentelemetry/rum/internal/ApplicationStateWatcherTest.java} (56%) create mode 100644 splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/OpenTelemetryRumBuilderTest.java rename splunk-otel-android/src/test/java/{com/splunk/rum => io/opentelemetry/rum/internal}/SessionIdChangeTracerTest.java (98%) create mode 100644 splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdSpanAppenderTest.java rename splunk-otel-android/src/test/java/{com/splunk/rum => io/opentelemetry/rum/internal}/SessionIdTest.java (99%) rename splunk-otel-android/src/test/java/{com/splunk/rum => io/opentelemetry/rum/internal}/SessionIdTimeoutHandlerTest.java (93%) diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/NetworkMonitor.java b/splunk-otel-android/src/main/java/com/splunk/rum/NetworkMonitor.java index 04fb526f5..5a3ecfe0e 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/NetworkMonitor.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/NetworkMonitor.java @@ -22,9 +22,10 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener; import java.util.concurrent.atomic.AtomicBoolean; -class NetworkMonitor implements AppStateListener { +class NetworkMonitor implements ApplicationStateListener { static final AttributeKey NETWORK_STATUS_KEY = stringKey("network.status"); private final ConnectionUtil connectionUtil; @@ -40,12 +41,12 @@ void addConnectivityListener(Tracer tracer) { } @Override - public void appForegrounded() { + public void onApplicationForegrounded() { shouldEmitChangeEvents.set(true); } @Override - public void appBackgrounded() { + public void onApplicationBackgrounded() { shouldEmitChangeEvents.set(false); } diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/NoOpSplunkRum.java b/splunk-otel-android/src/main/java/com/splunk/rum/NoOpSplunkRum.java index 346daabf2..74a971304 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/NoOpSplunkRum.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/NoOpSplunkRum.java @@ -21,6 +21,7 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.rum.internal.OpenTelemetryRum; import java.util.function.Consumer; import okhttp3.Call; import okhttp3.OkHttpClient; @@ -31,7 +32,7 @@ class NoOpSplunkRum extends SplunkRum { // passing null values here is fine, they'll never get used anyway @SuppressWarnings("NullAway") private NoOpSplunkRum() { - super(null, null, null); + super(OpenTelemetryRum.noop(), null); } @Override diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/RumAttributeAppender.java b/splunk-otel-android/src/main/java/com/splunk/rum/RumAttributeAppender.java index fff4c5323..542dba450 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/RumAttributeAppender.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/RumAttributeAppender.java @@ -36,23 +36,16 @@ class RumAttributeAppender implements SpanProcessor { static final AttributeKey SESSION_ID_KEY = stringKey("splunk.rumSessionId"); - private final SessionId sessionId; private final VisibleScreenTracker visibleScreenTracker; private final ConnectionUtil connectionUtil; - RumAttributeAppender( - SessionId sessionId, - VisibleScreenTracker visibleScreenTracker, - ConnectionUtil connectionUtil) { - this.sessionId = sessionId; + RumAttributeAppender(VisibleScreenTracker visibleScreenTracker, ConnectionUtil connectionUtil) { this.visibleScreenTracker = visibleScreenTracker; this.connectionUtil = connectionUtil; } @Override public void onStart(Context parentContext, ReadWriteSpan span) { - span.setAttribute(SESSION_ID_KEY, sessionId.getSessionId()); - String currentScreen = visibleScreenTracker.getCurrentlyVisibleScreen(); span.setAttribute(SplunkRum.SCREEN_NAME_KEY, currentScreen); diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java index ddfdc1849..491bf64be 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/RumInitializer.java @@ -26,7 +26,6 @@ import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.OS_TYPE; import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.OS_VERSION; import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; -import static java.util.Objects.requireNonNull; import android.app.Application; import android.os.Build; @@ -42,15 +41,14 @@ import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; import io.opentelemetry.rum.internal.GlobalAttributesSpanAppender; -import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.rum.internal.OpenTelemetryRum; +import io.opentelemetry.rum.internal.OpenTelemetryRumBuilder; +import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.ResourceBuilder; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; import io.opentelemetry.sdk.trace.SpanLimits; -import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; @@ -90,98 +88,161 @@ class RumInitializer { } SplunkRum initialize(ConnectionUtil.Factory connectionUtilFactory, Looper mainLooper) { - String rumVersion = detectRumVersion(); VisibleScreenTracker visibleScreenTracker = new VisibleScreenTracker(); long startTimeNanos = timingClock.now(); - List appStateListeners = new ArrayList<>(); + OpenTelemetryRumBuilder otelRumBuilder = OpenTelemetryRum.builder(); - ConnectionUtil connectionUtil = connectionUtilFactory.createAndStart(application); - initializationEvents.add( - new InitializationEvent("connectionUtilInitialized", timingClock.now())); - - SpanExporter zipkinExporter = buildFilteringExporter(connectionUtil); + otelRumBuilder.setResource(buildResource(builder.applicationName, detectRumVersion())); initializationEvents.add( - new RumInitializer.InitializationEvent("exporterInitialized", timingClock.now())); + new RumInitializer.InitializationEvent("resourceInitialized", timingClock.now())); - SessionIdTimeoutHandler timeoutHandler = new SessionIdTimeoutHandler(); - SessionId sessionId = new SessionId(timeoutHandler); - appStateListeners.add(timeoutHandler); + ConnectionUtil connectionUtil = connectionUtilFactory.createAndStart(application); initializationEvents.add( - new RumInitializer.InitializationEvent("sessionIdInitialized", timingClock.now())); + new InitializationEvent("connectionUtilInitialized", timingClock.now())); GlobalAttributesSpanAppender globalAttributesSpanAppender = GlobalAttributesSpanAppender.create(builder.globalAttributes); - SdkTracerProvider sdkTracerProvider = - buildTracerProvider( - Clock.getDefault(), - zipkinExporter, - sessionId, - rumVersion, - visibleScreenTracker, - connectionUtil, - globalAttributesSpanAppender); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "tracerProviderInitialized", timingClock.now())); + otelRumBuilder.addTracerProviderCustomizer( + (tracerProviderBuilder, app) -> { + SpanExporter zipkinExporter = buildFilteringExporter(connectionUtil); + initializationEvents.add( + new RumInitializer.InitializationEvent( + "exporterInitialized", timingClock.now())); + + BatchSpanProcessor batchSpanProcessor = + BatchSpanProcessor.builder(zipkinExporter).build(); + initializationEvents.add( + new RumInitializer.InitializationEvent( + "batchSpanProcessorInitialized", timingClock.now())); + + RumAttributeAppender attributeAppender = + new RumAttributeAppender(visibleScreenTracker, connectionUtil); + initializationEvents.add( + new RumInitializer.InitializationEvent( + "attributeAppenderInitialized", timingClock.now())); + + tracerProviderBuilder + .addSpanProcessor(attributeAppender) + .addSpanProcessor(globalAttributesSpanAppender) + .addSpanProcessor(batchSpanProcessor) + .setSpanLimits( + SpanLimits.builder() + .setMaxAttributeValueLength(MAX_ATTRIBUTE_LENGTH) + .build()); + + if (builder.sessionBasedSamplerEnabled) { + // TODO: this is hacky behavior that utilizes a mutable variable, fix this! + tracerProviderBuilder.setSampler( + new SessionIdRatioBasedSampler( + builder.sessionBasedSamplerRatio, SplunkRum::getInstance)); + } - OpenTelemetrySdk openTelemetrySdk = - OpenTelemetrySdk.builder().setTracerProvider(sdkTracerProvider).build(); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "openTelemetrySdkInitialized", timingClock.now())); + if (builder.debugEnabled) { + tracerProviderBuilder.addSpanProcessor( + SimpleSpanProcessor.create( + builder.decorateWithSpanFilter( + LoggingSpanExporter.create()))); + initializationEvents.add( + new RumInitializer.InitializationEvent( + "debugSpanExporterInitialized", timingClock.now())); + } + + initializationEvents.add( + new RumInitializer.InitializationEvent( + "tracerProviderInitialized", timingClock.now())); + return tracerProviderBuilder; + }); if (builder.anrDetectionEnabled) { - appStateListeners.add(initializeAnrReporting(mainLooper)); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "anrMonitorInitialized", timingClock.now())); + otelRumBuilder.addInstrumentation( + instrumentedApplication -> { + instrumentedApplication.registerApplicationStateListener( + initializeAnrReporting(mainLooper)); + initializationEvents.add( + new RumInitializer.InitializationEvent( + "anrMonitorInitialized", timingClock.now())); + }); } - Tracer tracer = openTelemetrySdk.getTracer(SplunkRum.RUM_TRACER_NAME); - sessionId.setSessionIdChangeListener(new SessionIdChangeTracer(tracer)); - if (builder.networkMonitorEnabled) { - NetworkMonitor networkMonitor = new NetworkMonitor(connectionUtil); - networkMonitor.addConnectivityListener(tracer); - appStateListeners.add(networkMonitor); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "networkMonitorInitialized", timingClock.now())); + otelRumBuilder.addInstrumentation( + instrumentedApplication -> { + NetworkMonitor networkMonitor = new NetworkMonitor(connectionUtil); + networkMonitor.addConnectivityListener( + instrumentedApplication + .getOpenTelemetrySdk() + .getTracer(SplunkRum.RUM_TRACER_NAME)); + instrumentedApplication.registerApplicationStateListener(networkMonitor); + initializationEvents.add( + new RumInitializer.InitializationEvent( + "networkMonitorInitialized", timingClock.now())); + }); } - // the app state listeners need to be run in the first ActivityLifecycleCallbacks since they - // might turn off/on additional telemetry depending on whether the app is active or not - application.registerActivityLifecycleCallbacks(new AppStateWatcher(appStateListeners)); - - SlowRenderingDetector slowRenderingDetector = buildSlowRenderingDetector(tracer); - slowRenderingDetector.start(application); - - Application.ActivityLifecycleCallbacks activityCallbacks; - if (Build.VERSION.SDK_INT < 29) { - activityCallbacks = - new Pre29ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); - } else { - activityCallbacks = new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); - } - application.registerActivityLifecycleCallbacks(activityCallbacks); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "activityLifecycleCallbacksInitialized", timingClock.now())); + otelRumBuilder.addInstrumentation( + instrumentedApplication -> { + SlowRenderingDetector slowRenderingDetector = + buildSlowRenderingDetector( + instrumentedApplication + .getOpenTelemetrySdk() + .getTracer(SplunkRum.RUM_TRACER_NAME)); + slowRenderingDetector.start(instrumentedApplication.getApplication()); + }); + + otelRumBuilder.addInstrumentation( + instrumentedApplication -> { + Tracer tracer = + instrumentedApplication + .getOpenTelemetrySdk() + .getTracer(SplunkRum.RUM_TRACER_NAME); + Application.ActivityLifecycleCallbacks activityCallbacks; + if (Build.VERSION.SDK_INT < 29) { + activityCallbacks = + new Pre29ActivityCallbacks( + tracer, visibleScreenTracker, startupTimer); + } else { + activityCallbacks = + new ActivityCallbacks(tracer, visibleScreenTracker, startupTimer); + } + instrumentedApplication + .getApplication() + .registerActivityLifecycleCallbacks(activityCallbacks); + initializationEvents.add( + new RumInitializer.InitializationEvent( + "activityLifecycleCallbacksInitialized", timingClock.now())); + }); if (builder.crashReportingEnabled) { - RuntimeDetails runtimeDetails = - RuntimeDetails.create(application.getApplicationContext()); - CrashReporter.initializeCrashReporting( - tracer, openTelemetrySdk.getSdkTracerProvider(), runtimeDetails); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "crashReportingInitialized", timingClock.now())); + otelRumBuilder.addInstrumentation( + instrumentedApplication -> { + RuntimeDetails runtimeDetails = + RuntimeDetails.create( + instrumentedApplication + .getApplication() + .getApplicationContext()); + CrashReporter.initializeCrashReporting( + instrumentedApplication + .getOpenTelemetrySdk() + .getTracer(SplunkRum.RUM_TRACER_NAME), + instrumentedApplication + .getOpenTelemetrySdk() + .getSdkTracerProvider(), + runtimeDetails); + initializationEvents.add( + new RumInitializer.InitializationEvent( + "crashReportingInitialized", timingClock.now())); + }); } - recordInitializationSpans(startTimeNanos, initializationEvents, tracer); + OpenTelemetryRum openTelemetryRum = otelRumBuilder.build(application); - return new SplunkRum(openTelemetrySdk, sessionId, globalAttributesSpanAppender); + recordInitializationSpans( + startTimeNanos, + initializationEvents, + openTelemetryRum.getOpenTelemetry().getTracer(SplunkRum.RUM_TRACER_NAME)); + + return new SplunkRum(openTelemetryRum, globalAttributesSpanAppender); } private SlowRenderingDetector buildSlowRenderingDetector(Tracer tracer) { @@ -201,25 +262,27 @@ private SlowRenderingDetector buildSlowRenderingDetector(Tracer tracer) { return new SlowRenderingDetectorImpl(tracer, builder.slowRenderingDetectionPollInterval); } - private AppStateListener initializeAnrReporting(Looper mainLooper) { + private ApplicationStateListener initializeAnrReporting(Looper mainLooper) { Thread mainThread = mainLooper.getThread(); Handler uiHandler = new Handler(mainLooper); + // TODO: this is hacky behavior that utilizes a mutable variable, fix this! AnrWatcher anrWatcher = new AnrWatcher(uiHandler, mainThread, SplunkRum::getInstance); ScheduledExecutorService anrScheduler = Executors.newScheduledThreadPool(1); final ScheduledFuture scheduledFuture = anrScheduler.scheduleAtFixedRate(anrWatcher, 1, 1, TimeUnit.SECONDS); - return new AppStateListener() { + return new ApplicationStateListener() { + @Nullable private ScheduledFuture future = scheduledFuture; @Override - public void appForegrounded() { + public void onApplicationForegrounded() { if (future == null) { future = anrScheduler.scheduleAtFixedRate(anrWatcher, 1, 1, TimeUnit.SECONDS); } } @Override - public void appBackgrounded() { + public void onApplicationBackgrounded() { if (future != null) { future.cancel(true); future = null; @@ -279,64 +342,6 @@ private void recordInitializationSpans( startupTimer.setCompletionCallback(() -> span.end(spanEndTime, TimeUnit.NANOSECONDS)); } - private SdkTracerProvider buildTracerProvider( - Clock clock, - SpanExporter zipkinExporter, - SessionId sessionId, - String rumVersion, - VisibleScreenTracker visibleScreenTracker, - ConnectionUtil connectionUtil, - SpanProcessor... additionalProcessors) { - BatchSpanProcessor batchSpanProcessor = BatchSpanProcessor.builder(zipkinExporter).build(); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "batchSpanProcessorInitialized", timingClock.now())); - - String applicationName = requireNonNull(builder.applicationName); - RumAttributeAppender attributeAppender = - new RumAttributeAppender(sessionId, visibleScreenTracker, connectionUtil); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "attributeAppenderInitialized", timingClock.now())); - - Resource resource = buildResource(applicationName, rumVersion); - initializationEvents.add( - new RumInitializer.InitializationEvent("resourceInitialized", timingClock.now())); - - SdkTracerProviderBuilder tracerProviderBuilder = - SdkTracerProvider.builder() - .setClock(clock) - .addSpanProcessor(batchSpanProcessor) - .addSpanProcessor(attributeAppender) - .setSpanLimits( - SpanLimits.builder() - .setMaxAttributeValueLength(MAX_ATTRIBUTE_LENGTH) - .build()) - .setResource(resource); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "tracerProviderBuilderInitialized", timingClock.now())); - - if (builder.sessionBasedSamplerEnabled) { - tracerProviderBuilder.setSampler( - new SessionIdRatioBasedSampler(builder.sessionBasedSamplerRatio, sessionId)); - } - - for (SpanProcessor spanProcessor : additionalProcessors) { - tracerProviderBuilder.addSpanProcessor(spanProcessor); - } - - if (builder.debugEnabled) { - tracerProviderBuilder.addSpanProcessor( - SimpleSpanProcessor.create( - builder.decorateWithSpanFilter(LoggingSpanExporter.create()))); - initializationEvents.add( - new RumInitializer.InitializationEvent( - "debugSpanExporterInitialized", timingClock.now())); - } - return tracerProviderBuilder.build(); - } - private Resource buildResource(String applicationName, String rumVersion) { ResourceBuilder resourceBuilder = Resource.getDefault().toBuilder() diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdRatioBasedSampler.java b/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdRatioBasedSampler.java index b2b942a71..5cb8baf44 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdRatioBasedSampler.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdRatioBasedSampler.java @@ -23,20 +23,21 @@ import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.sdk.trace.samplers.SamplingResult; import java.util.List; +import java.util.function.Supplier; /** * Session ID ratio based sampler. Uses {@link Sampler#traceIdRatioBased(double)} sampler * internally, but passes sessionId instead of traceId to the underlying sampler in order to use the - * same ratio logic but on sessionId instead. This is valid as {@link SessionId} uses {@link + * same ratio logic but on sessionId instead. This is valid as sessionId uses {@link * io.opentelemetry.api.trace.TraceId#fromLongs(long, long)} internally to generate random session * IDs. */ class SessionIdRatioBasedSampler implements Sampler { - private final SessionId sessionId; private final Sampler ratioBasedSampler; + private final Supplier splunkRumSupplier; - SessionIdRatioBasedSampler(double ratio, SessionId sessionId) { - this.sessionId = sessionId; + SessionIdRatioBasedSampler(double ratio, Supplier splunkRumSupplier) { + this.splunkRumSupplier = splunkRumSupplier; // SessionId uses the same format as TraceId, so we can reuse trace ID ratio sampler. this.ratioBasedSampler = Sampler.traceIdRatioBased(ratio); } @@ -51,7 +52,12 @@ public SamplingResult shouldSample( List parentLinks) { // Replace traceId with sessionId return ratioBasedSampler.shouldSample( - parentContext, sessionId.getSessionId(), name, spanKind, attributes, parentLinks); + parentContext, + splunkRumSupplier.get().getRumSessionId(), + name, + spanKind, + attributes, + parentLinks); } @Override diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java index e72c44c6a..e21b099db 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java +++ b/splunk-otel-android/src/main/java/com/splunk/rum/SplunkRum.java @@ -36,6 +36,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.instrumentation.okhttp.v3_0.OkHttpTelemetry; import io.opentelemetry.rum.internal.GlobalAttributesSpanAppender; +import io.opentelemetry.rum.internal.OpenTelemetryRum; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; import java.util.concurrent.TimeUnit; @@ -79,8 +80,7 @@ public class SplunkRum { @Nullable private static SplunkRum INSTANCE; - private final SessionId sessionId; - private final OpenTelemetrySdk openTelemetrySdk; + private final OpenTelemetryRum openTelemetryRum; private final GlobalAttributesSpanAppender globalAttributes; static { @@ -88,12 +88,8 @@ public class SplunkRum { startupTimer.detectBackgroundStart(handler); } - SplunkRum( - OpenTelemetrySdk openTelemetrySdk, - SessionId sessionId, - GlobalAttributesSpanAppender globalAttributes) { - this.openTelemetrySdk = openTelemetrySdk; - this.sessionId = sessionId; + SplunkRum(OpenTelemetryRum openTelemetryRum, GlobalAttributesSpanAppender globalAttributes) { + this.openTelemetryRum = openTelemetryRum; this.globalAttributes = globalAttributes; } @@ -119,7 +115,8 @@ static SplunkRum initialize( if (builder.debugEnabled) { Log.i( LOG_TAG, - "Splunk RUM monitoring initialized with session ID: " + INSTANCE.sessionId); + "Splunk RUM monitoring initialized with session ID: " + + INSTANCE.getRumSessionId()); } return INSTANCE; @@ -163,7 +160,7 @@ public Call.Factory createRumOkHttpCallFactory(OkHttpClient client) { } private OkHttpTelemetry createOkHttpTracing() { - return OkHttpTelemetry.builder(openTelemetrySdk) + return OkHttpTelemetry.builder(getOpenTelemetry()) .addAttributesExtractor( new RumResponseAttributesExtractor(new ServerTimingHeaderParser())) .build(); @@ -174,7 +171,7 @@ private OkHttpTelemetry createOkHttpTracing() { * instrumentation. */ public OpenTelemetry getOpenTelemetry() { - return openTelemetrySdk; + return openTelemetryRum.getOpenTelemetry(); } /** @@ -183,7 +180,7 @@ public OpenTelemetry getOpenTelemetry() { * recommended that you do not cache this value, but always retrieve it from here when needed. */ public String getRumSessionId() { - return sessionId.getSessionId(); + return openTelemetryRum.getRumSessionId(); } /** @@ -247,7 +244,7 @@ public void addRumException(Throwable throwable, Attributes attributes) { } Tracer getTracer() { - return openTelemetrySdk.getTracer(RUM_TRACER_NAME); + return getOpenTelemetry().getTracer(RUM_TRACER_NAME); } void recordAnr(StackTraceElement[] stackTrace) { @@ -307,7 +304,13 @@ static void resetSingletonForTest() { // (currently) for testing only void flushSpans() { - openTelemetrySdk.getSdkTracerProvider().forceFlush().join(1, TimeUnit.SECONDS); + OpenTelemetry openTelemetry = getOpenTelemetry(); + if (openTelemetry instanceof OpenTelemetrySdk) { + ((OpenTelemetrySdk) openTelemetry) + .getSdkTracerProvider() + .forceFlush() + .join(1, TimeUnit.SECONDS); + } } /** diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/AppStateWatcher.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/ApplicationStateWatcher.java similarity index 69% rename from splunk-otel-android/src/main/java/com/splunk/rum/AppStateWatcher.java rename to splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/ApplicationStateWatcher.java index 206ae2d1e..6531c7674 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/AppStateWatcher.java +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/ApplicationStateWatcher.java @@ -14,25 +14,26 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; import android.app.Activity; import android.app.Application; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; -class AppStateWatcher implements Application.ActivityLifecycleCallbacks { +final class ApplicationStateWatcher implements Application.ActivityLifecycleCallbacks { - private final List appStateListeners; + private final List applicationStateListeners = + new CopyOnWriteArrayList<>(); // we count the number of activities that have been "started" and not yet "stopped" here to // figure out when the app goes into the background. private int numberOfOpenActivities = 0; - AppStateWatcher(List appStateListeners) { - this.appStateListeners = appStateListeners; - } + public ApplicationStateWatcher() {} @Override public void onActivityCreated( @@ -41,8 +42,8 @@ public void onActivityCreated( @Override public void onActivityStarted(@NonNull Activity activity) { if (numberOfOpenActivities == 0) { - for (AppStateListener appListener : appStateListeners) { - appListener.appForegrounded(); + for (ApplicationStateListener listener : applicationStateListeners) { + listener.onApplicationForegrounded(); } } numberOfOpenActivities++; @@ -57,8 +58,8 @@ public void onActivityPaused(@NonNull Activity activity) {} @Override public void onActivityStopped(@NonNull Activity activity) { if (--numberOfOpenActivities == 0) { - for (AppStateListener appListener : appStateListeners) { - appListener.appBackgrounded(); + for (ApplicationStateListener listener : applicationStateListeners) { + listener.onApplicationBackgrounded(); } } } @@ -68,4 +69,8 @@ public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bun @Override public void onActivityDestroyed(@NonNull Activity activity) {} + + void registerListener(ApplicationStateListener listener) { + applicationStateListeners.add(listener); + } } diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/InstrumentedApplicationImpl.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/InstrumentedApplicationImpl.java new file mode 100644 index 000000000..a8274d4e7 --- /dev/null +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/InstrumentedApplicationImpl.java @@ -0,0 +1,53 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.rum.internal; + +import android.app.Application; +import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener; +import io.opentelemetry.rum.internal.instrumentation.InstrumentedApplication; +import io.opentelemetry.sdk.OpenTelemetrySdk; + +final class InstrumentedApplicationImpl implements InstrumentedApplication { + + private final Application application; + private final OpenTelemetrySdk openTelemetrySdk; + private final ApplicationStateWatcher applicationStateWatcher; + + InstrumentedApplicationImpl( + Application application, + OpenTelemetrySdk openTelemetrySdk, + ApplicationStateWatcher applicationStateWatcher) { + this.application = application; + this.openTelemetrySdk = openTelemetrySdk; + this.applicationStateWatcher = applicationStateWatcher; + } + + @Override + public Application getApplication() { + return application; + } + + @Override + public OpenTelemetrySdk getOpenTelemetrySdk() { + return openTelemetrySdk; + } + + @Override + public void registerApplicationStateListener(ApplicationStateListener listener) { + applicationStateWatcher.registerListener(listener); + } +} diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/AppStateListener.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/NoopOpenTelemetryRum.java similarity index 56% rename from splunk-otel-android/src/main/java/com/splunk/rum/AppStateListener.java rename to splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/NoopOpenTelemetryRum.java index 773fe4880..8d2d50ed8 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/AppStateListener.java +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/NoopOpenTelemetryRum.java @@ -14,10 +14,22 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; -interface AppStateListener { - void appForegrounded(); +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.TraceId; - void appBackgrounded(); +enum NoopOpenTelemetryRum implements OpenTelemetryRum { + INSTANCE; + + @Override + public OpenTelemetry getOpenTelemetry() { + return OpenTelemetry.noop(); + } + + @Override + public String getRumSessionId() { + // RUM sessionId has the same format as traceId + return TraceId.getInvalid(); + } } diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRum.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRum.java new file mode 100644 index 000000000..13ca7a902 --- /dev/null +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRum.java @@ -0,0 +1,51 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.rum.internal; + +import io.opentelemetry.api.OpenTelemetry; + +/** + * Entrypoint for the OpenTelemetry Real User Monitoring library for Android. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface OpenTelemetryRum { + + /** Returns a new {@link OpenTelemetryRumBuilder} for {@link OpenTelemetryRum}. */ + static OpenTelemetryRumBuilder builder() { + return new OpenTelemetryRumBuilder(); + } + + /** Returns a no-op implementation of {@link OpenTelemetryRum}. */ + static OpenTelemetryRum noop() { + return NoopOpenTelemetryRum.INSTANCE; + } + + /** + * Get a handle to the instance of the {@linkplain OpenTelemetry OpenTelemetry API} that this + * instance is using for instrumentation. + */ + OpenTelemetry getOpenTelemetry(); + + /** + * Get the client session ID associated with this instance of the RUM instrumentation library. + * Note: this value will change throughout the lifetime of an application instance, so it is + * recommended that you do not cache this value, but always retrieve it from here when needed. + */ + String getRumSessionId(); +} diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRumBuilder.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRumBuilder.java new file mode 100644 index 000000000..5cfe91ebd --- /dev/null +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRumBuilder.java @@ -0,0 +1,209 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.rum.internal; + +import android.app.Application; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.rum.internal.instrumentation.InstrumentedApplication; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +/** + * A builder of {@link OpenTelemetryRum}. It enabled configuring the OpenTelemetry SDK and disabling + * built-in Android instrumentations. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class OpenTelemetryRumBuilder { + + private Resource resource = Resource.getDefault(); + private final List> + tracerProviderCustomizers = new ArrayList<>(); + private final List> + meterProviderCustomizers = new ArrayList<>(); + private final List> + loggerProviderCustomizers = new ArrayList<>(); + private final List> instrumentationInstallers = + new ArrayList<>(); + + OpenTelemetryRumBuilder() {} + + /** + * Assign a {@link Resource} to be attached to all telemetry emitted by the {@link + * OpenTelemetryRum} created by this builder. + * + * @return {@code this} + */ + public OpenTelemetryRumBuilder setResource(Resource resource) { + this.resource = resource; + return this; + } + + /** + * Adds a {@link BiFunction} to invoke the with the {@link SdkTracerProviderBuilder} to allow + * customization. The return value of the {@link BiFunction} will replace the passed-in + * argument. + * + *

Multiple calls will execute the customizers in order. + * + *

Note: calling {@link SdkTracerProviderBuilder#setResource(Resource)} inside of your + * configuration function will cause any resource customizers to be ignored that were configured + * via {@link #setResource(Resource)}. + * + * @return {@code this} + */ + public OpenTelemetryRumBuilder addTracerProviderCustomizer( + BiFunction + customizer) { + tracerProviderCustomizers.add(customizer); + return this; + } + + /** + * Adds a {@link BiFunction} to invoke the with the {@link SdkMeterProviderBuilder} to allow + * customization. The return value of the {@link BiFunction} will replace the passed-in + * argument. + * + *

Multiple calls will execute the customizers in order. + * + *

Note: calling {@link SdkMeterProviderBuilder#setResource(Resource)} inside of your + * configuration function will cause any resource customizers to be ignored that were configured + * via {@link #setResource(Resource)}. + * + * @return {@code this} + */ + public OpenTelemetryRumBuilder addMeterProviderCustomizer( + BiFunction customizer) { + meterProviderCustomizers.add(customizer); + return this; + } + + /** + * Adds a {@link BiFunction} to invoke the with the {@link SdkLoggerProviderBuilder} to allow + * customization. The return value of the {@link BiFunction} will replace the passed-in + * argument. + * + *

Multiple calls will execute the customizers in order. + * + *

Note: calling {@link SdkLoggerProviderBuilder#setResource(Resource)} inside of your + * configuration function will cause any resource customizers to be ignored that were configured + * via {@link #setResource(Resource)}. + * + * @return {@code this} + */ + public OpenTelemetryRumBuilder addLoggerProviderCustomizer( + BiFunction + customizer) { + loggerProviderCustomizers.add(customizer); + return this; + } + + /** + * Adds an instrumentation installer function that will be run on an {@link + * InstrumentedApplication} instance as a part of the {@link #build(Application)} method call. + * + * @return {@code this} + */ + public OpenTelemetryRumBuilder addInstrumentation( + Consumer instrumentationInstaller) { + instrumentationInstallers.add(instrumentationInstaller); + return this; + } + + /** + * Creates a new instance of {@link OpenTelemetryRum} with the settings of this {@link + * OpenTelemetryRumBuilder}. + * + *

This method will initialize the OpenTelemetry SDK and install built-in system + * instrumentations in the passed Android {@link Application}. + * + * @param application The {@link Application} that is being instrumented. + * @return A new {@link OpenTelemetryRum} instance. + */ + public OpenTelemetryRum build(Application application) { + // the app state listeners need to be run in the first ActivityLifecycleCallbacks since they + // might turn off/on additional telemetry depending on whether the app is active or not + ApplicationStateWatcher applicationStateWatcher = new ApplicationStateWatcher(); + application.registerActivityLifecycleCallbacks(applicationStateWatcher); + + SessionIdTimeoutHandler timeoutHandler = new SessionIdTimeoutHandler(); + SessionId sessionId = new SessionId(timeoutHandler); + applicationStateWatcher.registerListener(timeoutHandler); + + OpenTelemetrySdk openTelemetrySdk = + OpenTelemetrySdk.builder() + .setTracerProvider(buildTracerProvider(sessionId, application)) + .setMeterProvider(buildMeterProvider(application)) + .setLoggerProvider(buildLoggerProvider(application)) + .build(); + + Tracer tracer = openTelemetrySdk.getTracer(OpenTelemetryRum.class.getSimpleName()); + sessionId.setSessionIdChangeListener(new SessionIdChangeTracer(tracer)); + + InstrumentedApplication instrumentedApplication = + new InstrumentedApplicationImpl( + application, openTelemetrySdk, applicationStateWatcher); + for (Consumer installer : instrumentationInstallers) { + installer.accept(instrumentedApplication); + } + + return new OpenTelemetryRumImpl(openTelemetrySdk, sessionId); + } + + private SdkTracerProvider buildTracerProvider(SessionId sessionId, Application application) { + SdkTracerProviderBuilder tracerProviderBuilder = + SdkTracerProvider.builder() + .setResource(resource) + .addSpanProcessor(new SessionIdSpanAppender(sessionId)); + for (BiFunction + customizer : tracerProviderCustomizers) { + tracerProviderBuilder = customizer.apply(tracerProviderBuilder, application); + } + return tracerProviderBuilder.build(); + } + + private SdkMeterProvider buildMeterProvider(Application application) { + SdkMeterProviderBuilder meterProviderBuilder = + SdkMeterProvider.builder().setResource(resource); + for (BiFunction customizer : + meterProviderCustomizers) { + meterProviderBuilder = customizer.apply(meterProviderBuilder, application); + } + return meterProviderBuilder.build(); + } + + private SdkLoggerProvider buildLoggerProvider(Application application) { + SdkLoggerProviderBuilder loggerProviderBuilder = + SdkLoggerProvider.builder().setResource(resource); + for (BiFunction + customizer : loggerProviderCustomizers) { + loggerProviderBuilder = customizer.apply(loggerProviderBuilder, application); + } + return loggerProviderBuilder.build(); + } +} diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRumImpl.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRumImpl.java new file mode 100644 index 000000000..2b749fc4a --- /dev/null +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/OpenTelemetryRumImpl.java @@ -0,0 +1,41 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.rum.internal; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.OpenTelemetrySdk; + +final class OpenTelemetryRumImpl implements OpenTelemetryRum { + + private final OpenTelemetrySdk openTelemetrySdk; + private final SessionId sessionId; + + OpenTelemetryRumImpl(OpenTelemetrySdk openTelemetrySdk, SessionId sessionId) { + this.openTelemetrySdk = openTelemetrySdk; + this.sessionId = sessionId; + } + + @Override + public OpenTelemetry getOpenTelemetry() { + return openTelemetrySdk; + } + + @Override + public String getRumSessionId() { + return sessionId.getSessionId(); + } +} diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SessionId.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionId.java similarity index 98% rename from splunk-otel-android/src/main/java/com/splunk/rum/SessionId.java rename to splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionId.java index a7361e2d0..7d71b4978 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SessionId.java +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionId.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; import static java.util.Objects.requireNonNull; diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdChangeListener.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdChangeListener.java similarity index 95% rename from splunk-otel-android/src/main/java/com/splunk/rum/SessionIdChangeListener.java rename to splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdChangeListener.java index 65c803e1c..01966b3b7 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdChangeListener.java +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdChangeListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; interface SessionIdChangeListener { diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdChangeTracer.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdChangeTracer.java similarity index 97% rename from splunk-otel-android/src/main/java/com/splunk/rum/SessionIdChangeTracer.java rename to splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdChangeTracer.java index 27f5ecded..96a075501 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdChangeTracer.java +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdChangeTracer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; import static io.opentelemetry.api.common.AttributeKey.stringKey; diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdSpanAppender.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdSpanAppender.java new file mode 100644 index 000000000..c4494e092 --- /dev/null +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdSpanAppender.java @@ -0,0 +1,55 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.rum.internal; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +final class SessionIdSpanAppender implements SpanProcessor { + + // TODO: rename to something that is not splunk specific + static final AttributeKey SESSION_ID_KEY = stringKey("splunk.rumSessionId"); + + private final SessionId sessionId; + + public SessionIdSpanAppender(SessionId sessionId) { + this.sessionId = sessionId; + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + span.setAttribute(SESSION_ID_KEY, sessionId.getSessionId()); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) {} + + @Override + public boolean isEndRequired() { + return false; + } +} diff --git a/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdTimeoutHandler.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdTimeoutHandler.java similarity index 90% rename from splunk-otel-android/src/main/java/com/splunk/rum/SessionIdTimeoutHandler.java rename to splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdTimeoutHandler.java index f2d55ad92..791aa43fc 100644 --- a/splunk-otel-android/src/main/java/com/splunk/rum/SessionIdTimeoutHandler.java +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/SessionIdTimeoutHandler.java @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; +import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener; import io.opentelemetry.sdk.common.Clock; import java.util.concurrent.TimeUnit; @@ -33,7 +34,7 @@ *

Consequently, when the app spent >15 minutes without any activity (spans) in the background, * after moving to the foreground the first span should trigger the sessionId timeout. */ -final class SessionIdTimeoutHandler implements AppStateListener { +final class SessionIdTimeoutHandler implements ApplicationStateListener { private static final long SESSION_TIMEOUT_NANOS = TimeUnit.MINUTES.toNanos(15); @@ -51,12 +52,12 @@ final class SessionIdTimeoutHandler implements AppStateListener { } @Override - public void appForegrounded() { + public void onApplicationForegrounded() { state = State.TRANSITIONING_TO_FOREGROUND; } @Override - public void appBackgrounded() { + public void onApplicationBackgrounded() { state = State.BACKGROUND; } diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/ApplicationStateListener.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/ApplicationStateListener.java new file mode 100644 index 000000000..cf30b877f --- /dev/null +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/ApplicationStateListener.java @@ -0,0 +1,35 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.rum.internal.instrumentation; + +/** + * Listener interface that is called whenever the instrumented application is brought to foreground + * from the background, or vice versa. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface ApplicationStateListener { + + /** + * Called whenever the application is brought to the foreground (i.e. first activity starts). + */ + void onApplicationForegrounded(); + + /** Called whenever the application is brought to the background (i.e. last activity stops). */ + void onApplicationBackgrounded(); +} diff --git a/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/InstrumentedApplication.java b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/InstrumentedApplication.java new file mode 100644 index 000000000..f853bd050 --- /dev/null +++ b/splunk-otel-android/src/main/java/io/opentelemetry/rum/internal/instrumentation/InstrumentedApplication.java @@ -0,0 +1,49 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.rum.internal.instrumentation; + +import android.app.Application; +import io.opentelemetry.rum.internal.OpenTelemetryRum; +import io.opentelemetry.sdk.OpenTelemetrySdk; + +/** + * Provides access to the {@linkplain OpenTelemetrySdk OpenTelemetry SDK}, the instrumented {@link + * Application}, allows registering {@linkplain ApplicationStateListener listeners}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface InstrumentedApplication { + + /** Returns the instrumented {@link Application}. */ + Application getApplication(); + + /** + * Returns the {@link OpenTelemetrySdk} that is a part of the constructed {@link + * OpenTelemetryRum}. + */ + OpenTelemetrySdk getOpenTelemetrySdk(); + + /** + * Registers the passed {@link ApplicationStateListener} - from now on it will be called + * whenever the application is moved from background to foreground, and vice versa. + * + *

Users of this method should take care to avoid passing the same listener instance multiple + * times; duplicates are not trimmed. + */ + void registerApplicationStateListener(ApplicationStateListener listener); +} diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/RumAttributeAppenderTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/RumAttributeAppenderTest.java index 1144675d5..df008270d 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/RumAttributeAppenderTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/RumAttributeAppenderTest.java @@ -31,7 +31,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class RumAttributeAppenderTest { +class RumAttributeAppenderTest { private VisibleScreenTracker visibleScreenTracker; private final ConnectionUtil connectionUtil = mock(ConnectionUtil.class); @@ -49,8 +49,7 @@ void setUp() { @Test void interfaceMethods() { RumAttributeAppender rumAttributeAppender = - new RumAttributeAppender( - mock(SessionId.class), visibleScreenTracker, connectionUtil); + new RumAttributeAppender(visibleScreenTracker, connectionUtil); assertTrue(rumAttributeAppender.isStartRequired()); assertFalse(rumAttributeAppender.isEndRequired()); @@ -58,17 +57,14 @@ void interfaceMethods() { @Test void appendAttributesOnStart() { - SessionId sessionId = mock(SessionId.class); - when(sessionId.getSessionId()).thenReturn("rumSessionId"); when(visibleScreenTracker.getCurrentlyVisibleScreen()).thenReturn("ScreenOne"); ReadWriteSpan span = mock(ReadWriteSpan.class); RumAttributeAppender rumAttributeAppender = - new RumAttributeAppender(sessionId, visibleScreenTracker, connectionUtil); + new RumAttributeAppender(visibleScreenTracker, connectionUtil); rumAttributeAppender.onStart(Context.current(), span); - verify(span).setAttribute(RumAttributeAppender.SESSION_ID_KEY, "rumSessionId"); verify(span).setAttribute(SplunkRum.SCREEN_NAME_KEY, "ScreenOne"); verify(span).setAttribute(SemanticAttributes.NET_HOST_CONNECTION_TYPE, "cell"); verify(span).setAttribute(SemanticAttributes.NET_HOST_CONNECTION_SUBTYPE, "LTE"); @@ -76,14 +72,12 @@ void appendAttributesOnStart() { @Test void appendAttributes_noCurrentScreens() { - SessionId sessionId = mock(SessionId.class); - when(sessionId.getSessionId()).thenReturn("rumSessionId"); when(visibleScreenTracker.getCurrentlyVisibleScreen()).thenReturn("unknown"); ReadWriteSpan span = mock(ReadWriteSpan.class); RumAttributeAppender rumAttributeAppender = - new RumAttributeAppender(sessionId, visibleScreenTracker, connectionUtil); + new RumAttributeAppender(visibleScreenTracker, connectionUtil); rumAttributeAppender.onStart(Context.current(), span); verify(span).setAttribute(SplunkRum.SCREEN_NAME_KEY, "unknown"); diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java index 0989d2fb9..a00814a70 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/RumInitializerTest.java @@ -91,9 +91,7 @@ SpanExporter buildFilteringExporter(ConnectionUtil connectionUtil) { assertTrue(events.size() > 0); checkEventExists(events, "connectionUtilInitialized"); checkEventExists(events, "exporterInitialized"); - checkEventExists(events, "sessionIdInitialized"); checkEventExists(events, "tracerProviderInitialized"); - checkEventExists(events, "openTelemetrySdkInitialized"); checkEventExists(events, "activityLifecycleCallbacksInitialized"); checkEventExists(events, "crashReportingInitialized"); checkEventExists(events, "anrMonitorInitialized"); diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdRatioBasedSamplerTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdRatioBasedSamplerTest.java index 4ed81f294..761fe254a 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdRatioBasedSamplerTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdRatioBasedSamplerTest.java @@ -17,7 +17,6 @@ package com.splunk.rum; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import io.opentelemetry.api.common.Attributes; @@ -34,6 +33,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -47,38 +47,37 @@ class SessionIdRatioBasedSamplerTest { private final List parentLinks = Collections.singletonList(LinkData.create(SpanContext.getInvalid())); + @Mock SplunkRum splunkRum; + @Test void samplerUsesSessionId() { - SessionId sessionId = mock(SessionId.class); - SessionIdRatioBasedSampler sampler = new SessionIdRatioBasedSampler(0.5, sessionId); + SessionIdRatioBasedSampler sampler = new SessionIdRatioBasedSampler(0.5, () -> splunkRum); // Sampler drops if TraceIdRatioBasedSampler would drop this sessionId - when(sessionId.getSessionId()).thenReturn(HIGH_ID); + when(splunkRum.getRumSessionId()).thenReturn(HIGH_ID); assertEquals(shouldSample(sampler), SamplingDecision.DROP); // Sampler accepts if TraceIdRatioBasedSampler would accept this sessionId - when(sessionId.getSessionId()).thenReturn(LOW_ID); + when(splunkRum.getRumSessionId()).thenReturn(LOW_ID); assertEquals(shouldSample(sampler), SamplingDecision.RECORD_AND_SAMPLE); } @Test void zeroRatioDropsAll() { - SessionId sessionId = mock(SessionId.class); - SessionIdRatioBasedSampler sampler = new SessionIdRatioBasedSampler(0.0, sessionId); + SessionIdRatioBasedSampler sampler = new SessionIdRatioBasedSampler(0.0, () -> splunkRum); for (String id : Arrays.asList(HIGH_ID, LOW_ID)) { - when(sessionId.getSessionId()).thenReturn(id); + when(splunkRum.getRumSessionId()).thenReturn(id); assertEquals(shouldSample(sampler), SamplingDecision.DROP); } } @Test void oneRatioAcceptsAll() { - SessionId sessionId = mock(SessionId.class); - SessionIdRatioBasedSampler sampler = new SessionIdRatioBasedSampler(1.0, sessionId); + SessionIdRatioBasedSampler sampler = new SessionIdRatioBasedSampler(1.0, () -> splunkRum); for (String id : Arrays.asList(HIGH_ID, LOW_ID)) { - when(sessionId.getSessionId()).thenReturn(id); + when(splunkRum.getRumSessionId()).thenReturn(id); assertEquals(shouldSample(sampler), SamplingDecision.RECORD_AND_SAMPLE); } } diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumTest.java b/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumTest.java index 882bda7cf..bc6eee5a4 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumTest.java +++ b/splunk-otel-android/src/test/java/com/splunk/rum/SplunkRumTest.java @@ -43,6 +43,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; import io.opentelemetry.rum.internal.GlobalAttributesSpanAppender; +import io.opentelemetry.rum.internal.OpenTelemetryRum; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; @@ -69,6 +70,7 @@ public class SplunkRumTest { private Tracer tracer; + @Mock private OpenTelemetryRum openTelemetryRum; @Mock private GlobalAttributesSpanAppender globalAttributes; @BeforeEach @@ -159,11 +161,9 @@ void nonNullMethods() { @Test void addEvent() { - SplunkRum splunkRum = - new SplunkRum( - (OpenTelemetrySdk) otelTesting.getOpenTelemetry(), - new SessionId(new SessionIdTimeoutHandler()), - globalAttributes); + when(openTelemetryRum.getOpenTelemetry()).thenReturn(otelTesting.getOpenTelemetry()); + + SplunkRum splunkRum = new SplunkRum(openTelemetryRum, globalAttributes); Attributes attributes = Attributes.of(stringKey("one"), "1", longKey("two"), 2L); splunkRum.addRumEvent("foo", attributes); @@ -182,11 +182,9 @@ void recordAnr() { stringBuilder.append(stackTraceElement).append("\n"); } - SplunkRum splunkRum = - new SplunkRum( - (OpenTelemetrySdk) otelTesting.getOpenTelemetry(), - new SessionId(new SessionIdTimeoutHandler()), - globalAttributes); + when(openTelemetryRum.getOpenTelemetry()).thenReturn(otelTesting.getOpenTelemetry()); + + SplunkRum splunkRum = new SplunkRum(openTelemetryRum, globalAttributes); Attributes expectedAttributes = Attributes.of( @@ -210,9 +208,9 @@ void addException() { InMemorySpanExporter testExporter = InMemorySpanExporter.create(); OpenTelemetrySdk testSdk = buildTestSdk(testExporter); - SplunkRum splunkRum = - new SplunkRum( - testSdk, new SessionId(new SessionIdTimeoutHandler()), globalAttributes); + when(openTelemetryRum.getOpenTelemetry()).thenReturn(testSdk); + + SplunkRum splunkRum = new SplunkRum(openTelemetryRum, globalAttributes); NullPointerException exception = new NullPointerException("oopsie"); Attributes attributes = Attributes.of(stringKey("one"), "1", longKey("two"), 2L); @@ -241,11 +239,9 @@ private OpenTelemetrySdk buildTestSdk(InMemorySpanExporter testExporter) { @Test void createAndEnd() { - SplunkRum splunkRum = - new SplunkRum( - (OpenTelemetrySdk) otelTesting.getOpenTelemetry(), - new SessionId(new SessionIdTimeoutHandler()), - globalAttributes); + when(openTelemetryRum.getOpenTelemetry()).thenReturn(otelTesting.getOpenTelemetry()); + + SplunkRum splunkRum = new SplunkRum(openTelemetryRum, globalAttributes); Span span = splunkRum.startWorkflow("workflow"); Span inner = tracer.spanBuilder("foo").startSpan(); @@ -307,11 +303,7 @@ void updateLocation() { .when(globalAttributes) .update(any()); - SplunkRum splunkRum = - new SplunkRum( - (OpenTelemetrySdk) otelTesting.getOpenTelemetry(), - new SessionId(new SessionIdTimeoutHandler()), - globalAttributes); + SplunkRum splunkRum = new SplunkRum(openTelemetryRum, globalAttributes); Location location = mock(Location.class); when(location.getLatitude()).thenReturn(42d); diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/AppStateWatcherTest.java b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/ApplicationStateWatcherTest.java similarity index 56% rename from splunk-otel-android/src/test/java/com/splunk/rum/AppStateWatcherTest.java rename to splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/ApplicationStateWatcherTest.java index 8e036f0c6..618278a02 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/AppStateWatcherTest.java +++ b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/ApplicationStateWatcherTest.java @@ -14,35 +14,37 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; -import static java.util.Arrays.asList; import static org.mockito.Mockito.inOrder; import android.app.Activity; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InOrder; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; -@RunWith(MockitoJUnitRunner.class) -public class AppStateWatcherTest { +@ExtendWith(MockitoExtension.class) +class ApplicationStateWatcherTest { @Mock Activity activity; - @Mock AppStateListener listener1; - @Mock AppStateListener listener2; + @Mock ApplicationStateListener listener1; + @Mock ApplicationStateListener listener2; - AppStateWatcher underTest; + ApplicationStateWatcher underTest; - @Before - public void setUp() { - underTest = new AppStateWatcher(asList(listener1, listener2)); + @BeforeEach + void setUp() { + underTest = new ApplicationStateWatcher(); + underTest.registerListener(listener1); + underTest.registerListener(listener2); } @Test - public void appForegrounded() { + void appForegrounded() { underTest.onActivityStarted(activity); underTest.onActivityStarted(activity); underTest.onActivityStopped(activity); @@ -50,23 +52,23 @@ public void appForegrounded() { underTest.onActivityStopped(activity); InOrder io = inOrder(listener1, listener2); - io.verify(listener1).appForegrounded(); - io.verify(listener2).appForegrounded(); + io.verify(listener1).onApplicationForegrounded(); + io.verify(listener2).onApplicationForegrounded(); io.verifyNoMoreInteractions(); } @Test - public void appBackgrounded() { + void appBackgrounded() { underTest.onActivityStarted(activity); underTest.onActivityStarted(activity); underTest.onActivityStopped(activity); underTest.onActivityStopped(activity); InOrder io = inOrder(listener1, listener2); - io.verify(listener1).appForegrounded(); - io.verify(listener2).appForegrounded(); - io.verify(listener1).appBackgrounded(); - io.verify(listener2).appBackgrounded(); + io.verify(listener1).onApplicationForegrounded(); + io.verify(listener2).onApplicationForegrounded(); + io.verify(listener1).onApplicationBackgrounded(); + io.verify(listener2).onApplicationBackgrounded(); io.verifyNoMoreInteractions(); } } diff --git a/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/GlobalAttributesSpanAppenderTest.java b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/GlobalAttributesSpanAppenderTest.java index 4bc3c9a2b..6660cc793 100644 --- a/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/GlobalAttributesSpanAppenderTest.java +++ b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/GlobalAttributesSpanAppenderTest.java @@ -18,14 +18,14 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.verify; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.context.Context; import io.opentelemetry.sdk.trace.ReadWriteSpan; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; diff --git a/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/OpenTelemetryRumBuilderTest.java b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/OpenTelemetryRumBuilderTest.java new file mode 100644 index 000000000..e259e94fa --- /dev/null +++ b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/OpenTelemetryRumBuilderTest.java @@ -0,0 +1,106 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.rum.internal; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.app.Application; +import io.opentelemetry.rum.internal.instrumentation.ApplicationStateListener; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class OpenTelemetryRumBuilderTest { + + final Resource resource = + Resource.getDefault().toBuilder().put("test.attribute", "abcdef").build(); + final InMemorySpanExporter spanExporter = InMemorySpanExporter.create(); + + @Mock Application application; + @Mock Activity activity; + @Mock ApplicationStateListener listener; + + @Captor ArgumentCaptor activityCallbacksCaptor; + + @Test + void shouldRegisterApplicationStateWatcher() { + OpenTelemetryRum.builder().build(application); + + verify(application).registerActivityLifecycleCallbacks(isA(ApplicationStateWatcher.class)); + } + + @Test + void shouldBuildTracerProvider() { + OpenTelemetryRum openTelemetryRum = + OpenTelemetryRum.builder() + .setResource(resource) + .addTracerProviderCustomizer( + (tracerProviderBuilder, app) -> + tracerProviderBuilder.addSpanProcessor( + SimpleSpanProcessor.create(spanExporter))) + .build(application); + + String sessionId = openTelemetryRum.getRumSessionId(); + openTelemetryRum + .getOpenTelemetry() + .getTracer("test") + .spanBuilder("test span") + .startSpan() + .end(); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).hasSize(1); + assertThat(spans.get(0)) + .hasName("test span") + .hasResource(resource) + .hasAttributesSatisfyingExactly( + equalTo(SessionIdSpanAppender.SESSION_ID_KEY, sessionId)); + } + + @Test + void shouldInstallInstrumentation() { + OpenTelemetryRum.builder() + .addInstrumentation( + instrumentedApplication -> { + assertThat(instrumentedApplication.getApplication()) + .isSameAs(application); + instrumentedApplication.registerApplicationStateListener(listener); + }) + .build(application); + + verify(application).registerActivityLifecycleCallbacks(activityCallbacksCaptor.capture()); + + activityCallbacksCaptor.getValue().onActivityStarted(activity); + verify(listener).onApplicationForegrounded(); + + activityCallbacksCaptor.getValue().onActivityStopped(activity); + verify(listener).onApplicationBackgrounded(); + } +} diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdChangeTracerTest.java b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdChangeTracerTest.java similarity index 98% rename from splunk-otel-android/src/test/java/com/splunk/rum/SessionIdChangeTracerTest.java rename to splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdChangeTracerTest.java index 5a1c1756a..c2a210a15 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdChangeTracerTest.java +++ b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdChangeTracerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdSpanAppenderTest.java b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdSpanAppenderTest.java new file mode 100644 index 000000000..f5eb77639 --- /dev/null +++ b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdSpanAppenderTest.java @@ -0,0 +1,50 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.rum.internal; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SessionIdSpanAppenderTest { + + @Mock SessionId sessionId; + @Mock ReadWriteSpan span; + + @Test + void shouldSetSessionIdAsSpanAttribute() { + when(sessionId.getSessionId()).thenReturn("42"); + + SessionIdSpanAppender underTest = new SessionIdSpanAppender(sessionId); + + assertTrue(underTest.isStartRequired()); + underTest.onStart(Context.root(), span); + + verify(span).setAttribute(SessionIdSpanAppender.SESSION_ID_KEY, "42"); + + assertFalse(underTest.isEndRequired()); + } +} diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdTest.java b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdTest.java similarity index 99% rename from splunk-otel-android/src/test/java/com/splunk/rum/SessionIdTest.java rename to splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdTest.java index 7419800e3..0fa2facaa 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdTest.java +++ b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; diff --git a/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdTimeoutHandlerTest.java b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdTimeoutHandlerTest.java similarity index 93% rename from splunk-otel-android/src/test/java/com/splunk/rum/SessionIdTimeoutHandlerTest.java rename to splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdTimeoutHandlerTest.java index 699b12111..e0265ca73 100644 --- a/splunk-otel-android/src/test/java/com/splunk/rum/SessionIdTimeoutHandlerTest.java +++ b/splunk-otel-android/src/test/java/io/opentelemetry/rum/internal/SessionIdTimeoutHandlerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.splunk.rum; +package io.opentelemetry.rum.internal; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -44,7 +44,7 @@ void shouldApply15MinutesTimeoutToAppsInBackground() { TestClock clock = TestClock.create(); SessionIdTimeoutHandler timeoutHandler = new SessionIdTimeoutHandler(clock); - timeoutHandler.appBackgrounded(); + timeoutHandler.onApplicationBackgrounded(); timeoutHandler.bump(); assertFalse(timeoutHandler.hasTimedOut()); @@ -75,11 +75,11 @@ void shouldApplyTimeoutToFirstSpanAfterAppBeingMovedToForeground() { TestClock clock = TestClock.create(); SessionIdTimeoutHandler timeoutHandler = new SessionIdTimeoutHandler(clock); - timeoutHandler.appBackgrounded(); + timeoutHandler.onApplicationBackgrounded(); timeoutHandler.bump(); // the first span after app is moved to the foreground gets timed out - timeoutHandler.appForegrounded(); + timeoutHandler.onApplicationForegrounded(); clock.advance(20, TimeUnit.MINUTES); assertTrue(timeoutHandler.hasTimedOut()); timeoutHandler.bump();