Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new session instrumentation #719

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ android {

dependencies {
implementation(project(":instrumentation:android-instrumentation"))
implementation(project(":services"))
implementation(project(":common"))
implementation(project(":services"))
implementation(project(":session"))

implementation(libs.androidx.core)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,8 @@
import io.opentelemetry.android.internal.services.ServiceManager;
import io.opentelemetry.android.internal.services.ServiceManagerImpl;
import io.opentelemetry.android.internal.services.periodicwork.PeriodicWorkService;
import io.opentelemetry.android.internal.session.SessionIdTimeoutHandler;
import io.opentelemetry.android.internal.session.SessionManagerImpl;
import io.opentelemetry.android.session.SessionManager;
import io.opentelemetry.android.session.SessionProvider;
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
Expand Down Expand Up @@ -96,6 +99,7 @@ public final class OpenTelemetryRumBuilder {

@Nullable private ServiceManager serviceManager;
@Nullable private ExportScheduleHandler exportScheduleHandler;
@Nullable private SessionManager sessionManager;

private static TextMapPropagator buildDefaultPropagator() {
return TextMapPropagator.composite(
Expand Down Expand Up @@ -306,8 +310,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()
Expand Down Expand Up @@ -348,6 +354,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import io.opentelemetry.android.instrumentation.AndroidInstrumentation
import io.opentelemetry.android.instrumentation.AndroidInstrumentationLoader
import io.opentelemetry.android.instrumentation.InstallationContext
import io.opentelemetry.android.internal.services.ServiceManager
import io.opentelemetry.android.internal.session.SessionIdTimeoutHandler
import io.opentelemetry.android.internal.session.SessionManagerImpl
import io.opentelemetry.android.session.SessionManager
import io.opentelemetry.sdk.OpenTelemetrySdk

Expand All @@ -19,7 +21,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,
) {
Expand Down Expand Up @@ -54,7 +56,7 @@ class SdkPreconfiguredRumBuilder
val openTelemetryRum = OpenTelemetryRumImpl(sdk, sessionManager)

// Install instrumentations
val ctx = InstallationContext(application, openTelemetryRum.openTelemetry, serviceManager)
val ctx = InstallationContext(application, openTelemetryRum.openTelemetry, sessionManager, serviceManager)
for (instrumentation in getInstrumentations()) {
instrumentation.install(ctx)
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.internal.session

import io.opentelemetry.android.internal.services.applifecycle.ApplicationStateListener
import io.opentelemetry.sdk.common.Clock
import java.time.Duration

/**
* This class encapsulates the following criteria about the sessionId timeout:
*
*
* * If the app is in the foreground sessionId should never time out.
* * If the app is in the background and no activity (spans) happens for >15 minutes, sessionId
* should time out.
* * If the app is in the background and some activity (spans) happens in <15 minute intervals,
* sessionId should not time out.
*
*
* 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.
*/
internal class SessionIdTimeoutHandler(
private val clock: Clock,
private val sessionTimeout: Duration,
) :
ApplicationStateListener {
@Volatile
private var timeoutStartNanos: Long = 0

@Volatile
private var state = State.FOREGROUND

// for testing
@JvmOverloads
internal constructor(sessionTimeout: Duration = DEFAULT_SESSION_TIMEOUT) : this(
Clock.getDefault(),
sessionTimeout,
)

override fun onApplicationForegrounded() {
state = State.TRANSITIONING_TO_FOREGROUND
}

override fun onApplicationBackgrounded() {
state = State.BACKGROUND
}

fun hasTimedOut(): Boolean {
// don't apply sessionId timeout to apps in the foreground
if (state == State.FOREGROUND) {
return false
}
val elapsedTime = clock.nanoTime() - timeoutStartNanos
return elapsedTime >= sessionTimeout.toNanos()
}

fun bump() {
timeoutStartNanos = clock.nanoTime()

// move from the temporary transition state to foreground after the first span
if (state == State.TRANSITIONING_TO_FOREGROUND) {
state = State.FOREGROUND
}
}

private enum class State {
FOREGROUND,
BACKGROUND,

/** A temporary state representing the first event after the app has been brought back. */
TRANSITIONING_TO_FOREGROUND,
}

companion object {
@JvmField
val DEFAULT_SESSION_TIMEOUT: Duration = Duration.ofMinutes(15)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.session
package io.opentelemetry.android.internal.session

import io.opentelemetry.android.SessionIdTimeoutHandler
import io.opentelemetry.android.session.Session
import io.opentelemetry.android.session.SessionIdGenerator
import io.opentelemetry.android.session.SessionManager
import io.opentelemetry.android.session.SessionObserver
import io.opentelemetry.android.session.SessionStorage
import io.opentelemetry.sdk.common.Clock
import java.util.Collections.synchronizedList
import java.util.concurrent.TimeUnit

internal class SessionManager(
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),
) : SessionProvider, SessionPublisher {
) : SessionManager {
// TODO: Make thread safe / wrap with AtomicReference?
private var session: Session = Session.NONE
private val observers = synchronizedList(ArrayList<SessionObserver>())
Expand Down Expand Up @@ -50,11 +54,12 @@ internal class SessionManager(
// observers need to be called after bumping the timer because it may
// create a new span
if (newSession != session) {
val previousSession = session
session = newSession
observers.forEach {
it.onSessionEnded(session)
it.onSessionStarted(newSession, session)
it.onSessionEnded(previousSession)
it.onSessionStarted(session, previousSession)
}
session = newSession
}
return session.getId()
}
Expand All @@ -69,8 +74,8 @@ internal class SessionManager(
fun create(
timeoutHandler: SessionIdTimeoutHandler,
sessionLifetimeNanos: Long,
): SessionManager {
return SessionManager(
): SessionManagerImpl {
return SessionManagerImpl(
timeoutHandler = timeoutHandler,
sessionLifetimeNanos = sessionLifetimeNanos,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
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.internal.session.SessionIdTimeoutHandler;
import io.opentelemetry.android.session.SessionManager;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.KeyValue;
import io.opentelemetry.api.common.Value;
Expand Down Expand Up @@ -192,6 +194,7 @@ public void shouldBuildLogRecordProvider() {
@Test
public void shouldInstallInstrumentation() {
ServiceManager serviceManager = createServiceManager();
SessionManager sessionManager = mock();
SessionIdTimeoutHandler timeoutHandler = mock();
AndroidInstrumentation localInstrumentation = mock();
AndroidInstrumentation classpathInstrumentation = mock();
Expand All @@ -203,19 +206,22 @@ 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));
}

@Test
public void shouldInstallInstrumentation_excludingClasspathImplsWhenRequestedInConfig() {
ServiceManager serviceManager = createServiceManager();
SessionManager sessionManager = mock();
SessionIdTimeoutHandler timeoutHandler = mock();
AndroidInstrumentation localInstrumentation = mock();
AndroidInstrumentation classpathInstrumentation = mock();
Expand All @@ -230,12 +236,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);
}
Expand Down
Loading
Loading