Android SDK observability reference

This LaunchDarkly observability plugin is available for early access

This LaunchDarkly observability plugin is currently available in Early Access, and APIs are subject to change until a 1.x version is released.

This topic documents how to get started with the LaunchDarkly observability plugin for the Android SDK.

The Android SDK supports the observability plugin for error monitoring, logging, tracing, and session replay.

SDK quick links

LaunchDarkly SDKs are open source. In addition to this reference guide, we provide source, API reference documentation, and a sample application:

ResourceLocation
SDK API documentation

Observability plugin API docs

GitHub repository

@launchdarkly/observability-android

Published moduleMaven

Prerequisites and dependencies

This reference guide assumes that you are somewhat familiar with the LaunchDarkly Android SDK.

The observability plugin is compatible with the Android SDK, version 5.9.0 and later.

The LaunchDarkly Android SDK is compatible with Android SDK versions 21 and higher (Android 5.0, Lollipop).

Get started

Follow these steps to get started:

Install the plugin

LaunchDarkly uses a plugin to the Android SDK to provide observability.

The first step is to make both the SDK and the observability plugin available as dependencies.

Here’s how:

1implementation 'com.launchdarkly:launchdarkly-android-client-sdk:5.+'
2implementation 'com.launchdarkly:launchdarkly-observability-android:0.21.0'

Then, import the plugin into your code:

1import com.launchdarkly.sdk.*;
2import com.launchdarkly.sdk.android.*;
3import com.launchdarkly.observability.plugin.Observability;
4import com.launchdarkly.sdk.android.integrations.Plugin;

Initialize the client

Next, initialize the SDK and the plugin.

To initialize, you need your LaunchDarkly environment’s mobile key and the context for which you want to evaluate flags. This authorizes your application to connect to a particular environment within LaunchDarkly. To learn more, read Initialize the client in the Android SDK reference guide.

Android observability SDK credentials

The Android observability SDK uses a mobile key. Keys are specific to each project and environment. They are available on the SDK keys page under Settings. To learn more about key types, read Keys.

Mobile keys are not secret and you can expose them in your client-side code without risk. However, never embed a server-side SDK key into a client-side application.

Here’s how to initialize the SDK and plugin:

1String mobileKey = "example-mobile-key";
2
3LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled)
4 .mobileKey(mobileKey)
5 .plugins(Components.plugins().setPlugins(
6 Collections.singletonList<Plugin>(Observability(this.getApplication(), mobileKey))
7 ))
8 // other options
9 .build();
10
11// You'll need this context later, but you can ignore it for now.
12LDContext context = LDContext.create("example-context-key");
13
14LDClient client = LDClient.init(this.getApplication(), ldConfig, context, 0);

Configure the plugin options

You can configure options for the observability plugin when you initialize the SDK. The plugin constructor takes an optional object with the configuration details.

Here is an example:

Plugin options, Android SDK v5.9+
1String mobileKey = "example-mobile-key";
2
3LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled)
4 .mobileKey(mobileKey)
5 .plugins(
6 Components.plugins().setPlugins(
7 Collections.singletonList<Plugin>(
8 Observability(
9 this.getApplication(),
10 mobileKey,
11 ObservabilityOptions(
12 resourceAttributes = Attributes.of(
13 AttributeKey.stringKey("serviceName"), "example-service"
14 )
15 )
16 )
17 )
18 )
19 )
20 .build();

For more information on plugin options, read Configuration for client-side observability.

Advanced configuration options

You can customize the observability plugin with additional options:

