From 641411bb0d42eb3996fefb911c0ceea556f9d094 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Thu, 5 Dec 2024 10:30:38 -0800 Subject: [PATCH 1/8] rebase --- .../android/SdkPreconfiguredRumBuilder.kt | 9 +++ .../android/SessionIdEventSender.kt | 40 ++++++++++ .../android/OpenTelemetryRumBuilderTest.java | 18 +++-- .../android/SessionIdEventSenderTest.java | 77 +++++++++++++++++++ 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 core/src/main/java/io/opentelemetry/android/SessionIdEventSender.kt create mode 100644 core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java diff --git a/core/src/main/java/io/opentelemetry/android/SdkPreconfiguredRumBuilder.kt b/core/src/main/java/io/opentelemetry/android/SdkPreconfiguredRumBuilder.kt index 7bf926952..78891dfc0 100644 --- a/core/src/main/java/io/opentelemetry/android/SdkPreconfiguredRumBuilder.kt +++ b/core/src/main/java/io/opentelemetry/android/SdkPreconfiguredRumBuilder.kt @@ -12,6 +12,7 @@ import io.opentelemetry.android.instrumentation.InstallationContext import io.opentelemetry.android.internal.services.ServiceManager import io.opentelemetry.android.session.SessionManager import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider class SdkPreconfiguredRumBuilder @JvmOverloads @@ -51,6 +52,14 @@ class SdkPreconfiguredRumBuilder // might turn off/on additional telemetry depending on whether the app is active or not appLifecycleService.registerListener(timeoutHandler) + val eventLogger = + SdkEventLoggerProvider.create(sdk.logsBridge) + .get(OpenTelemetryRum::class.java.simpleName) + + sessionManager.addObserver(SessionIdEventSender(eventLogger)) + // After addObserver(), we call getSessionId() to trigger a session.start event + sessionManager.getSessionId() + val openTelemetryRum = OpenTelemetryRumImpl(sdk, sessionManager) // Install instrumentations diff --git a/core/src/main/java/io/opentelemetry/android/SessionIdEventSender.kt b/core/src/main/java/io/opentelemetry/android/SessionIdEventSender.kt new file mode 100644 index 000000000..45a67cad6 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/SessionIdEventSender.kt @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android + +import io.opentelemetry.android.common.RumConstants.Events.EVENT_SESSION_END +import io.opentelemetry.android.common.RumConstants.Events.EVENT_SESSION_START +import io.opentelemetry.android.session.Session +import io.opentelemetry.android.session.SessionObserver +import io.opentelemetry.api.incubator.events.EventLogger +import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes.SESSION_ID +import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes.SESSION_PREVIOUS_ID + +internal class SessionIdEventSender(private val eventLogger: EventLogger) : SessionObserver { + override fun onSessionStarted( + newSession: Session, + previousSession: Session, + ) { + val eventBuilder = + eventLogger + .builder(EVENT_SESSION_START) + .put(SESSION_ID, newSession.getId()) + val previousSessionId = previousSession.getId() + if (previousSessionId.isNotEmpty()) { + eventBuilder.put(SESSION_PREVIOUS_ID, previousSessionId) + } + eventBuilder.emit() + } + + override fun onSessionEnded(session: Session) { + if (session.getId().isEmpty()) { + return + } + eventLogger.builder(EVENT_SESSION_END) + .put(SESSION_ID, session.getId()) + .emit() + } +} diff --git a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index 28654643c..a62f8d724 100644 --- a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java +++ b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java @@ -55,12 +55,12 @@ import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.contrib.disk.buffering.SpanToDiskExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.LogRecordExporter; import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider; import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.data.SpanData; @@ -69,6 +69,7 @@ import java.io.IOException; import java.time.Duration; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -173,8 +174,10 @@ public void shouldBuildLogRecordProvider() { eventLogger.builder("test.event").put("body.field", "foo").setAttributes(attrs).emit(); List logs = logsExporter.getFinishedLogRecordItems(); - assertThat(logs).hasSize(1); + assertThat(logs).hasSize(2); assertThat(logs.get(0)) + .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.start")); + assertThat(logs.get(1)) .hasAttributesSatisfyingExactly( equalTo(SESSION_ID, openTelemetryRum.getRumSessionId()), equalTo(stringKey("event.name"), "test.event"), @@ -182,7 +185,7 @@ public void shouldBuildLogRecordProvider() { equalTo(stringKey("mega"), "hit")) .hasResource(resource); - Value bodyValue = logs.get(0).getBodyValue(); + Value bodyValue = logs.get(1).getBodyValue(); List payload = (List) bodyValue.getValue(); assertThat(payload).hasSize(1); KeyValue expected = KeyValue.of("body.field", Value.of("foo")); @@ -318,8 +321,11 @@ public void setLogRecordExporterCustomizer() { () -> assertThat(logsExporter.getFinishedLogRecordItems()).isNotEmpty()); assertThat(wasCalled.get()).isTrue(); Collection logs = logsExporter.getFinishedLogRecordItems(); - assertThat(logs).hasSize(1); - assertThat(logs.iterator().next()) + assertThat(logs).hasSize(2); + Iterator iter = logs.iterator(); + assertThat(iter.next()) + .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.start")); + assertThat(iter.next()) .hasBody("foo") .hasAttributesSatisfyingExactly( equalTo(stringKey("bing"), "bang"), @@ -433,7 +439,7 @@ public void verifyGlobalAttrsForLogs() { List recordedLogs = logRecordExporter.getFinishedLogRecordItems(); assertThat(recordedLogs).hasSize(1); LogRecordData logRecordData = recordedLogs.get(0); - OpenTelemetryAssertions.assertThat(logRecordData) + assertThat(logRecordData) .hasAttributes( Attributes.builder() .put(SESSION_ID, rum.getRumSessionId()) diff --git a/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java b/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java new file mode 100644 index 000000000..4d2a077f2 --- /dev/null +++ b/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.android.session.Session; +import io.opentelemetry.api.incubator.events.EventLogger; +import io.opentelemetry.api.incubator.logs.KeyAnyValue; +import io.opentelemetry.api.logs.LoggerProvider; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.internal.AnyValueBody; +import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SessionIdEventSenderTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private SessionIdEventSender underTest; + + @BeforeEach + void setup() { + LoggerProvider loggerProvider = otelTesting.getOpenTelemetry().getLogsBridge(); + EventLogger eventLogger = SdkEventLoggerProvider.create(loggerProvider).get("testLogging"); + underTest = new SessionIdEventSender(eventLogger); + } + + @Test + void shouldEmitSessionStartEvent() { + Session newSession = new Session.DefaultSession("123", 0); + Session oldSession = new Session.DefaultSession("666", 0); + underTest.onSessionStarted(newSession, oldSession); + + List logs = otelTesting.getLogRecords(); + assertEquals(1, logs.size()); + LogRecordData log = logs.get(0); + // TODO: Use new event body assertions when available. + assertThat(log) + .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.start")); + + AnyValueBody body = (AnyValueBody) log.getBody(); + List kvBody = (List) body.asAnyValue().getValue(); + assertThat(kvBody.get(0).getKey()).isEqualTo("session.previous_id"); + assertThat(kvBody.get(0).getAnyValue().asString()).isEqualTo("666"); + assertThat(kvBody.get(1).getKey()).isEqualTo("session.id"); + assertThat(kvBody.get(1).getAnyValue().asString()).isEqualTo("123"); + } + + @Test + void shouldEmitSessionEndEvent() { + Session session = new Session.DefaultSession("123", 0); + underTest.onSessionEnded(session); + + List logs = otelTesting.getLogRecords(); + assertEquals(1, logs.size()); + LogRecordData log = logs.get(0); + // TODO: Use new event body assertions when available. + assertThat(log) + .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.end")); + + AnyValueBody body = (AnyValueBody) log.getBody(); + List kvBody = (List) body.asAnyValue().getValue(); + assertThat(kvBody.get(0).getKey()).isEqualTo("session.id"); + assertThat(kvBody.get(0).getAnyValue().asString()).isEqualTo("123"); + } +} From 5a28da17158ee1a32fbd71246f6182d3f6171f7f Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 11 Sep 2024 09:06:59 -0700 Subject: [PATCH 2/8] address sdk breaking changes --- .../android/SessionIdEventSenderTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java b/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java index 4d2a077f2..e58e0e262 100644 --- a/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java +++ b/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java @@ -11,11 +11,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import io.opentelemetry.android.session.Session; +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; import io.opentelemetry.api.incubator.events.EventLogger; -import io.opentelemetry.api.incubator.logs.KeyAnyValue; import io.opentelemetry.api.logs.LoggerProvider; import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.logs.internal.AnyValueBody; import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider; import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; import java.util.List; @@ -49,12 +49,12 @@ void shouldEmitSessionStartEvent() { assertThat(log) .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.start")); - AnyValueBody body = (AnyValueBody) log.getBody(); - List kvBody = (List) body.asAnyValue().getValue(); + Value> body = (Value>) log.getBodyValue(); + List kvBody = body.getValue(); assertThat(kvBody.get(0).getKey()).isEqualTo("session.previous_id"); - assertThat(kvBody.get(0).getAnyValue().asString()).isEqualTo("666"); + assertThat(kvBody.get(0).getValue().asString()).isEqualTo("666"); assertThat(kvBody.get(1).getKey()).isEqualTo("session.id"); - assertThat(kvBody.get(1).getAnyValue().asString()).isEqualTo("123"); + assertThat(kvBody.get(1).getValue().asString()).isEqualTo("123"); } @Test @@ -69,9 +69,9 @@ void shouldEmitSessionEndEvent() { assertThat(log) .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.end")); - AnyValueBody body = (AnyValueBody) log.getBody(); - List kvBody = (List) body.asAnyValue().getValue(); + Value> body = (Value>) log.getBodyValue(); + List kvBody = body.getValue(); assertThat(kvBody.get(0).getKey()).isEqualTo("session.id"); - assertThat(kvBody.get(0).getAnyValue().asString()).isEqualTo("123"); + assertThat(kvBody.get(0).getValue().asString()).isEqualTo("123"); } } From 737fea7dd2fe7a66e4d5fa96fbb449d2ee51669b Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 20 Nov 2024 16:22:15 -0800 Subject: [PATCH 3/8] spotless --- .../io/opentelemetry/android/OpenTelemetryRumBuilderTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index a62f8d724..dad2e1885 100644 --- a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java +++ b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java @@ -55,7 +55,6 @@ import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.contrib.disk.buffering.SpanToDiskExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.LogRecordExporter; import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; From 4c303827c9a415ab7ac9db568cfcf01868f7ad9c Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Thu, 21 Nov 2024 10:03:25 -0800 Subject: [PATCH 4/8] fix stack overflow and tests --- .../io/opentelemetry/android/session/SessionManager.kt | 7 ++++--- .../android/OpenTelemetryRumBuilderTest.java | 8 ++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/io/opentelemetry/android/session/SessionManager.kt b/core/src/main/java/io/opentelemetry/android/session/SessionManager.kt index 7608da1b1..6f61ae5e5 100644 --- a/core/src/main/java/io/opentelemetry/android/session/SessionManager.kt +++ b/core/src/main/java/io/opentelemetry/android/session/SessionManager.kt @@ -50,11 +50,12 @@ internal class SessionManager( // observers need to be called after bumping the timer because it may // create a new span if (newSession != session) { + val oldSession = session + session = newSession observers.forEach { - it.onSessionEnded(session) - it.onSessionStarted(newSession, session) + it.onSessionEnded(oldSession) + it.onSessionStarted(session, oldSession) } - session = newSession } return session.getId() } diff --git a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index dad2e1885..037e80008 100644 --- a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java +++ b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java @@ -175,7 +175,9 @@ public void shouldBuildLogRecordProvider() { List logs = logsExporter.getFinishedLogRecordItems(); assertThat(logs).hasSize(2); assertThat(logs.get(0)) - .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.start")); + .hasAttributesSatisfyingExactly( + equalTo(SESSION_ID, openTelemetryRum.getRumSessionId()), + equalTo(stringKey("event.name"), "session.start")); assertThat(logs.get(1)) .hasAttributesSatisfyingExactly( equalTo(SESSION_ID, openTelemetryRum.getRumSessionId()), @@ -323,7 +325,9 @@ public void setLogRecordExporterCustomizer() { assertThat(logs).hasSize(2); Iterator iter = logs.iterator(); assertThat(iter.next()) - .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.start")); + .hasAttributesSatisfyingExactly( + equalTo(SESSION_ID, rum.getRumSessionId()), + equalTo(stringKey("event.name"), "session.start")); assertThat(iter.next()) .hasBody("foo") .hasAttributesSatisfyingExactly( From 41a4add56324c6ef37b47659641382b5d4397290 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Thu, 5 Dec 2024 14:52:16 -0800 Subject: [PATCH 5/8] fixes after rebase --- .../android/OpenTelemetryRumBuilderTest.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index 037e80008..c8b32ac91 100644 --- a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java +++ b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java @@ -9,6 +9,7 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.incubating.EventIncubatingAttributes.EVENT_NAME; import static io.opentelemetry.semconv.incubating.SessionIncubatingAttributes.SESSION_ID; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.anyCollection; @@ -42,6 +43,7 @@ import io.opentelemetry.android.internal.services.applifecycle.AppLifecycleService; import io.opentelemetry.android.internal.services.applifecycle.ApplicationStateListener; import io.opentelemetry.android.internal.services.visiblescreen.VisibleScreenService; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.KeyValue; import io.opentelemetry.api.common.Value; @@ -177,6 +179,7 @@ public void shouldBuildLogRecordProvider() { assertThat(logs.get(0)) .hasAttributesSatisfyingExactly( equalTo(SESSION_ID, openTelemetryRum.getRumSessionId()), + equalTo(SCREEN_NAME_KEY, CUR_SCREEN_NAME), equalTo(stringKey("event.name"), "session.start")); assertThat(logs.get(1)) .hasAttributesSatisfyingExactly( @@ -327,6 +330,7 @@ public void setLogRecordExporterCustomizer() { assertThat(iter.next()) .hasAttributesSatisfyingExactly( equalTo(SESSION_ID, rum.getRumSessionId()), + equalTo(SCREEN_NAME_KEY, CUR_SCREEN_NAME), equalTo(stringKey("event.name"), "session.start")); assertThat(iter.next()) .hasBody("foo") @@ -424,8 +428,8 @@ public void diskBufferingDisabled() { public void verifyGlobalAttrsForLogs() { ServiceManager serviceManager = createServiceManager(); OtelRumConfig otelRumConfig = buildConfig(); - otelRumConfig.setGlobalAttributes( - () -> Attributes.of(stringKey("someGlobalKey"), "someGlobalValue")); + AttributeKey globalKey = stringKey("someGlobalKey"); + otelRumConfig.setGlobalAttributes(() -> Attributes.of(globalKey, "someGlobalValue")); OpenTelemetryRum rum = OpenTelemetryRum.builder(application, otelRumConfig) @@ -440,16 +444,19 @@ public void verifyGlobalAttrsForLogs() { logger.logRecordBuilder().setAttribute(stringKey("localAttrKey"), "localAttrValue").emit(); List recordedLogs = logRecordExporter.getFinishedLogRecordItems(); - assertThat(recordedLogs).hasSize(1); - LogRecordData logRecordData = recordedLogs.get(0); - assertThat(logRecordData) - .hasAttributes( - Attributes.builder() - .put(SESSION_ID, rum.getRumSessionId()) - .put("someGlobalKey", "someGlobalValue") - .put("localAttrKey", "localAttrValue") - .put(SCREEN_NAME_KEY, CUR_SCREEN_NAME) - .build()); + assertThat(recordedLogs).hasSize(2); // session start, the the above log + assertThat(recordedLogs.get(0)) + .hasAttributesSatisfyingExactly( + equalTo(EVENT_NAME, "session.start"), + equalTo(globalKey, "someGlobalValue"), + equalTo(SESSION_ID, rum.getRumSessionId()), + equalTo(SCREEN_NAME_KEY, CUR_SCREEN_NAME)); + assertThat(recordedLogs.get(1)) + .hasAttributesSatisfyingExactly( + equalTo(SESSION_ID, rum.getRumSessionId()), + equalTo(globalKey, "someGlobalValue"), + equalTo(stringKey("localAttrKey"), "localAttrValue"), + equalTo(SCREEN_NAME_KEY, CUR_SCREEN_NAME)); } @Test From f314669582416b4e91200c3e62c45fca109f3ec2 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Tue, 10 Dec 2024 16:46:32 -0800 Subject: [PATCH 6/8] move session instrumentation into its own instrumentation module. --- .../android/OpenTelemetryRumBuilder.java | 15 +++- .../android/SdkPreconfiguredRumBuilder.kt | 23 ++--- .../android/session/SessionManager.kt | 79 ++--------------- .../android/session/SessionManagerImpl.kt | 80 ++++++++++++++++++ .../android/OpenTelemetryRumBuilderTest.java | 37 ++++---- .../android/SessionIdEventSenderTest.java | 77 ----------------- ...nagerTest.kt => SessionManagerImplTest.kt} | 10 +-- .../ActivityLifecycleInstrumentationTest.kt | 5 +- .../instrumentation/InstallationContext.kt | 2 + .../crash/CrashReporterTest.java | 2 + instrumentation/sessions/README.md | 13 +++ instrumentation/sessions/build.gradle.kts | 37 ++++++++ instrumentation/sessions/consumer-rules.pro | 0 .../sessions}/SessionIdEventSender.kt | 6 +- .../sessions/SessionInstrumentation.kt | 25 ++++++ .../sessions/SessionIdEventSenderTest.kt | 84 +++++++++++++++++++ .../SlowRenderingInstrumentationTest.kt | 4 +- .../startup/StartupInstrumentationTest.kt | 2 + settings.gradle.kts | 1 + 19 files changed, 307 insertions(+), 195 deletions(-) create mode 100644 core/src/main/java/io/opentelemetry/android/session/SessionManagerImpl.kt delete mode 100644 core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java rename core/src/test/java/io/opentelemetry/android/session/{SessionManagerTest.kt => SessionManagerImplTest.kt} (91%) create mode 100644 instrumentation/sessions/README.md create mode 100644 instrumentation/sessions/build.gradle.kts create mode 100644 instrumentation/sessions/consumer-rules.pro rename {core/src/main/java/io/opentelemetry/android => instrumentation/sessions/src/main/kotlin/io/opentelemetry/android/instrumentation/sessions}/SessionIdEventSender.kt (87%) create mode 100644 instrumentation/sessions/src/main/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionInstrumentation.kt create mode 100644 instrumentation/sessions/src/test/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionIdEventSenderTest.kt diff --git a/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java b/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java index a01e497ec..276b7ad2a 100644 --- a/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java +++ b/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java @@ -10,6 +10,7 @@ import android.app.Application; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import io.opentelemetry.android.common.RumConstants; import io.opentelemetry.android.config.OtelRumConfig; import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration; @@ -31,6 +32,7 @@ import io.opentelemetry.android.internal.services.ServiceManagerImpl; import io.opentelemetry.android.internal.services.periodicwork.PeriodicWorkService; import io.opentelemetry.android.session.SessionManager; +import io.opentelemetry.android.session.SessionManagerImpl; import io.opentelemetry.android.session.SessionProvider; import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; @@ -95,6 +97,7 @@ public final class OpenTelemetryRumBuilder { private Resource resource; @Nullable private ServiceManager serviceManager; + @Nullable private SessionManager sessionManager; @Nullable private ExportScheduleHandler exportScheduleHandler; private static TextMapPropagator buildDefaultPropagator() { @@ -306,8 +309,10 @@ public OpenTelemetryRum build() { } initializationEvents.spanExporterInitialized(spanExporter); - SessionManager sessionManager = - SessionManager.create(timeoutHandler, config.getSessionTimeout().toNanos()); + if (sessionManager == null) { + sessionManager = + SessionManagerImpl.create(timeoutHandler, config.getSessionTimeout().toNanos()); + } OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() @@ -348,6 +353,12 @@ public OpenTelemetryRumBuilder setServiceManager(ServiceManager serviceManager) return this; } + @VisibleForTesting + OpenTelemetryRumBuilder setSessionManager(SessionManager sessionManager) { + this.sessionManager = sessionManager; + return this; + } + /** * Sets a scheduler that will take care of periodically read data stored in disk and export it. * If not specified, the default schedule exporter will be used. diff --git a/core/src/main/java/io/opentelemetry/android/SdkPreconfiguredRumBuilder.kt b/core/src/main/java/io/opentelemetry/android/SdkPreconfiguredRumBuilder.kt index 78891dfc0..b3f6b1b57 100644 --- a/core/src/main/java/io/opentelemetry/android/SdkPreconfiguredRumBuilder.kt +++ b/core/src/main/java/io/opentelemetry/android/SdkPreconfiguredRumBuilder.kt @@ -11,8 +11,8 @@ import io.opentelemetry.android.instrumentation.AndroidInstrumentationLoader import io.opentelemetry.android.instrumentation.InstallationContext import io.opentelemetry.android.internal.services.ServiceManager import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionManagerImpl import io.opentelemetry.sdk.OpenTelemetrySdk -import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider class SdkPreconfiguredRumBuilder @JvmOverloads @@ -20,7 +20,7 @@ class SdkPreconfiguredRumBuilder private val application: Application, private val sdk: OpenTelemetrySdk, private val timeoutHandler: SessionIdTimeoutHandler = SessionIdTimeoutHandler(), - private val sessionManager: SessionManager = SessionManager(timeoutHandler = timeoutHandler), + private val sessionManager: SessionManager = SessionManagerImpl(timeoutHandler = timeoutHandler), private val discoverInstrumentations: Boolean, private val serviceManager: ServiceManager, ) { @@ -52,22 +52,23 @@ class SdkPreconfiguredRumBuilder // might turn off/on additional telemetry depending on whether the app is active or not appLifecycleService.registerListener(timeoutHandler) - val eventLogger = - SdkEventLoggerProvider.create(sdk.logsBridge) - .get(OpenTelemetryRum::class.java.simpleName) - - sessionManager.addObserver(SessionIdEventSender(eventLogger)) - // After addObserver(), we call getSessionId() to trigger a session.start event - sessionManager.getSessionId() - val openTelemetryRum = OpenTelemetryRumImpl(sdk, sessionManager) // Install instrumentations - val ctx = InstallationContext(application, openTelemetryRum.openTelemetry, serviceManager) + val ctx = + InstallationContext( + application = application, + openTelemetry = openTelemetryRum.openTelemetry, + sessionManager = sessionManager, + serviceManager = serviceManager, + ) for (instrumentation in getInstrumentations()) { instrumentation.install(ctx) } + // After installing all instrumentations, we call getSessionId() to trigger the session start + sessionManager.getSessionId() + return openTelemetryRum } diff --git a/core/src/main/java/io/opentelemetry/android/session/SessionManager.kt b/core/src/main/java/io/opentelemetry/android/session/SessionManager.kt index 6f61ae5e5..c23c3c07b 100644 --- a/core/src/main/java/io/opentelemetry/android/session/SessionManager.kt +++ b/core/src/main/java/io/opentelemetry/android/session/SessionManager.kt @@ -5,76 +5,9 @@ package io.opentelemetry.android.session -import io.opentelemetry.android.SessionIdTimeoutHandler -import io.opentelemetry.sdk.common.Clock -import java.util.Collections.synchronizedList -import java.util.concurrent.TimeUnit - -internal class SessionManager( - private val clock: Clock = Clock.getDefault(), - private val sessionStorage: SessionStorage = SessionStorage.InMemory(), - private val timeoutHandler: SessionIdTimeoutHandler, - private val idGenerator: SessionIdGenerator = SessionIdGenerator.DEFAULT, - private val sessionLifetimeNanos: Long = TimeUnit.HOURS.toNanos(4), -) : SessionProvider, SessionPublisher { - // TODO: Make thread safe / wrap with AtomicReference? - private var session: Session = Session.NONE - private val observers = synchronizedList(ArrayList()) - - init { - sessionStorage.save(session) - } - - override fun addObserver(observer: SessionObserver) { - observers.add(observer) - } - - override fun getSessionId(): String { - // value will never be null - var newSession = session - - if (sessionHasExpired() || timeoutHandler.hasTimedOut()) { - val newId = idGenerator.generateSessionId() - - // TODO FIXME: This is not threadsafe -- if two threads call getSessionId() - // at the same time while timed out, two new sessions are created - // Could require SessionStorage impls to be atomic/threadsafe or - // do the locking in this class? - - newSession = Session.DefaultSession(newId, clock.now()) - sessionStorage.save(newSession) - } - - timeoutHandler.bump() - - // observers need to be called after bumping the timer because it may - // create a new span - if (newSession != session) { - val oldSession = session - session = newSession - observers.forEach { - it.onSessionEnded(oldSession) - it.onSessionStarted(session, oldSession) - } - } - return session.getId() - } - - private fun sessionHasExpired(): Boolean { - val elapsedTime = clock.now() - session.getStartTimestamp() - return elapsedTime >= sessionLifetimeNanos - } - - companion object { - @JvmStatic - fun create( - timeoutHandler: SessionIdTimeoutHandler, - sessionLifetimeNanos: Long, - ): SessionManager { - return SessionManager( - timeoutHandler = timeoutHandler, - sessionLifetimeNanos = sessionLifetimeNanos, - ) - } - } -} +/** + * The SessionManager is a public-facing tag interface that brings together + * the SessionProvider and SessionPublisher interfaces under a common + * name. + */ +interface SessionManager : SessionProvider, SessionPublisher diff --git a/core/src/main/java/io/opentelemetry/android/session/SessionManagerImpl.kt b/core/src/main/java/io/opentelemetry/android/session/SessionManagerImpl.kt new file mode 100644 index 000000000..d223b9a60 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/session/SessionManagerImpl.kt @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.session + +import io.opentelemetry.android.SessionIdTimeoutHandler +import io.opentelemetry.sdk.common.Clock +import java.util.Collections.synchronizedList +import java.util.concurrent.TimeUnit + +internal class SessionManagerImpl( + private val clock: Clock = Clock.getDefault(), + private val sessionStorage: SessionStorage = SessionStorage.InMemory(), + private val timeoutHandler: SessionIdTimeoutHandler, + private val idGenerator: SessionIdGenerator = SessionIdGenerator.DEFAULT, + private val sessionLifetimeNanos: Long = TimeUnit.HOURS.toNanos(4), +) : SessionManager { + // TODO: Make thread safe / wrap with AtomicReference? + private var session: Session = Session.NONE + private val observers = synchronizedList(ArrayList()) + + init { + sessionStorage.save(session) + } + + override fun addObserver(observer: SessionObserver) { + observers.add(observer) + } + + override fun getSessionId(): String { + // value will never be null + var newSession = session + + if (sessionHasExpired() || timeoutHandler.hasTimedOut()) { + val newId = idGenerator.generateSessionId() + + // TODO FIXME: This is not threadsafe -- if two threads call getSessionId() + // at the same time while timed out, two new sessions are created + // Could require SessionStorage impls to be atomic/threadsafe or + // do the locking in this class? + + newSession = Session.DefaultSession(newId, clock.now()) + sessionStorage.save(newSession) + } + + timeoutHandler.bump() + + // observers need to be called after bumping the timer because it may + // create a new span + if (newSession != session) { + val oldSession = session + session = newSession + observers.forEach { + it.onSessionEnded(oldSession) + it.onSessionStarted(session, oldSession) + } + } + return session.getId() + } + + private fun sessionHasExpired(): Boolean { + val elapsedTime = clock.now() - session.getStartTimestamp() + return elapsedTime >= sessionLifetimeNanos + } + + companion object { + @JvmStatic + fun create( + timeoutHandler: SessionIdTimeoutHandler, + sessionLifetimeNanos: Long, + ): SessionManagerImpl { + return SessionManagerImpl( + timeoutHandler = timeoutHandler, + sessionLifetimeNanos = sessionLifetimeNanos, + ) + } + } +} diff --git a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index c8b32ac91..f5e7c9c40 100644 --- a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java +++ b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java @@ -9,7 +9,6 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static io.opentelemetry.semconv.incubating.EventIncubatingAttributes.EVENT_NAME; import static io.opentelemetry.semconv.incubating.SessionIncubatingAttributes.SESSION_ID; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.anyCollection; @@ -43,6 +42,7 @@ import io.opentelemetry.android.internal.services.applifecycle.AppLifecycleService; import io.opentelemetry.android.internal.services.applifecycle.ApplicationStateListener; import io.opentelemetry.android.internal.services.visiblescreen.VisibleScreenService; +import io.opentelemetry.android.session.SessionManager; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.KeyValue; @@ -175,13 +175,8 @@ public void shouldBuildLogRecordProvider() { eventLogger.builder("test.event").put("body.field", "foo").setAttributes(attrs).emit(); List logs = logsExporter.getFinishedLogRecordItems(); - assertThat(logs).hasSize(2); + assertThat(logs).hasSize(1); assertThat(logs.get(0)) - .hasAttributesSatisfyingExactly( - equalTo(SESSION_ID, openTelemetryRum.getRumSessionId()), - equalTo(SCREEN_NAME_KEY, CUR_SCREEN_NAME), - equalTo(stringKey("event.name"), "session.start")); - assertThat(logs.get(1)) .hasAttributesSatisfyingExactly( equalTo(SESSION_ID, openTelemetryRum.getRumSessionId()), equalTo(stringKey("event.name"), "test.event"), @@ -189,7 +184,7 @@ public void shouldBuildLogRecordProvider() { equalTo(stringKey("mega"), "hit")) .hasResource(resource); - Value bodyValue = logs.get(1).getBodyValue(); + Value bodyValue = logs.get(0).getBodyValue(); List payload = (List) bodyValue.getValue(); assertThat(payload).hasSize(1); KeyValue expected = KeyValue.of("body.field", Value.of("foo")); @@ -199,6 +194,7 @@ public void shouldBuildLogRecordProvider() { @Test public void shouldInstallInstrumentation() { ServiceManager serviceManager = createServiceManager(); + SessionManager sessionManager = mock(SessionManager.class); SessionIdTimeoutHandler timeoutHandler = mock(); AndroidInstrumentation localInstrumentation = mock(); AndroidInstrumentation classpathInstrumentation = mock(); @@ -210,12 +206,15 @@ public void shouldInstallInstrumentation() { new OpenTelemetryRumBuilder(application, buildConfig(), timeoutHandler) .addInstrumentation(localInstrumentation) .setServiceManager(serviceManager) + .setSessionManager(sessionManager) .build(); verify(serviceManager.getAppLifecycleService()).registerListener(timeoutHandler); InstallationContext expectedCtx = - new InstallationContext(application, rum.getOpenTelemetry(), serviceManager); + new InstallationContext( + application, rum.getOpenTelemetry(), sessionManager, serviceManager); + verify(localInstrumentation).install(eq(expectedCtx)); verify(classpathInstrumentation).install(eq(expectedCtx)); } @@ -223,6 +222,7 @@ public void shouldInstallInstrumentation() { @Test public void shouldInstallInstrumentation_excludingClasspathImplsWhenRequestedInConfig() { ServiceManager serviceManager = createServiceManager(); + SessionManager sessionManager = mock(SessionManager.class); SessionIdTimeoutHandler timeoutHandler = mock(); AndroidInstrumentation localInstrumentation = mock(); AndroidInstrumentation classpathInstrumentation = mock(); @@ -237,12 +237,14 @@ public void shouldInstallInstrumentation_excludingClasspathImplsWhenRequestedInC timeoutHandler) .addInstrumentation(localInstrumentation) .setServiceManager(serviceManager) + .setSessionManager(sessionManager) .build(); verify(serviceManager.getAppLifecycleService()).registerListener(timeoutHandler); InstallationContext expectedCtx = - new InstallationContext(application, rum.getOpenTelemetry(), serviceManager); + new InstallationContext( + application, rum.getOpenTelemetry(), sessionManager, serviceManager); verify(localInstrumentation).install(eq(expectedCtx)); verifyNoInteractions(classpathInstrumentation); } @@ -325,13 +327,8 @@ public void setLogRecordExporterCustomizer() { () -> assertThat(logsExporter.getFinishedLogRecordItems()).isNotEmpty()); assertThat(wasCalled.get()).isTrue(); Collection logs = logsExporter.getFinishedLogRecordItems(); - assertThat(logs).hasSize(2); + assertThat(logs).hasSize(1); Iterator iter = logs.iterator(); - assertThat(iter.next()) - .hasAttributesSatisfyingExactly( - equalTo(SESSION_ID, rum.getRumSessionId()), - equalTo(SCREEN_NAME_KEY, CUR_SCREEN_NAME), - equalTo(stringKey("event.name"), "session.start")); assertThat(iter.next()) .hasBody("foo") .hasAttributesSatisfyingExactly( @@ -444,14 +441,8 @@ public void verifyGlobalAttrsForLogs() { logger.logRecordBuilder().setAttribute(stringKey("localAttrKey"), "localAttrValue").emit(); List recordedLogs = logRecordExporter.getFinishedLogRecordItems(); - assertThat(recordedLogs).hasSize(2); // session start, the the above log + assertThat(recordedLogs).hasSize(1); // session start, the the above log assertThat(recordedLogs.get(0)) - .hasAttributesSatisfyingExactly( - equalTo(EVENT_NAME, "session.start"), - equalTo(globalKey, "someGlobalValue"), - equalTo(SESSION_ID, rum.getRumSessionId()), - equalTo(SCREEN_NAME_KEY, CUR_SCREEN_NAME)); - assertThat(recordedLogs.get(1)) .hasAttributesSatisfyingExactly( equalTo(SESSION_ID, rum.getRumSessionId()), equalTo(globalKey, "someGlobalValue"), diff --git a/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java b/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java deleted file mode 100644 index e58e0e262..000000000 --- a/core/src/test/java/io/opentelemetry/android/SessionIdEventSenderTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.android; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.opentelemetry.android.session.Session; -import io.opentelemetry.api.common.KeyValue; -import io.opentelemetry.api.common.Value; -import io.opentelemetry.api.incubator.events.EventLogger; -import io.opentelemetry.api.logs.LoggerProvider; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider; -import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -class SessionIdEventSenderTest { - @RegisterExtension - static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); - - private SessionIdEventSender underTest; - - @BeforeEach - void setup() { - LoggerProvider loggerProvider = otelTesting.getOpenTelemetry().getLogsBridge(); - EventLogger eventLogger = SdkEventLoggerProvider.create(loggerProvider).get("testLogging"); - underTest = new SessionIdEventSender(eventLogger); - } - - @Test - void shouldEmitSessionStartEvent() { - Session newSession = new Session.DefaultSession("123", 0); - Session oldSession = new Session.DefaultSession("666", 0); - underTest.onSessionStarted(newSession, oldSession); - - List logs = otelTesting.getLogRecords(); - assertEquals(1, logs.size()); - LogRecordData log = logs.get(0); - // TODO: Use new event body assertions when available. - assertThat(log) - .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.start")); - - Value> body = (Value>) log.getBodyValue(); - List kvBody = body.getValue(); - assertThat(kvBody.get(0).getKey()).isEqualTo("session.previous_id"); - assertThat(kvBody.get(0).getValue().asString()).isEqualTo("666"); - assertThat(kvBody.get(1).getKey()).isEqualTo("session.id"); - assertThat(kvBody.get(1).getValue().asString()).isEqualTo("123"); - } - - @Test - void shouldEmitSessionEndEvent() { - Session session = new Session.DefaultSession("123", 0); - underTest.onSessionEnded(session); - - List logs = otelTesting.getLogRecords(); - assertEquals(1, logs.size()); - LogRecordData log = logs.get(0); - // TODO: Use new event body assertions when available. - assertThat(log) - .hasAttributesSatisfyingExactly(equalTo(stringKey("event.name"), "session.end")); - - Value> body = (Value>) log.getBodyValue(); - List kvBody = body.getValue(); - assertThat(kvBody.get(0).getKey()).isEqualTo("session.id"); - assertThat(kvBody.get(0).getValue().asString()).isEqualTo("123"); - } -} diff --git a/core/src/test/java/io/opentelemetry/android/session/SessionManagerTest.kt b/core/src/test/java/io/opentelemetry/android/session/SessionManagerImplTest.kt similarity index 91% rename from core/src/test/java/io/opentelemetry/android/session/SessionManagerTest.kt rename to core/src/test/java/io/opentelemetry/android/session/SessionManagerImplTest.kt index fa28c9c06..8dd860104 100644 --- a/core/src/test/java/io/opentelemetry/android/session/SessionManagerTest.kt +++ b/core/src/test/java/io/opentelemetry/android/session/SessionManagerImplTest.kt @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test import java.util.concurrent.TimeUnit import java.util.regex.Pattern -internal class SessionManagerTest { +internal class SessionManagerImplTest { @MockK lateinit var timeoutHandler: SessionIdTimeoutHandler @@ -35,7 +35,7 @@ internal class SessionManagerTest { @Test fun valueValid() { - val sessionManager = SessionManager(TestClock.create(), timeoutHandler = timeoutHandler) + val sessionManager = SessionManagerImpl(TestClock.create(), timeoutHandler = timeoutHandler) val sessionId = sessionManager.getSessionId() assertThat(sessionId).isNotNull() assertThat(sessionId).hasSize(32) @@ -45,7 +45,7 @@ internal class SessionManagerTest { @Test fun valueSameUntil4Hours() { val clock = TestClock.create() - val sessionManager = SessionManager(clock, timeoutHandler = timeoutHandler) + val sessionManager = SessionManagerImpl(clock, timeoutHandler = timeoutHandler) val value = sessionManager.getSessionId() assertThat(value).isEqualTo(sessionManager.getSessionId()) clock.advance(3, TimeUnit.HOURS) @@ -69,7 +69,7 @@ internal class SessionManagerTest { every { observer.onSessionStarted(any(), any()) } just Runs every { observer.onSessionEnded(any()) } just Runs - val sessionManager = SessionManager(clock, timeoutHandler = timeoutHandler) + val sessionManager = SessionManagerImpl(clock, timeoutHandler = timeoutHandler) sessionManager.addObserver(observer) // The first call expires the Session.NONE initial session and notifies @@ -108,7 +108,7 @@ internal class SessionManagerTest { @Test fun shouldCreateNewSessionIdAfterTimeout() { - val sessionId = SessionManager(timeoutHandler = timeoutHandler) + val sessionId = SessionManagerImpl(timeoutHandler = timeoutHandler) val value = sessionId.getSessionId() verify { timeoutHandler.bump() } diff --git a/instrumentation/activity/src/test/java/io/opentelemetry/android/instrumentation/activity/ActivityLifecycleInstrumentationTest.kt b/instrumentation/activity/src/test/java/io/opentelemetry/android/instrumentation/activity/ActivityLifecycleInstrumentationTest.kt index 20e7c195f..f47fd1e94 100644 --- a/instrumentation/activity/src/test/java/io/opentelemetry/android/instrumentation/activity/ActivityLifecycleInstrumentationTest.kt +++ b/instrumentation/activity/src/test/java/io/opentelemetry/android/instrumentation/activity/ActivityLifecycleInstrumentationTest.kt @@ -14,6 +14,7 @@ import io.opentelemetry.android.common.RumConstants import io.opentelemetry.android.instrumentation.InstallationContext import io.opentelemetry.android.internal.services.ServiceManager import io.opentelemetry.android.internal.services.visiblescreen.VisibleScreenService +import io.opentelemetry.android.session.SessionManager import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.SpanBuilder import io.opentelemetry.api.trace.Tracer @@ -29,6 +30,7 @@ class ActivityLifecycleInstrumentationTest { private lateinit var application: Application private lateinit var openTelemetry: OpenTelemetrySdk private lateinit var serviceManager: ServiceManager + private lateinit var sessionManager: SessionManager @Before fun setUp() { @@ -36,6 +38,7 @@ class ActivityLifecycleInstrumentationTest { openTelemetry = mockk() activityLifecycleInstrumentation = ActivityLifecycleInstrumentation() serviceManager = mockk() + sessionManager = mockk() every { serviceManager.getVisibleScreenService() }.returns(mockk()) } @@ -53,7 +56,7 @@ class ActivityLifecycleInstrumentationTest { ) every { startupSpanBuilder.startSpan() }.returns(startupSpan) - val ctx = InstallationContext(application, openTelemetry, serviceManager) + val ctx = InstallationContext(application, openTelemetry, sessionManager, serviceManager) activityLifecycleInstrumentation.install(ctx) verify { diff --git a/instrumentation/android-instrumentation/src/main/java/io/opentelemetry/android/instrumentation/InstallationContext.kt b/instrumentation/android-instrumentation/src/main/java/io/opentelemetry/android/instrumentation/InstallationContext.kt index 826a24a42..3bd7093aa 100644 --- a/instrumentation/android-instrumentation/src/main/java/io/opentelemetry/android/instrumentation/InstallationContext.kt +++ b/instrumentation/android-instrumentation/src/main/java/io/opentelemetry/android/instrumentation/InstallationContext.kt @@ -7,10 +7,12 @@ package io.opentelemetry.android.instrumentation import android.app.Application import io.opentelemetry.android.internal.services.ServiceManager +import io.opentelemetry.android.session.SessionManager import io.opentelemetry.api.OpenTelemetry data class InstallationContext( val application: Application, val openTelemetry: OpenTelemetry, + val sessionManager: SessionManager, val serviceManager: ServiceManager, ) diff --git a/instrumentation/crash/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReporterTest.java b/instrumentation/crash/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReporterTest.java index da6de01d3..21ff4604b 100644 --- a/instrumentation/crash/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReporterTest.java +++ b/instrumentation/crash/src/test/java/io/opentelemetry/android/instrumentation/crash/CrashReporterTest.java @@ -13,6 +13,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import io.opentelemetry.android.instrumentation.InstallationContext; import io.opentelemetry.android.internal.services.ServiceManager; +import io.opentelemetry.android.session.SessionManager; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.logs.SdkLoggerProvider; @@ -64,6 +65,7 @@ public void integrationTest() throws InterruptedException { new InstallationContext( RuntimeEnvironment.getApplication(), openTelemetrySdk, + mock(SessionManager.class), mock(ServiceManager.class)); instrumentation.install(ctx); diff --git a/instrumentation/sessions/README.md b/instrumentation/sessions/README.md new file mode 100644 index 000000000..ee4993b39 --- /dev/null +++ b/instrumentation/sessions/README.md @@ -0,0 +1,13 @@ + +# Session Instrumentation + +Out of the box, the `opentelemetry-android` agent will create and manage sessions. Sessions are associated +with a unique identifier and will expire after a duration. This instrumentation is responsible +for adding a `SessionObserver` instance that will generate OpenTelemetry events for +`session.start` and `session.end` when interesting things happen to the session. + +See [session.md in the semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/session.md) +for additional details about sessions. + +See ["Session Events"](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/session.md#session-events) +for more information about the specific events generated by this instrumentation. diff --git a/instrumentation/sessions/build.gradle.kts b/instrumentation/sessions/build.gradle.kts new file mode 100644 index 000000000..96df01c10 --- /dev/null +++ b/instrumentation/sessions/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("otel.android-library-conventions") + id("otel.publish-conventions") +} + +description = "OpenTelemetry Android session instrumentation" + +android { + namespace = "io.opentelemetry.android.instrumentation.sessions" + + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + } + + testOptions { + unitTests.isReturnDefaultValues = true + unitTests.isIncludeAndroidResources = true + } +} + +dependencies { + api(platform(libs.opentelemetry.platform.alpha)) + implementation(libs.opentelemetry.api.incubator) + implementation(libs.opentelemetry.sdk) + implementation(libs.opentelemetry.semconv.incubating) + api(project(":instrumentation:common-api")) + api(project(":core")) + +// api(libs.opentelemetry.api) +// api(project(":core")) +// implementation(libs.androidx.core) +// implementation(libs.opentelemetry.semconv.incubating) +// implementation(libs.opentelemetry.sdk) +// implementation(libs.opentelemetry.instrumentation.api) +// testImplementation(libs.robolectric) +// testImplementation(libs.androidx.test.core) +} diff --git a/instrumentation/sessions/consumer-rules.pro b/instrumentation/sessions/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/src/main/java/io/opentelemetry/android/SessionIdEventSender.kt b/instrumentation/sessions/src/main/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionIdEventSender.kt similarity index 87% rename from core/src/main/java/io/opentelemetry/android/SessionIdEventSender.kt rename to instrumentation/sessions/src/main/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionIdEventSender.kt index 45a67cad6..30190791d 100644 --- a/core/src/main/java/io/opentelemetry/android/SessionIdEventSender.kt +++ b/instrumentation/sessions/src/main/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionIdEventSender.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.android +package io.opentelemetry.android.instrumentation.sessions import io.opentelemetry.android.common.RumConstants.Events.EVENT_SESSION_END import io.opentelemetry.android.common.RumConstants.Events.EVENT_SESSION_START @@ -13,6 +13,10 @@ import io.opentelemetry.api.incubator.events.EventLogger import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes.SESSION_ID import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes.SESSION_PREVIOUS_ID +/** + * This class is responsible for generating the session related events as + * specified in the OpenTelemetry semantic conventions. + */ internal class SessionIdEventSender(private val eventLogger: EventLogger) : SessionObserver { override fun onSessionStarted( newSession: Session, diff --git a/instrumentation/sessions/src/main/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionInstrumentation.kt b/instrumentation/sessions/src/main/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionInstrumentation.kt new file mode 100644 index 000000000..e687d5e59 --- /dev/null +++ b/instrumentation/sessions/src/main/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionInstrumentation.kt @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation.sessions + +import io.opentelemetry.android.OpenTelemetryRum +import io.opentelemetry.android.instrumentation.AndroidInstrumentation +import io.opentelemetry.android.instrumentation.InstallationContext +import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider + +/** + * This instrumentation will register an observer with the SessionManager, + * and this observer will send start/end events when the session id changes. + */ +class SessionInstrumentation : AndroidInstrumentation { + override fun install(ctx: InstallationContext) { + val eventLogger = + SdkEventLoggerProvider.create(ctx.openTelemetry.logsBridge) + .get(OpenTelemetryRum::class.java.simpleName) + + ctx.sessionManager.addObserver(SessionIdEventSender(eventLogger)) + } +} diff --git a/instrumentation/sessions/src/test/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionIdEventSenderTest.kt b/instrumentation/sessions/src/test/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionIdEventSenderTest.kt new file mode 100644 index 000000000..31b053ab3 --- /dev/null +++ b/instrumentation/sessions/src/test/kotlin/io/opentelemetry/android/instrumentation/sessions/SessionIdEventSenderTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.instrumentation.sessions + +import io.opentelemetry.android.session.Session +import io.opentelemetry.android.session.Session.DefaultSession +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.common.KeyValue +import io.opentelemetry.api.common.Value +import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class SessionIdEventSenderTest { + private var underTest: SessionIdEventSender? = null + + @BeforeEach + fun setup() { + val loggerProvider = otelTesting.openTelemetry.logsBridge + val eventLogger = SdkEventLoggerProvider.create(loggerProvider)["testLogging"] + underTest = SessionIdEventSender(eventLogger) + } + + @Test + fun `should emit session start event`() { + val newSession: Session = DefaultSession("123", 0) + val oldSession: Session = DefaultSession("666", 0) + underTest!!.onSessionStarted(newSession, oldSession) + + val logs = otelTesting.logRecords + assertEquals(1, logs.size) + val log = logs[0] + + // TODO: Use new event body assertions when available. + assertThat(log) + .hasAttributesSatisfyingExactly(OpenTelemetryAssertions.equalTo(AttributeKey.stringKey("event.name"), "session.start")) + + val body = log.bodyValue as Value>? + val kvBody = body!!.value + assertThat(kvBody[0].key).isEqualTo("session.previous_id") + assertThat(kvBody[0].value.asString()).isEqualTo("666") + assertThat(kvBody[1].key).isEqualTo("session.id") + assertThat(kvBody[1].value.asString()).isEqualTo("123") + } + + @Test + fun `should emit session end event`() { + val session: Session = DefaultSession("123", 0) + underTest!!.onSessionEnded(session) + + val logs = otelTesting.logRecords + assertEquals(1, logs.size) + val log = logs[0] + + // TODO: Use new event body assertions when available. + assertThat(log) + .hasAttributesSatisfyingExactly( + OpenTelemetryAssertions.equalTo( + AttributeKey.stringKey("event.name"), + "session.end", + ), + ) + + val body = log.bodyValue as Value>? + val kvBody = body!!.value + assertThat(kvBody[0].key).isEqualTo("session.id") + assertThat(kvBody[0].value.asString()).isEqualTo("123") + } + + companion object { + @RegisterExtension + @JvmField + var otelTesting: OpenTelemetryExtension = OpenTelemetryExtension.create() + } +} diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentationTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentationTest.kt index 973b3880a..6570ccf72 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentationTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentationTest.kt @@ -64,7 +64,7 @@ class SlowRenderingInstrumentationTest { @Config(sdk = [23]) @Test fun `Not installing instrumentation on devices with API level lower than 24`() { - val ctx = InstallationContext(application, openTelemetry, mockk()) + val ctx = InstallationContext(application, openTelemetry, mockk(), mockk()) slowRenderingInstrumentation.install(ctx) verify { @@ -81,7 +81,7 @@ class SlowRenderingInstrumentationTest { val capturedListener = slot() every { openTelemetry.getTracer(any()) }.returns(mockk()) every { application.registerActivityLifecycleCallbacks(any()) } just Runs - val ctx = InstallationContext(application, openTelemetry, mockk()) + val ctx = InstallationContext(application, openTelemetry, mockk(), mockk()) slowRenderingInstrumentation.install(ctx) verify { openTelemetry.getTracer("io.opentelemetry.slow-rendering") } diff --git a/instrumentation/startup/src/test/java/io/opentelemetry/android/instrumentation/startup/StartupInstrumentationTest.kt b/instrumentation/startup/src/test/java/io/opentelemetry/android/instrumentation/startup/StartupInstrumentationTest.kt index 98f0eb6f5..467cfb268 100644 --- a/instrumentation/startup/src/test/java/io/opentelemetry/android/instrumentation/startup/StartupInstrumentationTest.kt +++ b/instrumentation/startup/src/test/java/io/opentelemetry/android/instrumentation/startup/StartupInstrumentationTest.kt @@ -15,6 +15,7 @@ import io.mockk.verify import io.opentelemetry.android.instrumentation.InstallationContext import io.opentelemetry.android.internal.initialization.InitializationEvents import io.opentelemetry.android.internal.services.ServiceManager +import io.opentelemetry.android.session.SessionManager import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -64,6 +65,7 @@ class StartupInstrumentationTest { return InstallationContext( mockk(), otelTesting.openTelemetry, + mockk(), mockk(), ) } diff --git a/settings.gradle.kts b/settings.gradle.kts index b095e855c..94723c74f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ include(":instrumentation:okhttp:okhttp-3.0:agent") include(":instrumentation:okhttp:okhttp-3.0:library") include(":instrumentation:okhttp:okhttp-3.0:testing") include(":instrumentation:network") +include(":instrumentation:sessions") include(":instrumentation:slowrendering") include(":instrumentation:startup") include(":instrumentation:volley:library") From 5d937d8398eec51b9eaac3241b56c2cbde4b6250 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Tue, 10 Dec 2024 16:57:46 -0800 Subject: [PATCH 7/8] cleanup old copypasta --- instrumentation/sessions/build.gradle.kts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/instrumentation/sessions/build.gradle.kts b/instrumentation/sessions/build.gradle.kts index 96df01c10..a3977773d 100644 --- a/instrumentation/sessions/build.gradle.kts +++ b/instrumentation/sessions/build.gradle.kts @@ -25,13 +25,4 @@ dependencies { implementation(libs.opentelemetry.semconv.incubating) api(project(":instrumentation:common-api")) api(project(":core")) - -// api(libs.opentelemetry.api) -// api(project(":core")) -// implementation(libs.androidx.core) -// implementation(libs.opentelemetry.semconv.incubating) -// implementation(libs.opentelemetry.sdk) -// implementation(libs.opentelemetry.instrumentation.api) -// testImplementation(libs.robolectric) -// testImplementation(libs.androidx.test.core) } From 54768fc5b4295b6594fb34b29a43d2a17a7a9d83 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Tue, 10 Dec 2024 17:37:32 -0800 Subject: [PATCH 8/8] wire session instrumentation up in demo app --- demo-app/build.gradle.kts | 1 + demo-app/settings.gradle.kts | 9 ++++++++- .../io/opentelemetry/android/demo/OtelDemoApplication.kt | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/demo-app/build.gradle.kts b/demo-app/build.gradle.kts index a7246496d..e89984a6e 100644 --- a/demo-app/build.gradle.kts +++ b/demo-app/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { coreLibraryDesugaring(libs.desugarJdkLibs) implementation("io.opentelemetry.android:android-agent") //parent dir + implementation("io.opentelemetry.android:instrumentation-sessions") implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/demo-app/settings.gradle.kts b/demo-app/settings.gradle.kts index 3f3fc66ab..c3244f09b 100644 --- a/demo-app/settings.gradle.kts +++ b/demo-app/settings.gradle.kts @@ -19,4 +19,11 @@ dependencyResolutionManagement { google() } } -includeBuild("..") \ No newline at end of file +includeBuild("..") { + dependencySubstitution { + substitute(module("io.opentelemetry.android:android-agent")) + .using(project(":android-agent")) + substitute(module("io.opentelemetry.android:instrumentation-sessions")) + .using(project(":instrumentation:sessions")) + } +} \ No newline at end of file diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt index 1dadceeb5..c02c5cd73 100644 --- a/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/OtelDemoApplication.kt @@ -12,6 +12,7 @@ import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.OpenTelemetryRumBuilder import io.opentelemetry.android.config.OtelRumConfig import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration +import io.opentelemetry.android.instrumentation.sessions.SessionInstrumentation import io.opentelemetry.api.common.AttributeKey.stringKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.incubator.events.EventBuilder @@ -55,6 +56,8 @@ class OtelDemoApplication : Application() { .setEndpoint(logsIngestUrl) .build() } + // TODO: This should NOT be necessary if it's in the runtime classpath... + .addInstrumentation(SessionInstrumentation()) try { rum = otelRumBuilder.build() Log.d(TAG, "RUM session started: " + rum!!.rumSessionId)