Skip to content

Commit 007e597

Browse files
feat: adds configuration options to Android observability plugin (#172)
## Summary O11Y-360 - Add configuration APIs to android obs plugin. ## How did you test this change? Benchtesting at the moment. Plan to add unit tests and integration tests later. ## Are there any deployment considerations? None --------- Co-authored-by: Vadim Korolik <[email protected]>
1 parent c06accb commit 007e597

File tree

11 files changed

+180
-46
lines changed

11 files changed

+180
-46
lines changed

.github/workflows/release-please.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,3 @@ jobs:
8585
with:
8686
workspace-path: sdk/@launchdarkly/observability-android
8787
aws-role-arn: ${{ vars.AWS_ROLE_ARN }}
88-
dry-run: true

e2e/android/app/build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ android {
4040
}
4141

4242
dependencies {
43+
// Uncomment to use the local project
44+
implementation(project(":observability-android"))
45+
// Uncomment to use the publicly released version (note this may be behind branch/main)
46+
// implementation("com.launchdarkly:launchdarkly-observability-android:0.2.0")
47+
4348
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.9.0")
4449

4550
implementation("io.opentelemetry:opentelemetry-api:1.51.0")
@@ -48,7 +53,6 @@ dependencies {
4853
implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0")
4954

5055
implementation("com.google.android.material:material:1.12.0")
51-
implementation(project(":observability-android"))
5256
implementation(libs.androidx.core.ktx)
5357
implementation(libs.androidx.lifecycle.runtime.ktx)
5458
implementation(libs.androidx.activity.compose)

e2e/android/app/src/main/java/com/example/androidobservability/BaseApplication.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package com.example.androidobservability
22

33
import android.app.Application
4+
import com.launchdarkly.observability.api.Options
45
import com.launchdarkly.sdk.ContextKind
56
import com.launchdarkly.sdk.LDContext
67
import com.launchdarkly.sdk.android.Components
78
import com.launchdarkly.sdk.android.LDClient
89
import com.launchdarkly.sdk.android.LDConfig
910
import com.launchdarkly.observability.plugin.Observability
11+
import com.launchdarkly.sdk.android.LDAndroidLogging
1012
import com.launchdarkly.sdk.android.integrations.Plugin
13+
import io.opentelemetry.api.common.AttributeKey
14+
import io.opentelemetry.api.common.Attributes
1115
import java.util.Collections
1216

1317
class BaseApplication : Application() {
@@ -27,9 +31,22 @@ class BaseApplication : Application() {
2731
// Use AutoEnvAttributes.Disabled as the argument to the Builder
2832
val ldConfig = LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Enabled)
2933
.mobileKey(LAUNCHDARKLY_MOBILE_KEY)
30-
.plugins(Components.plugins().setPlugins(
31-
Collections.singletonList<Plugin>(Observability(this@BaseApplication))
32-
))
34+
.plugins(
35+
Components.plugins().setPlugins(
36+
Collections.singletonList<Plugin>(
37+
Observability(
38+
this@BaseApplication,
39+
Options(
40+
resourceAttributes = Attributes.of(
41+
AttributeKey.stringKey("example"), "value"
42+
),
43+
debug = true,
44+
logAdapter = LDAndroidLogging.adapter(),
45+
)
46+
)
47+
)
48+
)
49+
)
3350
.build()
3451

3552
// Set up the context properties. This context should appear on your LaunchDarkly context

e2e/android/app/src/main/java/com/example/androidobservability/ViewModel.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.launchdarkly.observability.sdk.LDObserve
66
import com.launchdarkly.sdk.android.LDClient
77
import io.opentelemetry.api.common.AttributeKey
88
import io.opentelemetry.api.common.Attributes
9+
import io.opentelemetry.api.logs.Severity
910
import io.opentelemetry.api.trace.Span
1011

1112
class ViewModel : ViewModel() {
@@ -26,7 +27,7 @@ class ViewModel : ViewModel() {
2627
fun triggerLog() {
2728
LDObserve.recordLog(
2829
"Test Log",
29-
"debug",
30+
Severity.DEBUG,
3031
Attributes.of(AttributeKey.stringKey("FakeAttribute"), "FakeVal")
3132
)
3233
}
@@ -39,6 +40,7 @@ class ViewModel : ViewModel() {
3940
}
4041

4142
fun triggerStopSpan() {
43+
// TODO O11Y-397: for some reason stopped spans are stacking, the current span might be the problem
4244
lastSpan?.end()
4345
lastSpan = null
4446
}

sdk/@launchdarkly/observability-android/lib/build.gradle.kts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ allprojects {
1717

1818
dependencies {
1919
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.9.0")
20+
implementation("com.jakewharton.timber:timber:5.0.1")
2021

2122
// TODO: revise these versions to be as old as usable for compatibility
2223
implementation("io.opentelemetry:opentelemetry-api:1.51.0")
@@ -26,10 +27,13 @@ dependencies {
2627
implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0")
2728
implementation("io.opentelemetry:opentelemetry-sdk-logs:1.51.0")
2829

30+
// TODO: Evaluate risks associated with incubator APIs
31+
implementation("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha")
32+
2933
// Android instrumentation
30-
implementation("io.opentelemetry.android:core:0.10.0-alpha")
31-
implementation("io.opentelemetry.android:instrumentation-activity:0.10.0-alpha")
32-
implementation("io.opentelemetry.android:session:0.10.0-alpha")
34+
implementation("io.opentelemetry.android:core:0.11.0-alpha")
35+
implementation("io.opentelemetry.android.instrumentation:activity:0.11.0-alpha")
36+
implementation("io.opentelemetry.android:session:0.11.0-alpha")
3337

3438
// Use JUnit Jupiter for testing.
3539
testImplementation("org.junit.jupiter:junit-jupiter")
@@ -42,10 +46,15 @@ android {
4246
namespace = "com.launchdarkly.observability"
4347
compileSdk = 30
4448

49+
buildFeatures {
50+
buildConfig = true
51+
}
52+
4553
defaultConfig {
4654
minSdk = 24
4755
version = releaseVersion
4856
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
57+
buildConfigField("String", "OBSERVABILITY_SDK_VERSION", "\"${project.version}\"")
4958
}
5059

5160
buildTypes {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.launchdarkly.observability.api
2+
3+
import com.launchdarkly.logging.LDLogAdapter
4+
import com.launchdarkly.observability.BuildConfig
5+
import com.launchdarkly.sdk.android.LDTimberLogging
6+
import io.opentelemetry.api.common.Attributes
7+
import kotlin.time.Duration
8+
import kotlin.time.Duration.Companion.minutes
9+
10+
private const val DEFAULT_OTLP_ENDPOINT = "https://otel.observability.app.launchdarkly.com:4318"
11+
private const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdarkly.com"
12+
13+
/**
14+
* Configuration options for the Observability plugin.
15+
*
16+
* @property serviceName The service name for the application. Defaults to the app package name if not set.
17+
* @property serviceVersion The version of the service. Defaults to the app version if not set.
18+
* @property otlpEndpoint The OTLP exporter endpoint. Defaults to LaunchDarkly endpoint.
19+
* @property backendUrl The backend URL for non-OTLP operations. Defaults to LaunchDarkly url.
20+
* @property resourceAttributes Additional resource attributes to include in telemetry data.
21+
* @property customHeaders Custom headers to include with OTLP exports.
22+
* @property sessionBackgroundTimeout Session timeout if app is backgrounded. Defaults to 15 minutes.
23+
* @property debug Enables verbose telemetry logging if true as well as other debug functionality. Defaults to false.
24+
* @property disableErrorTracking Disables error tracking if true. Defaults to false.
25+
* @property disableLogs Disables logs if true. Defaults to false.
26+
* @property disableTraces Disables traces if true. Defaults to false.
27+
* @property disableMetrics Disables metrics if true. Defaults to false.
28+
* @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.
29+
* @property loggerName The name of the logger to use. Defaults to "LaunchDarklyObservabilityPlugin".
30+
*/
31+
data class Options(
32+
val serviceName: String = "observability-android",
33+
val serviceVersion: String = BuildConfig.OBSERVABILITY_SDK_VERSION,
34+
val otlpEndpoint: String = DEFAULT_OTLP_ENDPOINT,
35+
val backendUrl: String = DEFAULT_BACKEND_URL,
36+
val resourceAttributes: Attributes = Attributes.empty(),
37+
val customHeaders: Map<String, String> = emptyMap(),
38+
val sessionBackgroundTimeout: Duration = 15.minutes,
39+
val debug: Boolean = false,
40+
// TODO O11Y-398: implement disable config options after all other instrumentations are implemented
41+
val disableErrorTracking: Boolean = false,
42+
val disableLogs: Boolean = false,
43+
val disableTraces: Boolean = false,
44+
val disableMetrics: Boolean = false,
45+
val logAdapter: LDLogAdapter = LDTimberLogging.adapter(), // this follows the LaunchDarkly SDK's default log adapter
46+
val loggerName: String = "LaunchDarklyObservabilityPlugin"
47+
)

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/InstrumentationManager.kt

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.launchdarkly.observability.client
22

33
import android.app.Application
4+
import com.launchdarkly.logging.LDLogger
5+
import com.launchdarkly.observability.api.Options
46
import com.launchdarkly.observability.interfaces.Metric
57
import io.opentelemetry.android.OpenTelemetryRum
8+
import io.opentelemetry.android.config.OtelRumConfig
9+
import io.opentelemetry.android.session.SessionConfig
610
import io.opentelemetry.api.common.Attributes
711
import io.opentelemetry.api.logs.Logger
12+
import io.opentelemetry.api.logs.Severity
813
import io.opentelemetry.api.metrics.Meter
914
import io.opentelemetry.api.trace.Span
1015
import io.opentelemetry.api.trace.Tracer
@@ -19,10 +24,9 @@ import io.opentelemetry.sdk.resources.Resource
1924
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor
2025
import java.util.concurrent.TimeUnit
2126

22-
private const val URL = "https://otel.observability.app.launchdarkly.com:4318"
23-
private const val URL_METRICS = URL + "/v1/metrics"
24-
private const val URL_LOGS = URL + "/v1/logs"
25-
private const val URL_TRACES = URL + "/v1/traces"
27+
private const val METRICS_PATH = "/v1/metrics"
28+
private const val LOGS_PATH = "/v1/logs"
29+
private const val TRACES_PATH = "/v1/traces"
2630
private const val INSTRUMENTATION_SCOPE_NAME = "com.launchdarkly.observability"
2731

2832
/**
@@ -34,21 +38,25 @@ class InstrumentationManager(
3438
private val application: Application,
3539
private val sdkKey: String,
3640
private val resources: Resource,
41+
private val logger: LDLogger,
42+
options: Options,
3743
) {
3844
private val otelRUM: OpenTelemetryRum
3945
private var otelMeter: Meter
4046
private var otelLogger: Logger
4147
private var otelTracer: Tracer
4248

4349
init {
44-
45-
otelRUM = OpenTelemetryRum.builder(application)
50+
val otelRumConfig = OtelRumConfig().setSessionConfig(
51+
SessionConfig(backgroundInactivityTimeout = options.sessionBackgroundTimeout)
52+
)
53+
otelRUM = OpenTelemetryRum.builder(application, otelRumConfig)
4654
.addLoggerProviderCustomizer { sdkLoggerProviderBuilder, application ->
4755
val logExporter = OtlpHttpLogRecordExporter.builder()
48-
.setEndpoint(URL_LOGS)
56+
.setEndpoint(options.otlpEndpoint + LOGS_PATH)
57+
.setHeaders { options.customHeaders }
4958
.build()
5059

51-
// TODO: support configuring these options via parameters
5260
val processor = BatchLogRecordProcessor.builder(logExporter)
5361
.setMaxQueueSize(100)
5462
.setScheduleDelay(1000, TimeUnit.MILLISECONDS)
@@ -59,10 +67,38 @@ class InstrumentationManager(
5967
sdkLoggerProviderBuilder
6068
.setResource(resources)
6169
.addLogRecordProcessor(processor)
70+
71+
if (options.debug) {
72+
val adapterLogExporter = object : io.opentelemetry.sdk.logs.export.LogRecordExporter {
73+
override fun export(logRecords: Collection<io.opentelemetry.sdk.logs.data.LogRecordData>): io.opentelemetry.sdk.common.CompletableResultCode {
74+
for (record in logRecords) {
75+
logger.info(record.toString()) // TODO: Figure out why logger.debug is being blocked by Log.isLoggable is adapter.
76+
}
77+
return io.opentelemetry.sdk.common.CompletableResultCode.ofSuccess()
78+
}
79+
override fun flush(): io.opentelemetry.sdk.common.CompletableResultCode {
80+
return io.opentelemetry.sdk.common.CompletableResultCode.ofSuccess()
81+
}
82+
override fun shutdown(): io.opentelemetry.sdk.common.CompletableResultCode {
83+
return io.opentelemetry.sdk.common.CompletableResultCode.ofSuccess()
84+
}
85+
}
86+
87+
val adapterProcessor = BatchLogRecordProcessor.builder(adapterLogExporter)
88+
.setMaxQueueSize(100)
89+
.setScheduleDelay(1000, TimeUnit.MILLISECONDS)
90+
.setExporterTimeout(5000, TimeUnit.MILLISECONDS)
91+
.setMaxExportBatchSize(10)
92+
.build()
93+
sdkLoggerProviderBuilder.addLogRecordProcessor(adapterProcessor)
94+
}
95+
96+
sdkLoggerProviderBuilder
6297
}
6398
.addTracerProviderCustomizer { sdkTracerProviderBuilder, application ->
6499
val spanExporter = OtlpHttpSpanExporter.builder()
65-
.setEndpoint(URL_TRACES)
100+
.setEndpoint(options.otlpEndpoint + TRACES_PATH)
101+
.setHeaders { options.customHeaders }
66102
.build()
67103

68104
val spanProcessor = BatchSpanProcessor.builder(spanExporter)
@@ -78,7 +114,8 @@ class InstrumentationManager(
78114
}
79115
.addMeterProviderCustomizer { sdkMeterProviderBuilder, application ->
80116
val metricExporter: MetricExporter = OtlpHttpMetricExporter.builder()
81-
.setEndpoint(URL_METRICS)
117+
.setEndpoint(options.otlpEndpoint + METRICS_PATH)
118+
.setHeaders { options.customHeaders }
82119
.build()
83120

84121
// Configure a periodic reader that pushes metrics every 10 seconds.
@@ -96,7 +133,6 @@ class InstrumentationManager(
96133
otelMeter = otelRUM.openTelemetry.meterProvider.get(INSTRUMENTATION_SCOPE_NAME)
97134
otelLogger = otelRUM.openTelemetry.logsBridge.get(INSTRUMENTATION_SCOPE_NAME)
98135
otelTracer = otelRUM.openTelemetry.tracerProvider.get(INSTRUMENTATION_SCOPE_NAME)
99-
otelRUM.rumSessionId
100136
}
101137

102138

@@ -126,16 +162,16 @@ class InstrumentationManager(
126162
.build().add(metric.value.toLong(), metric.attributes)
127163
}
128164

129-
fun recordLog(message: String, level: String, attributes: Attributes) {
165+
fun recordLog(message: String, severity: Severity, attributes: Attributes) {
130166
otelLogger.logRecordBuilder()
131167
.setBody(message)
132168
.setTimestamp(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
133-
.setSeverityText(level)
169+
.setSeverity(severity)
170+
.setSeverityText(severity.toString())
134171
.setAllAttributes(attributes)
135172
.emit()
136173
}
137174

138-
// TODO: add otel span optional param that will take precedence over current span and/or created span
139175
fun recordError(error: Error, attributes: Attributes) {
140176
val span = otelTracer
141177
.spanBuilder("highlight.error")
@@ -147,11 +183,6 @@ class InstrumentationManager(
147183
val attrBuilder = Attributes.builder()
148184
attrBuilder.putAll(attributes)
149185

150-
// TODO: should exception.cause be added here? At least one other SDK is doing this
151-
// error.cause?.let {
152-
// span.setAttribute("exception.cause", it.message)
153-
// }
154-
155186
span.recordException(error, attrBuilder.build())
156187
span.end()
157188
}

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityClient.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
package com.launchdarkly.observability.client
22

33
import android.app.Application
4-
import com.launchdarkly.observability.client.InstrumentationManager
4+
import com.launchdarkly.logging.LDLogger
5+
import com.launchdarkly.observability.api.Options
56
import com.launchdarkly.observability.interfaces.Metric
67
import com.launchdarkly.observability.interfaces.Observe
78
import io.opentelemetry.api.common.Attributes
9+
import io.opentelemetry.api.logs.Severity
810
import io.opentelemetry.api.trace.Span
911
import io.opentelemetry.sdk.resources.Resource
1012

11-
public class ObservabilityClient: Observe {
13+
class ObservabilityClient: Observe {
1214
private val instrumentationManager: InstrumentationManager
1315

1416
constructor(
1517
application: Application,
1618
sdkKey: String,
17-
resource: Resource
19+
resource: Resource,
20+
logger: LDLogger,
21+
options: Options
1822
) {
19-
this.instrumentationManager = InstrumentationManager(application, sdkKey, resource)
23+
this.instrumentationManager = InstrumentationManager(application, sdkKey, resource, logger, options)
2024
}
2125

2226
override fun recordMetric(metric: Metric) {
@@ -43,8 +47,8 @@ public class ObservabilityClient: Observe {
4347
instrumentationManager.recordError(error, attributes)
4448
}
4549

46-
override fun recordLog(message: String, level: String, attributes: Attributes) {
47-
instrumentationManager.recordLog(message, level, attributes)
50+
override fun recordLog(message: String, severity: Severity, attributes: Attributes) {
51+
instrumentationManager.recordLog(message, severity, attributes)
4852
}
4953

5054
override fun startSpan(name: String, attributes: Attributes): Span {

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/interfaces/Observe.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.launchdarkly.observability.interfaces
22

33
import io.opentelemetry.api.common.Attributes
4+
import io.opentelemetry.api.logs.Severity
45
import io.opentelemetry.api.trace.Span
56

67
/**
@@ -49,10 +50,10 @@ interface Observe {
4950
/**
5051
* Record a log message.
5152
* @param message The log message to record
52-
* @param level The level of the log message
53+
* @param severity The severity of the log message
5354
* @param attributes The attributes to record with the log message
5455
*/
55-
fun recordLog(message: String, level: String, attributes: Attributes)
56+
fun recordLog(message: String, severity: Severity, attributes: Attributes)
5657

5758
/**
5859
* Start a span.

0 commit comments

Comments
 (0)