Advanced options example
1val mobileKey = "example-mobile-key"
2
3val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
4 .mobileKey(mobileKey)
5 .plugins(
6 Components.plugins().setPlugins(
7 listOf(
8 Observability(
9 this@BaseApplication,
10 mobileKey,
11 ObservabilityOptions(
12 serviceName = "my-android-app",
13 serviceVersion = "1.0.0",
14 debug = true,
15 logsApiLevel = ObservabilityOptions.LogLevel.WARN,
16 tracesApi = ObservabilityOptions.TracesApi(includeErrors = true, includeSpans = false),
17 metricsApi = ObservabilityOptions.MetricsApi.disabled(),
18 instrumentations = ObservabilityOptions.Instrumentations(
19 crashReporting = false,
20 launchTime = true,
21 userTaps = true,
22 screens = true
23 ),
24 resourceAttributes = Attributes.of(
25 AttributeKey.stringKey("environment"), "production",
26 AttributeKey.stringKey("team"), "mobile"
27 ),
28 customHeaders = mapOf(
29 "X-Custom-Header" to "custom-value"
30 )
31 )
32 )
33 )
34 )
35 )
36 .build()

The available ObservabilityOptions configuration options are:

  • logsApiLevel: Minimum log severity to export. Defaults to INFO. Set to ObservabilityOptions.LogLevel.NONE to disable log exporting.
  • tracesApi: Controls trace recording. Defaults to enabled. Use ObservabilityOptions.TracesApi.disabled() to disable all tracing, or set includeErrors/includeSpans individually.
  • metricsApi: Controls metric export. Defaults to enabled. Use ObservabilityOptions.MetricsApi.disabled() to disable metrics.
  • instrumentations: Enables or disables specific automatic instrumentations:
    • crashReporting: If true, automatically reports uncaught exceptions as errors. Defaults to true.
    • launchTime: If true, automatically measures and reports application startup time as metrics. Defaults to false.
    • userTaps: If true, runs tap detection. The analytics.taps option separately controls whether the plugin publishes detected taps as click spans. If userTaps is false, the plugin publishes no click spans regardless of the analytics.taps value. Neither option affects session replay capture. Defaults to true.
    • screens: If true, automatically detects screen changes from Android Activity lifecycle callbacks. Screen detection drives both the automatic screen_view span and the session replay Navigate events. Defaults to true. The analytics.screenViews option gates screen_view spans.
  • sessionBackgroundTimeout: How long the app can stay in the background before the current session ends. In Kotlin, this is a kotlin.time.Duration, such as 30.minutes. In Java, use the sessionBackgroundTimeoutMillis(long) builder method instead. Defaults to 15 minutes.
  • serviceName: The service name for the application. Defaults to “observability-android”.
  • serviceVersion: The version of the service. Defaults to the SDK version.
  • debug: Enables verbose internal logging and debug functionality. Defaults to false.
  • resourceAttributes: Additional resource attributes to include in telemetry data.
  • customHeaders: Custom headers to include with OTLP exports.

Configure additional instrumentations

To enable HTTP request instrumentation and user interaction instrumentation, add the following plugin and dependencies to your top level application’s Gradle file.

1plugins {
2 id 'net.bytebuddy.byte-buddy-gradle-plugin' version '1.+'
3}
4
5dependencies {
6 // Android HTTP Url instrumentation
7 implementation 'io.opentelemetry.android.instrumentation:httpurlconnection-library:0.11.0-alpha'
8 byteBuddy 'io.opentelemetry.android.instrumentation:httpurlconnection-agent:0.11.0-alpha'
9
10 // OkHTTP instrumentation
11 implementation 'io.opentelemetry.android.instrumentation:okhttp3-library:0.11.0-alpha'
12 byteBuddy 'io.opentelemetry.android.instrumentation:okhttp3-agent:0.11.0-alpha'
13}

Configure product analytics event collection

The Android observability plugin can record the following product analytics events as OpenTelemetry spans:

  • Taps (automatic): A click span for each user tap, with details about the tapped element and screen location. Enabled by default.
  • Track events (automatic): A track span when your code calls track(). Enabled by default. To learn more, read Recording product analytics events.
  • Screen views (automatic and manual): A screen_view span when the app shows a screen, either detected automatically or recorded manually when your code calls trackScreenView(). To learn more, read Recording product analytics events.
  • App lifecycle (automatic): An app_foreground or app_background span as the app moves between the foreground and background states. Enabled by default.
  • App launches (automatic): An app_launch span once per process launch, with the launch type and version information, plus an app.start span event that records the cold or warm startup dimension. Enabled by default. To learn more, read App launch events.

