Skip to content
2 changes: 1 addition & 1 deletion e2e/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ dependencies {
// Uncomment to use the publicly released version (note this may be behind branch/main)
// implementation("com.launchdarkly:launchdarkly-observability-android:0.2.0")

implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.9.0")
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.10.0")

implementation("io.opentelemetry:opentelemetry-api:1.51.0")
implementation("io.opentelemetry:opentelemetry-sdk:1.51.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@ package com.example.androidobservability
import android.app.Application
import com.launchdarkly.observability.api.Options
import com.launchdarkly.observability.client.TelemetryInspector
import com.launchdarkly.observability.plugin.Observability
import com.launchdarkly.observability.replay.PrivacyProfile
import com.launchdarkly.observability.replay.ReplayOptions
import com.launchdarkly.observability.replay.SessionReplay
import com.launchdarkly.sdk.ContextKind
import com.launchdarkly.sdk.LDContext
import com.launchdarkly.sdk.android.Components
import com.launchdarkly.sdk.android.LDAndroidLogging
import com.launchdarkly.sdk.android.LDClient
import com.launchdarkly.sdk.android.LDConfig
import com.launchdarkly.observability.plugin.Observability
import com.launchdarkly.observability.replay.PrivacyProfile
import com.launchdarkly.observability.replay.ReplayInstrumentation
import com.launchdarkly.observability.replay.ReplayOptions
import com.launchdarkly.sdk.android.LDAndroidLogging
import com.launchdarkly.sdk.android.integrations.Plugin
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.common.Attributes
import java.util.Collections

open class BaseApplication : Application() {

Expand All @@ -32,14 +30,6 @@ open class BaseApplication : Application() {
),
debug = true,
logAdapter = LDAndroidLogging.adapter(),
// TODO: consider these being factories so that the obs plugin can pass instantiation data, log adapter
instrumentations = listOf(
ReplayInstrumentation(
options = ReplayOptions(
privacyProfile = PrivacyProfile(maskText = false)
)
)
),
)

var telemetryInspector: TelemetryInspector? = null
Expand All @@ -52,6 +42,12 @@ open class BaseApplication : Application() {
options = testUrl?.let { pluginOptions.copy(backendUrl = it, otlpEndpoint = it) } ?: pluginOptions
)

val sessionReplayPlugin = SessionReplay(
options = ReplayOptions(
privacyProfile = PrivacyProfile(maskText = false)
)
)

// Set LAUNCHDARKLY_MOBILE_KEY to your LaunchDarkly mobile key found on the LaunchDarkly
// dashboard in the start guide.
// If you want to disable the Auto EnvironmentAttributes functionality.
Expand All @@ -60,7 +56,10 @@ open class BaseApplication : Application() {
.mobileKey(LAUNCHDARKLY_MOBILE_KEY)
.plugins(
Components.plugins().setPlugins(
Collections.singletonList<Plugin>(observabilityPlugin)
listOf(
observabilityPlugin,
sessionReplayPlugin
)
)
)
.build()
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.0"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
Expand Down
25 changes: 25 additions & 0 deletions sdk/@launchdarkly/observability-android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,31 @@ span.end()

### Session Replay

#### Enable Session Replay

Add the Session Replay plugin **after** Observability when configuring the LaunchDarkly SDK:

```kotlin
import com.launchdarkly.observability.plugin.Observability
import com.launchdarkly.observability.replay.SessionReplay

val ldConfig = LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Enabled)
.mobileKey("your-mobile-key")
.plugins(
Components.plugins().setPlugins(
listOf(
Observability(this@MyApplication, "your-mobile-key"),
SessionReplay() // depends on Observability being present first
)
)
)
.build()
```

Notes:
- SessionReplay depends on Observability. If Observability is missing or listed after SessionReplay, the plugin logs an error and stays inactive.
- Observability runs fine without SessionReplay; adding SessionReplay extends the Observability pipeline to include session recording.

#### Masking sensitive UI

Use `ldMask()` to mark views that should be masked in session replay. There are helpers for both XML-based Views and Jetpack Compose.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format

[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.1.20" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.1.20" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.2.0" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.2.0" }
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ allprojects {
}

dependencies {
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.9.0")
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.10.0")
implementation("com.jakewharton.timber:timber:5.0.1")

// Android
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.launchdarkly.observability.api

import com.launchdarkly.logging.LDLogAdapter
import com.launchdarkly.observability.BuildConfig
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation
import com.launchdarkly.sdk.android.LDTimberLogging
import io.opentelemetry.api.common.Attributes
import kotlin.time.Duration
Expand All @@ -29,7 +28,6 @@ const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdarkly.com"
* @property disableMetrics Disables metrics if true. Defaults to false.
* @property logAdapter The log adapter to use. Defaults to using the LaunchDarkly SDK's LDTimberLogging.adapter(). Use LDAndroidLogging.adapter() to use the Android logging adapter.
* @property loggerName The name of the logger to use. Defaults to "LaunchDarklyObservabilityPlugin".
* @property instrumentations List of additional instrumentations to use
*/
data class Options(
val serviceName: String = DEFAULT_SERVICE_NAME,
Expand All @@ -46,6 +44,4 @@ data class Options(
val disableMetrics: Boolean = false,
val logAdapter: LDLogAdapter = LDTimberLogging.adapter(), // this follows the LaunchDarkly SDK's default log adapter
val loggerName: String = "LaunchDarklyObservabilityPlugin",
// TODO: update this to provide a list of factories instead of instances
val instrumentations: List<LDExtendedInstrumentation> = emptyList()
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.launchdarkly.observability.client
import android.app.Application
import com.launchdarkly.logging.LDLogger
import com.launchdarkly.observability.api.Options
import com.launchdarkly.observability.coroutines.DispatcherProviderHolder
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation
import com.launchdarkly.observability.interfaces.Metric
import com.launchdarkly.observability.network.GraphQLClient
import com.launchdarkly.observability.network.SamplingApiService
Expand All @@ -12,10 +14,8 @@ import com.launchdarkly.observability.sampling.SamplingConfig
import com.launchdarkly.observability.sampling.SamplingLogProcessor
import com.launchdarkly.observability.sampling.SamplingTraceExporter
import com.launchdarkly.observability.sampling.SpansSampler
import com.launchdarkly.observability.coroutines.DispatcherProviderHolder
import io.opentelemetry.android.OpenTelemetryRum
import io.opentelemetry.android.config.OtelRumConfig
import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfig
import io.opentelemetry.android.session.SessionConfig
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.logs.Logger
Expand Down Expand Up @@ -54,18 +54,25 @@ import java.util.concurrent.TimeUnit
/**
* Manages instrumentation for LaunchDarkly Observability.
*
* This class is responsible for setting up and managing the OpenTelemetry RUM (Real User Monitoring)
* instrumentation. It configures the providers for logs, traces, and metrics based on the
* provided options. It also handles dynamic sampling configuration and provides methods to
* record various telemetry signals like metrics, logs, and traces.
*
* @param application The application instance.
* @param sdkKey The SDK key.
* @param sdkKey The SDK key for authentication.
* @param resources The OpenTelemetry resource describing this service.
* @param logger The logger.
* @param options Additional options.
* @param logger The logger for internal logging.
* @param options Additional configuration options for the SDK.
* @param instrumentations A list of custom instrumentations to be added.
*/
class InstrumentationManager(
private val application: Application,
private val sdkKey: String,
private val resources: Resource,
private val logger: LDLogger,
private val options: Options,
private val instrumentations: List<LDExtendedInstrumentation>,
) {
private val otelRUM: OpenTelemetryRum
private var otelMeter: Meter
Expand Down Expand Up @@ -104,7 +111,8 @@ class InstrumentationManager(
resources,
logger,
telemetryInspector,
options
options,
instrumentations
)
logProcessor = processor
sdkLoggerProviderBuilder.addLogRecordProcessor(processor)
Expand All @@ -125,7 +133,7 @@ class InstrumentationManager(
}
}

for (instrumentation in options.instrumentations) {
for (instrumentation in instrumentations) {
rumBuilder.addInstrumentation(instrumentation)
}

Expand Down Expand Up @@ -402,6 +410,7 @@ class InstrumentationManager(
logger: LDLogger,
telemetryInspector: TelemetryInspector?,
options: Options,
instrumentations: List<LDExtendedInstrumentation>,
): LogRecordProcessor {
val primaryLogExporter = createOtlpLogExporter(options)
sdkLoggerProviderBuilder.setResource(resource)
Expand Down Expand Up @@ -432,7 +441,7 @@ class InstrumentationManager(
pipeline to provide instrumentation specific caching and export.
*/
val routingLogRecordProcessor = RoutingLogRecordProcessor(fallthroughProcessor = baseProcessor)
options.instrumentations.forEach { instrumentation ->
instrumentations.forEach { instrumentation ->
instrumentation.getLogRecordProcessor(credential = sdkKey)?.let { processor ->
instrumentation.getLoggerScopeName().let { scopeName ->
routingLogRecordProcessor.addProcessor(scopeName, processor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.launchdarkly.observability.client
import android.app.Application
import com.launchdarkly.logging.LDLogger
import com.launchdarkly.observability.api.Options
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation
import com.launchdarkly.observability.interfaces.Metric
import com.launchdarkly.observability.interfaces.Observe
import io.opentelemetry.api.common.Attributes
Expand All @@ -14,26 +15,34 @@ import io.opentelemetry.sdk.resources.Resource
* The [ObservabilityClient] can be used for recording observability data such as
* metrics, logs, errors, and traces.
*
* It is recommended to use the [Observability] plugin with the LaunchDarkly Android
* Client SDK, as that will automatically initialize the [LDObserve] singleton instance.
* It is recommended to use the [com.launchdarkly.observability.plugin.Observability] plugin with the LaunchDarkly Android
* Client SDK, as that will automatically initialize the [com.launchdarkly.observability.sdk.LDObserve] singleton instance.
*
* @param application The application instance.
* @param sdkKey The SDK key.
* @param resource The resource.
* @param logger The logger.
* @param options Additional options for the client.
*/
class ObservabilityClient : Observe {
private val instrumentationManager: InstrumentationManager

/**
* Creates a new ObservabilityClient.
*
* @param application The application instance.
* @param sdkKey The SDK key for the environment.
* @param resource The resource.
* @param logger The logger.
* @param options Additional options for the client.
* @param instrumentations A list of extended instrumentation providers.
*/
constructor(
application: Application,
sdkKey: String,
resource: Resource,
logger: LDLogger,
options: Options
options: Options,
instrumentations: List<LDExtendedInstrumentation>
) {
this.instrumentationManager = InstrumentationManager(application, sdkKey, resource, logger, options)
this.instrumentationManager = InstrumentationManager(
application, sdkKey, resource, logger, options, instrumentations
)
}

internal constructor(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.launchdarkly.observability.client

import android.app.Application
import com.launchdarkly.logging.LDLogger
import com.launchdarkly.observability.api.Options

/**
* Shared information between plugins.
*/
data class ObservabilityContext(
val sdkKey: String,
val options: Options,
val application: Application,
val logger: LDLogger,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.launchdarkly.observability.plugin
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation

/**
* Plugins can implement this to contribute OpenTelemetry instrumentations that should
* be installed by the Observability plugin.
*/
interface InstrumentationContributor {
fun provideInstrumentations(): List<LDExtendedInstrumentation>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.launchdarkly.observability.plugin

import com.launchdarkly.sdk.android.LDClient
import java.util.WeakHashMap

/**
* Manages a collection of instrumentation contributors associated with an [com.launchdarkly.sdk.android.LDClient] instance.
*
* This object provides a central place to register and retrieve instrumentation contributors for a given LDClient.
* It uses a [java.util.WeakHashMap] to store contributors, which allows the [com.launchdarkly.sdk.android.LDClient] instances and
* associated contributors to be garbage collected when they are no longer in use.
*/
internal object InstrumentationContributorManager {
private val contributors = WeakHashMap<LDClient, MutableList<InstrumentationContributor>>()

/**
* Adds a [InstrumentationContributor] to the list of contributors associated with the given [LDClient].
*
* If no contributors have been added for the client before, a new list is created.
*
* @param client The [LDClient] to associate the contributor with.
* @param contributor The [InstrumentationContributor] to add.
*/
fun add(client: LDClient, contributor: InstrumentationContributor) {
synchronized(contributors) {
contributors.getOrPut(client) { mutableListOf() }.add(contributor)
}
}

/**
* Retrieves a list of [InstrumentationContributor]s associated with the given [LDClient].
*
* The returned list is a snapshot and is safe to iterate over.
*
* @param client The [LDClient] to get the contributors for.
* @return A list of [InstrumentationContributor]s, or empty if no contributors are associated with the client.
*/
fun get(client: LDClient): List<InstrumentationContributor> = synchronized(contributors) {
contributors[client]?.toList().orEmpty()
}

/**
* Clears all contributor registrations.
*/
fun reset() = synchronized(contributors) {
contributors.clear()
}
}
Loading
Loading