Skip to content
Closed
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
11 changes: 9 additions & 2 deletions e2e/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ plugins {

android {
namespace = "com.example.androidobservability"
compileSdk = 35
compileSdk = 36

defaultConfig {
applicationId = "com.example.androidobservability"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"

Expand Down Expand Up @@ -40,6 +39,14 @@ android {
}
}

configurations.all {
// Needed to exclude okhttp-jvm dependency from io.opentelemetry:opentelemetry-exporter-otlp that collided with
// okhttp dependency from com.launchdarkly:launchdarkly-android-client-sdk. Next steps would be to update
// com.launchdarkly:launchdarkly-android-client-sdk to use okhttp 5+ as otel libraries are using 5+ at the
// time of writing this.
exclude(group = "com.squareup.okhttp3", module = "okhttp-jvm")
}

dependencies {
// Uncomment to use the local project
implementation(project(":observability-android"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import com.example.androidobservability.ui.theme.AndroidObservabilityTheme

Expand All @@ -27,52 +29,73 @@ class MainActivity : ComponentActivity() {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column {
Text(
text = "Hello Telemetry",
text = "Hello Android Observability",
modifier = Modifier.padding(innerPadding)
)
Button(
modifier = Modifier.semantics {
contentDescription = "buttonGoToSecondActivity"
},
onClick = {
[email protected](Intent(this@MainActivity, SecondaryActivity::class.java))
}
) {
Text("Go to Secondary Activity")
}
Button(
modifier = Modifier.semantics {
contentDescription = "buttonTriggerHttp"
},
onClick = {
viewModel.triggerHttpRequests()
}
) {
Text("Trigger HTTP Request")
}
Button(
modifier = Modifier.semantics {
contentDescription = "buttonTriggerMetric"
},
onClick = {
viewModel.triggerMetric()
}
) {
Text("Trigger Metric")
}
Button(
modifier = Modifier.semantics {
contentDescription = "buttonTriggerError"
},
onClick = {
viewModel.triggerError()
}
) {
Text("Trigger Error")
}
Button(
modifier = Modifier.semantics {
contentDescription = "buttonTriggerLog"
},
onClick = {
viewModel.triggerLog()
}
) {
Text("Trigger Log")
}
Button(
modifier = Modifier.semantics {
contentDescription = "buttonTriggerNestedSpans"
},
onClick = {
viewModel.triggerNestedSpans()
}
) {
Text("Trigger Nested Spans")
}
Button(
modifier = Modifier.semantics {
contentDescription = "buttonTriggerCrash"
},
onClick = {
viewModel.triggerCrash()
}
Expand Down
2 changes: 1 addition & 1 deletion e2e/android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
agp = "8.9.2"
kotlin = "2.0.21"
kotlin = "2.2.10"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
Expand Down
22 changes: 17 additions & 5 deletions sdk/@launchdarkly/observability-android/lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ allprojects {
}
}

configurations.all {
// Needed to exclude okhttp-jvm dependency from io.opentelemetry:opentelemetry-exporter-otlp that collided with
// okhttp dependency from com.launchdarkly:launchdarkly-android-client-sdk. Next steps would be to update
// com.launchdarkly:launchdarkly-android-client-sdk to use okhttp 5+ as otel libraries are using 5+ at the
// time of writing this.
exclude(group = "com.squareup.okhttp3", module = "okhttp-jvm")
}

dependencies {
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.9.0")
implementation("com.jakewharton.timber:timber:5.0.1")
Expand All @@ -31,12 +39,16 @@ dependencies {
implementation("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha")

// Android instrumentation
implementation("io.opentelemetry.android:core:0.11.0-alpha")
implementation("io.opentelemetry.android.instrumentation:activity:0.11.0-alpha")
implementation("io.opentelemetry.android:session:0.11.0-alpha")
implementation("io.opentelemetry.android:core:0.14.0-alpha")
implementation("io.opentelemetry.android.instrumentation:activity:0.14.0-alpha")
implementation("io.opentelemetry.android:session:0.14.0-alpha")
implementation("io.opentelemetry.android:android-agent:0.14.0-alpha")

// Android crash instrumentation
implementation("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha")
implementation("io.opentelemetry.android.instrumentation:crash:0.14.0-alpha")

// Android click instrumentation for Compose
implementation("io.opentelemetry.android.instrumentation:compose-click:0.14.0-alpha")

// Use JUnit Jupiter for testing.
testImplementation("org.junit.jupiter:junit-jupiter")
Expand All @@ -47,7 +59,7 @@ val releaseVersion = version.toString()

android {
namespace = "com.launchdarkly.observability"
compileSdk = 30
compileSdk = 36

buildFeatures {
buildConfig = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import android.app.Application
import com.launchdarkly.logging.LDLogger
import com.launchdarkly.observability.api.Options
import com.launchdarkly.observability.interfaces.Metric
import com.launchdarkly.observability.vendored.otel.SessionIdTimeoutHandler
import com.launchdarkly.observability.vendored.otel.SessionManager
import io.opentelemetry.android.OpenTelemetryRum
import io.opentelemetry.android.agent.session.SessionConfig
import io.opentelemetry.android.config.OtelRumConfig
import io.opentelemetry.android.session.SessionConfig
import io.opentelemetry.android.internal.services.Services
import io.opentelemetry.android.session.SessionProvider
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.logs.Logger
import io.opentelemetry.api.logs.Severity
Expand Down Expand Up @@ -47,10 +51,9 @@ class InstrumentationManager(
private var otelTracer: Tracer

init {
val otelRumConfig = OtelRumConfig().setSessionConfig(
SessionConfig(backgroundInactivityTimeout = options.sessionBackgroundTimeout)
)
val otelRumConfig = OtelRumConfig() // default
otelRUM = OpenTelemetryRum.builder(application, otelRumConfig)
.setSessionProvider(createSessionProvider(application, SessionConfig(backgroundInactivityTimeout = options.sessionBackgroundTimeout)))
.addLoggerProviderCustomizer { sdkLoggerProviderBuilder, application ->
val logExporter = OtlpHttpLogRecordExporter.builder()
.setEndpoint(options.otlpEndpoint + LOGS_PATH)
Expand Down Expand Up @@ -192,4 +195,13 @@ class InstrumentationManager(
.setAllAttributes(attributes)
.startSpan()
}

private fun createSessionProvider(
application: Application,
sessionConfig: SessionConfig,
): SessionProvider {
val timeoutHandler = SessionIdTimeoutHandler(sessionConfig)
Services.get(application).appLifecycle.registerListener(timeoutHandler)
return SessionManager.create(timeoutHandler, sessionConfig)
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unmodified vendored file

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Originally from https://github.com/open-telemetry/opentelemetry-android/blob/main/android-agent/src/main/kotlin/io/opentelemetry/android/agent/session/SessionManager.kt
*
* Was publicly available before 0.14.0-alpha and this implementation meets our needs. We will come back
* to check for any updates before this is released in a 1.X version of our plugin. O11Y-443 tracks this task.
*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package com.launchdarkly.observability.vendored.otel

import io.opentelemetry.android.agent.session.SessionConfig
import io.opentelemetry.android.internal.services.applifecycle.ApplicationStateListener
import io.opentelemetry.sdk.common.Clock
import kotlin.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 sessionBackgroundInactivityTimeout: Duration,
) : ApplicationStateListener {
@Volatile
private var timeoutStartNanos: Long = 0

@Volatile
private var state = State.FOREGROUND
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since line 69 performs a non atomic operation (check and set), I think @Volatile here is not enough and we should replace it by wrapping the state in an AtomicReference to ensure another thread cannot change the state between the check and the update.


// for testing
internal constructor(sessionConfig: SessionConfig) : this(
Clock.getDefault(),
sessionConfig.backgroundInactivityTimeout,
)

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 >= sessionBackgroundInactivityTimeout.inWholeNanoseconds
}

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,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Originally from https://github.com/open-telemetry/opentelemetry-android/blob/main/android-agent/src/main/kotlin/io/opentelemetry/android/agent/session/SessionManager.kt
*
* Was publicly available before 0.14.0-alpha and this implementation meets our needs. There are a couple thread safety concerns in this code,
* but we expect those to be addressed by the otel-android maintainers. Rather than fix them ourselves and deviate/fork, we will come back
* to check for any updates before this is released in a 1.X version of our plugin. O11Y-443 tracks this task.
*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package com.launchdarkly.observability.vendored.otel

import io.opentelemetry.android.agent.session.SessionConfig
import io.opentelemetry.android.agent.session.SessionIdGenerator
import io.opentelemetry.android.agent.session.SessionStorage
import io.opentelemetry.android.session.Session
import io.opentelemetry.android.session.SessionObserver
import io.opentelemetry.android.session.SessionProvider
import io.opentelemetry.android.session.SessionPublisher
import io.opentelemetry.sdk.common.Clock
import java.util.Collections.synchronizedList
import kotlin.time.Duration

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 maxSessionLifetime: Duration,
) : SessionProvider,
SessionPublisher {
// TODO: Make thread safe / wrap with AtomicReference?
private var session: Session = Session.NONE
private val observers = synchronizedList(ArrayList<SessionObserver>())

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 previousSession = session
session = newSession
observers.forEach {
it.onSessionEnded(previousSession)
it.onSessionStarted(session, previousSession)
}
}
return session.getId()
}

private fun sessionHasExpired(): Boolean {
val elapsedTime = clock.now() - session.getStartTimestamp()
return elapsedTime >= maxSessionLifetime.inWholeNanoseconds
}

companion object {
@JvmStatic
fun create(
timeoutHandler: SessionIdTimeoutHandler,
sessionConfig: SessionConfig,
): SessionManager =
SessionManager(
timeoutHandler = timeoutHandler,
maxSessionLifetime = sessionConfig.maxLifetime,
)
}
}