All product analytics span events also include information about the LaunchDarkly context that generated the event.

Use the generated span events to create custom product analytics charts, such as time series and funnels. To learn more, read Product analytics events.

The plugin records all event types by default. To customize, pass an ObservabilityOptions.Analytics object to the analytics parameter:

Customize individual event types
1ObservabilityOptions(
2 analytics = ObservabilityOptions.Analytics(
3 taps = true,
4 trackEvents = true,
5 screenViews = true,
6 appLifecycle = true,
7 appLaunch = true,
8 )
9)

The ObservabilityOptions.Analytics options are:

  • taps: Emits a click span for each detected tap. Defaults to true.
  • trackEvents: Emits a track span when a custom event is tracked with track(). Defaults to true.
  • screenViews: Emits a screen_view span when the app displays a screen. Defaults to true.
  • appLifecycle: Emits app_foreground and app_background spans as the app moves between states. Defaults to true.
  • appLaunch: Emits an app_launch span once per process launch. Defaults to true.

Track screen views

The plugin emits a screen_view span each time your app shows a screen. Each screen_view span uses the event.* attribute namespace, and includes the event.previous_screen attribute from a shared navigation stack. The span broadcasts a session replay Navigate event to reflect navigation in the replay timeline.

Capture activities automatically

When instrumentations.screens is true (the default), the plugin captures every Android Activity as it resumes. It derives the screen name from the class name. For example, ProfileActivity becomes Profile. The plugin populates the event.screen_class and event.screen_id attributes from the Activity class.

To customize the reported name or category, implement LDScreenNameProvider on your Activity:

Custom screen name (Kotlin)
1import com.launchdarkly.observability.client.screen.LDScreenNameProvider
2
3class CheckoutActivity : ComponentActivity(), LDScreenNameProvider {
4 override val ldScreenName: String = "Checkout"
5 override val ldScreenCategory: String = "Commerce"
6}

Capture fragments and compose destinations manually

The plugin does not automatically capture screens that lack a distinct Activity, such as Fragments and Jetpack Compose destinations. Record these kinds of screens with LDObserve.trackScreenView. To learn more, read Recording product analytics events.

Only one call per appearance is required. The plugin resolves event.previous_screen through the shared navigation stack, which handles both re-appearance and back navigation.

About detection and span emission

Two separate options control screen views:

  • instrumentations.screens controls automatic screen detection. It drives both the automatic screen_view span and the session replay Navigate event.
  • analytics.screenViews only gates the screen_view span. Detection and the Navigate event are independent of this option, and manual trackScreenView calls still work if you disable instrumentations.screens.

Configure session replay

The Android SDK supports session replay, which captures snapshots of your app’s UI at regular intervals. This helps you review user sessions in LaunchDarkly to better understand user behavior and diagnose issues.

To enable session replay, add the SessionReplay plugin to the plugins list after the Observability plugin. Session replay depends on the Observability plugin being present and initialized first.

Here’s how:

Enable session replay
1import com.launchdarkly.observability.plugin.Observability
2import com.launchdarkly.observability.replay.plugin.SessionReplay
3
4val mobileKey = "example-mobile-key"
5
6val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
7 .mobileKey(mobileKey)
8 .plugins(
9 Components.plugins().setPlugins(
10 listOf(
11 Observability(this@BaseApplication, mobileKey),
12 SessionReplay() // depends on Observability being present first
13 )
14 )
15 )
16 .build()

Important 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.

Initialize the plugins after the SDK client

You can initialize the session replay plugin manually, after the SDK client is initialized.

This approach supports feature-flagged rollouts or dynamic initialization after end user consent. Set enabled to false in ReplayOptions, then call LDReplay.start() when you’re ready to begin recording.

First, configure the plugin with enabled = false:

Manual start configuration
1import com.launchdarkly.observability.plugin.Observability
2import com.launchdarkly.observability.replay.plugin.SessionReplay
3import com.launchdarkly.observability.replay.ReplayOptions
4
5val mobileKey = "example-mobile-key"
6
7val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
8 .mobileKey(mobileKey)
9 .plugins(
10 Components.plugins().setPlugins(
11 listOf(
12 Observability(this@BaseApplication, mobileKey),
13 SessionReplay(
14 options = ReplayOptions(
15 enabled = false // Don't start recording automatically
16 )
17 )
18 )
19 )
20 )
21 .build()
22
23val context = LDContext.create("example-context-key")
24val client = LDClient.init(this@BaseApplication, ldConfig, context, 0)

Then, start the session replay plugin when appropriate, such as after receiving end user consent or when a feature flag enables session replay.

1import com.launchdarkly.observability.sdk.LDReplay
2
3// Start recording after user consent or feature flag check
4LDReplay.start()

This approach allows you to:

  • Feature-flag the rollout of session replay to a subset of end users
  • Wait for end user consent before starting data collection
  • Dynamically enable session replay based on runtime conditions
  • Maintain compliance with privacy regulations

Session replay configuration options

You can customize session replay behavior by passing a ReplayOptions object to the SessionReplay constructor:

Session replay options
1import com.launchdarkly.observability.plugin.Observability
2import com.launchdarkly.observability.replay.plugin.SessionReplay
3import com.launchdarkly.observability.replay.ReplayOptions
4import com.launchdarkly.observability.replay.PrivacyProfile
5
6val mobileKey = "example-mobile-key"
7
8val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
9 .mobileKey(mobileKey)
10 .plugins(
11 Components.plugins().setPlugins(
12 listOf(
13 Observability(this@BaseApplication, mobileKey),
14 SessionReplay(
15 options = ReplayOptions(
16 privacyProfile = PrivacyProfile(
17 maskTextInputs = true,
18 maskText = true
19 ),
20 capturePeriodMillis = 1000,
21 debug = false
22 )
23 )
24 )
25 )
26 )
27 .build()

The available ReplayOptions configuration options are:

  • privacyProfile: Controls how UI elements are masked in the replay. To learn more, read Privacy options.
  • capturePeriodMillis: Period between UI captures in milliseconds. Defaults to 1000 (1 second).
  • debug: Enables verbose logging when set to true. Defaults to false.

Note: Service configuration options like serviceName and serviceVersion are set in ObservabilityOptions, not in ReplayOptions.

Privacy options

The PrivacyProfile class controls how UI elements are masked during session replay. Session replay for Android uses Jetpack Compose semantics to identify and mask UI elements. By default, text inputs are masked to protect user privacy.

Here’s how to configure privacy settings:

Privacy profile configuration
1import com.launchdarkly.observability.plugin.Observability
2import com.launchdarkly.observability.replay.plugin.SessionReplay
3import com.launchdarkly.observability.replay.ReplayOptions
4import com.launchdarkly.observability.replay.PrivacyProfile
5
6val mobileKey = "example-mobile-key"
7
8val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
9 .mobileKey(mobileKey)
10 .plugins(
11 Components.plugins().setPlugins(
12 listOf(
13 Observability(this@BaseApplication, mobileKey),
14 SessionReplay(
15 options = ReplayOptions(
16 privacyProfile = PrivacyProfile(
17 maskTextInputs = true,
18 maskText = false
19 )
20 )
21 )
22 )
23 )
24 )
25 .build()

The available privacy options are:

  • maskTextInputs: When true, masks all text input fields including editable text and paste operations. Defaults to true.
  • maskText: When true, masks all non-input text elements in the UI. Defaults to false.
  • maskBySemanticsKeywords: When true, masks sensitive views that contain password fields or text matching sensitive keywords. Defaults to false.
  • maskViews: A list of Android View classes to mask. Because matching uses the exact class names, subclasses do not match. Use the view() helper with either a Kotlin class or a fully qualified class name.
  • maskWebViews: When true, masks the contents of web views using a default list of WebView class names. This includes subclasses, and web views hosted inside Jetpack Compose through AndroidView. Defaults to false.
  • maskXMLViewIds: A list of resource IDs to mask. Accepts the @+id/example, @id/example, or example format.
  • unmaskXMLViewIds: A list of resource IDs to unmask. Uses the same format as maskXMLViewIds.

Sensitive keywords

When maskBySemanticsKeywords is enabled, the SDK automatically masks any Compose UI text or content descriptions containing predetermined keywords. Keywords you specify are not case-sensitive. For the current set of keywords, read PrivacyProfile.

Common privacy configurations

For maximum privacy (recommended for production):

Maximum privacy
1privacyProfile = PrivacyProfile(
2 maskTextInputs = true,
3 maskText = true,
4 maskBySemanticsKeywords = true
5)

For debugging or development, you can turn masking off:

No masking
1privacyProfile = PrivacyProfile(
2 maskTextInputs = false,
3 maskText = false,
4 maskBySemanticsKeywords = false
5)

For selective masking, which masks inputs and sensitive data but shows regular text:

Selective masking
1privacyProfile = PrivacyProfile(
2 maskTextInputs = true,
3 maskText = false,
4 maskBySemanticsKeywords = true
5)

Custom masking with ldMask

In addition to the privacy profile settings, you can explicitly mask individual UI elements by using the .ldMask() modifier. This is useful when you need to mask specific sensitive fields while allowing other content to remain visible.

Masking XML views

For traditional Android XML-based views, you can mask any View by calling the .ldMask() extension function:

Masking XML views
1import com.launchdarkly.observability.api.ldMask
2
3class LoginActivity : AppCompatActivity() {
4 override fun onCreate(savedInstanceState: Bundle?) {
5 super.onCreate(savedInstanceState)
6 setContentView(R.layout.activity_login)
7
8 val password = findViewById<EditText>(R.id.password)
9 password.ldMask() // mask this field in session replay
10 }
11}

Masking Jetpack Compose elements

For Jetpack Compose, you can add the .ldMask() modifier to any composable to mask it in session replay recordings:

Masking Compose elements
1import com.launchdarkly.observability.api.ldMask
2
3@Composable
4fun CreditCardField() {
5 var number by remember { mutableStateOf("") }
6 TextField(
7 value = number,
8 onValueChange = { number = it },
9 modifier = Modifier
10 .fillMaxWidth()
11 .ldMask() // mask this composable in session replay
12 )
13}

Unmasking elements

You can also explicitly unmask elements that would otherwise be masked by the privacy profile settings using the .ldUnmask() modifier:

Unmasking elements
1import com.launchdarkly.observability.api.ldUnmask
2
3@Composable
4fun PublicInfoField() {
5 var info by remember { mutableStateOf("") }
6 TextField(
7 value = info,
8 onValueChange = { info = it },
9 modifier = Modifier
10 .fillMaxWidth()
11 .ldUnmask() // explicitly unmask this field
12 )
13}

The .ldMask() and .ldUnmask() modifiers give you fine-grained control over which UI elements are masked in session replay recordings, allowing you to balance privacy protection with useful debugging information.

Mask by resource ID

Instead of calling .ldMask() on each element, you can mask or unmask XML views by their resource ID. Use the maskXMLViewIds and unmaskXMLViewIds options in your PrivacyProfile.

Here is an example:

Mask by resource ID
1import com.launchdarkly.observability.replay.PrivacyProfile
2import com.launchdarkly.observability.replay.ReplayOptions
3import com.launchdarkly.observability.replay.view
4import com.launchdarkly.observability.replay.plugin.SessionReplay
5
6val sessionReplay = SessionReplay(
7 ReplayOptions(
8 privacyProfile = PrivacyProfile(
9 maskTextInputs = true,
10 maskViews = listOf(
11 view(android.widget.ImageView::class),
12 view("android.widget.EditText"),
13 ),
14 maskWebViews = true,
15 maskXMLViewIds = listOf(
16 "@+id/password",
17 "credit_card_number",
18 ),
19 unmaskXMLViewIds = listOf(
20 "@+id/greeting",
21 ),
22 )
23 )
24)

These options apply to views with an ID that resolves to a resource entry name. unmaskXMLViewIds takes precedence over global rules such as maskText and maskTextInputs, but an explicit mask on the same view or any of its parent views still wins. To learn more, read Masking precedence.

Masking precedence

When the SDK decides whether to mask a view, it evaluates the following rules in order and stops at the first rule that applies:

  1. Explicit masking: If the view or any of its parent views is explicitly masked with .ldMask() or a matching maskXMLViewIds entry, the SDK masks the view. This overrides all other rules.
  2. Explicit unmasking: If the view or any of its parent views is explicitly unmasked with .ldUnmask() or a matching unmaskXMLViewIds entry, the SDK does not mask the view.
  3. Global configuration: If a global privacy option such as maskTextInputs or maskText applies to the view, the SDK follows that option.

If rules conflict at the same level, masking wins over unmasking.

For more information on session replay configuration, read Configuration for session replay.

Manual instrumentation

After initializing the observability plugin, use the LDObserve singleton to manually instrument your Android application with custom metrics, logs, errors, traces, and product analytics events.

Recording custom metrics

Use the metric methods on LDObserve to record a Metric with a name and a value. Each method maps to a different OpenTelemetry instrument type:

Record metrics (Kotlin)
1import com.launchdarkly.observability.sdk.LDObserve
2import com.launchdarkly.observability.interfaces.Metric
3
4// Record a raw measurement
5LDObserve.recordMetric(Metric("user_actions", 1.0))
6
7// Record a monotonically increasing counter
8LDObserve.recordCount(Metric("api_calls", 1.0))
9
10// Increment a counter
11LDObserve.recordIncr(Metric("page_views", 1.0))
12
13// Record a value distribution
14LDObserve.recordHistogram(Metric("response_time", 150.0))
15
16// Record a value that can increase or decrease
17LDObserve.recordUpDownCounter(Metric("active_connections", 1.0))

Recording custom logs

Use LDObserve.recordLog to emit a structured log. Pass attributes as a plain Kotlin map through the properties parameter, or use the Attributes overload when you need precise OpenTelemetry typing:

1// Record a basic log message
2LDObserve.recordLog(
3 message = "User login successful",
4 severity = Severity.INFO
5)
6
7// Record logs with custom properties
8LDObserve.recordLog(
9 message = "Authentication completed",
10 severity = Severity.INFO,
11 properties = mapOf(
12 "user_id" to "12345",
13 "action" to "login"
14 )
15)

Recording custom errors

Use LDObserve.recordError to report a caught exception, with optional attributes for context:

Record errors (Kotlin)
1import com.launchdarkly.observability.sdk.LDObserve
2import io.opentelemetry.api.common.AttributeKey
3import io.opentelemetry.api.common.Attributes
4
5try {
6 processPayment()
7} catch (e: Exception) {
8 LDObserve.recordError(
9 e,
10 Attributes.of(
11 AttributeKey.stringKey("component"), "payment",
12 AttributeKey.stringKey("error_code"), "PAYMENT_FAILED"
13 )
14 )
15}

The plugin also reports uncaught exceptions automatically while instrumentations.crashReporting remains true, which is the default.

Recording custom traces

Use LDObserve.startSpan to create a span for tracing an operation. Always end the span when the operation completes:

1// Start a span with custom properties
2val span = LDObserve.startSpan(
3 name = "database_query",
4 properties = mapOf(
5 "table" to "users",
6 "operation" to "select"
7 )
8)
9
10// Perform your operation
11performDatabaseQuery()
12
13// Always end the span
14span.end()

Recording product analytics events

Use track to record a custom event as a product analytics span. Use trackScreenView to record a screen view:

Track a custom event (Kotlin)
1// Track an event with properties and an optional metric value
2LDObserve.track(
3 key = "purchase_completed",
4 properties = mapOf(
5 "product_id" to "SKU-123",
6 "price" to 29.99
7 ),
8 metricValue = 29.99
9)
10
11// Track an event with no properties
12LDObserve.track(key = "button_tapped")
Track a screen view (Kotlin)
1// Convenience — name only
2LDObserve.trackScreenView(name = "ProductDetail")
3
4// With category
5LDObserve.trackScreenView(name = "Checkout", category = "purchase")
6
7// Full details with custom properties
8LDObserve.trackScreenView(
9 name = "ProductDetail",
10 screenClass = "ProductDetailActivity",
11 screenId = "product-123",
12 category = "browsing",
13 properties = mapOf(
14 "product_id" to "SKU-123",
15 "source" to "search_results"
16 )
17)

Distributed tracing

To implement distributed tracing, all spans generated across different function calls and services for the same user request must share the same context.

While nested spans in the same synchronous scope automatically use the context of their parent span, the context is not automatically propagated when launching new coroutines or switching dispatchers. Use the OpenTelemetry Context API to capture and restore the parent span context explicitly in asynchronous code:

1import io.opentelemetry.context.Context;
2import io.opentelemetry.context.Scope;
3
4Span parentSpan = LDObserve.startSpan("parentSpan", new HashMap<>());
5try (Scope parentScope = parentSpan.makeCurrent()) {
6 Context context = Context.current();
7
8 new Thread(() -> {
9 try (Scope childScope = context.makeCurrent()) {
10 Span childSpan = LDObserve.startSpan("childSpan", new HashMap<>());
11 // do work
12 childSpan.end();
13 }
14 }).start();
15} finally {
16 parentSpan.end();
17}

Alternatively, you can store the current context and restore it later in a different scope:

1import io.opentelemetry.context.Context;
2import io.opentelemetry.context.Scope;
3
4Span parentSpan = LDObserve.startSpan("parentSpan", new HashMap<>());
5try (Scope parentScope = parentSpan.makeCurrent()) {
6 // Now parentSpan is active in Context.current()
7 Context ctx = Context.current();
8
9 // Later, in another thread, restore the context
10 executor.execute(() -> {
11 try (Scope scope = ctx.makeCurrent()) {
12 Span nestedSpan = LDObserve.startSpan("nestedSpan", new HashMap<>());
13 // do work — nestedSpan is a child of parentSpan
14 nestedSpan.end();
15 }
16 });
17} finally {
18 parentSpan.end();
19}

To propagate a span context to an entirely different service, use the OpenTelemetry W3CTraceContextPropagator to inject the context into outgoing HTTP headers. Services that receive the headers can then extract and use the context.

1import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
2import io.opentelemetry.context.Context;
3import okhttp3.Request;
4
5W3CTraceContextPropagator propagator = W3CTraceContextPropagator.getInstance();
6Request.Builder requestBuilder = new Request.Builder().url(url);
7
8propagator.inject(
9 Context.current(),
10 requestBuilder,
11 (carrier, key, value) -> carrier.addHeader(key, value)
12);
13
14Request request = requestBuilder.build();

The above example adds header content similar to:

Example header content
1traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
2tracestate: (optional vendor data)

Services that receive the headers use the extract method from W3CTraceContextPropagator to obtain the trace context and use it as the parent context for new spans.

Explore supported features

The observability plugin supports the following features. After the SDK and plugins are initialized, you can access these from within your application:

Review observability data in LaunchDarkly

After you initialize the SDK and observability plugin, your application automatically starts sending observability data back to LaunchDarkly. You can review this information in the LaunchDarkly user interface. To learn how, read Observability.

The observability data collected includes:

  • Error monitoring: Unhandled exceptions, crashes, and manually recorded errors with stack traces
  • Logs: Application logs with configurable severity levels and custom attributes
  • Traces: Distributed tracing data including span timing, nested operations, and custom instrumentation
  • Metrics: Performance metrics, custom counters, histograms, and gauge measurements
  • Session data: User session information including lifecycle events and timing

Specifically, the observability data includes events that LaunchDarkly uses to automatically create the following metrics:

  • User error rate and crash frequency
  • Application performance metrics such as launch time and session duration
  • Feature flag evaluation context and timing
  • Custom business metrics recorded through the SDK

To learn more about autogenerated metrics, read Observability autogenerated metrics.