Skip to content
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,3 @@ jobs:
with:
workspace-path: sdk/@launchdarkly/observability-android
aws-role-arn: ${{ vars.AWS_ROLE_ARN }}
dry-run: true
6 changes: 5 additions & 1 deletion e2e/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ android {
}

dependencies {
// Uncomment to use the local project
implementation(project(":observability-android"))
// 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("io.opentelemetry:opentelemetry-api:1.51.0")
Expand All @@ -48,7 +53,6 @@ dependencies {
implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0")

implementation("com.google.android.material:material:1.12.0")
implementation(project(":observability-android"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.example.androidobservability

import android.app.Application
import com.launchdarkly.observability.api.Options
import com.launchdarkly.sdk.ContextKind
import com.launchdarkly.sdk.LDContext
import com.launchdarkly.sdk.android.Components
import com.launchdarkly.sdk.android.LDClient
import com.launchdarkly.sdk.android.LDConfig
import com.launchdarkly.observability.plugin.Observability
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

class BaseApplication : Application() {
Expand All @@ -27,9 +31,22 @@ class BaseApplication : Application() {
// Use AutoEnvAttributes.Disabled as the argument to the Builder
val ldConfig = LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Enabled)
.mobileKey(LAUNCHDARKLY_MOBILE_KEY)
.plugins(Components.plugins().setPlugins(
Collections.singletonList<Plugin>(Observability(this@BaseApplication))
))
.plugins(
Components.plugins().setPlugins(
Collections.singletonList<Plugin>(
Observability(
this@BaseApplication,
Options(
resourceAttributes = Attributes.of(
AttributeKey.stringKey("example"), "value"
),
debug = true,
logAdapter = LDAndroidLogging.adapter(),
)
)
)
)
)
.build()

// Set up the context properties. This context should appear on your LaunchDarkly context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.launchdarkly.observability.sdk.LDObserve
import com.launchdarkly.sdk.android.LDClient
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.logs.Severity
import io.opentelemetry.api.trace.Span

class ViewModel : ViewModel() {
Expand All @@ -26,7 +27,7 @@ class ViewModel : ViewModel() {
fun triggerLog() {
LDObserve.recordLog(
"Test Log",
"debug",
Severity.DEBUG,
Attributes.of(AttributeKey.stringKey("FakeAttribute"), "FakeVal")
)
}
Expand All @@ -39,6 +40,7 @@ class ViewModel : ViewModel() {
}

fun triggerStopSpan() {
// TODO O11Y-397: for some reason stopped spans are stacking, the current span might be the problem
lastSpan?.end()
lastSpan = null
}
Expand Down
15 changes: 12 additions & 3 deletions sdk/@launchdarkly/observability-android/lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ allprojects {

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

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

// TODO: Evaluate risks associated with incubator APIs
implementation("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha")

// Android instrumentation
implementation("io.opentelemetry.android:core:0.10.0-alpha")
implementation("io.opentelemetry.android:instrumentation-activity:0.10.0-alpha")
implementation("io.opentelemetry.android:session:0.10.0-alpha")
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")

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

buildFeatures {
buildConfig = true
}

defaultConfig {
minSdk = 24
version = releaseVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "OBSERVABILITY_SDK_VERSION", "\"${project.version}\"")
}

buildTypes {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.launchdarkly.observability.api

import com.launchdarkly.logging.LDLogAdapter
import com.launchdarkly.observability.BuildConfig
import com.launchdarkly.sdk.android.LDTimberLogging
import io.opentelemetry.api.common.Attributes
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes

private const val DEFAULT_OTLP_ENDPOINT = "https://otel.observability.app.launchdarkly.com:4318"
private const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdarkly.com"

/**
* Configuration options for the Observability plugin.
*
* @property serviceName The service name for the application. Defaults to the app package name if not set.
* @property serviceVersion The version of the service. Defaults to the app version if not set.
* @property otlpEndpoint The OTLP exporter endpoint. Defaults to LaunchDarkly endpoint.
* @property backendUrl The backend URL for non-OTLP operations. Defaults to LaunchDarkly url.
* @property resourceAttributes Additional resource attributes to include in telemetry data.
* @property customHeaders Custom headers to include with OTLP exports.
* @property sessionBackgroundTimeout Session timeout if app is backgrounded. Defaults to 15 minutes.
* @property debug Enables verbose telemetry logging if true as well as other debug functionality. Defaults to false.
* @property disableErrorTracking Disables error tracking if true. Defaults to false.
* @property disableLogs Disables logs if true. Defaults to false.
* @property disableTraces Disables traces if true. Defaults to false.
* @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".
*/
data class Options(
val serviceName: String = "observability-android",
val serviceVersion: String = BuildConfig.OBSERVABILITY_SDK_VERSION,
val otlpEndpoint: String = DEFAULT_OTLP_ENDPOINT,
val backendUrl: String = DEFAULT_BACKEND_URL,
val resourceAttributes: Attributes = Attributes.empty(),
val customHeaders: Map<String, String> = emptyMap(),
val sessionBackgroundTimeout: Duration = 15.minutes,
val debug: Boolean = false,
// TODO O11Y-398: implement disable config options after all other instrumentations are implemented
val disableErrorTracking: Boolean = false,
val disableLogs: Boolean = false,
val disableTraces: Boolean = false,
val disableMetrics: Boolean = false,
val logAdapter: LDLogAdapter = LDTimberLogging.adapter(), // this follows the LaunchDarkly SDK's default log adapter
val loggerName: String = "LaunchDarklyObservabilityPlugin"
)
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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.Metric
import io.opentelemetry.android.OpenTelemetryRum
import io.opentelemetry.android.config.OtelRumConfig
import io.opentelemetry.android.session.SessionConfig
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.logs.Logger
import io.opentelemetry.api.logs.Severity
import io.opentelemetry.api.metrics.Meter
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.Tracer
Expand All @@ -19,10 +24,9 @@ import io.opentelemetry.sdk.resources.Resource
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor
import java.util.concurrent.TimeUnit

private const val URL = "https://otel.observability.app.launchdarkly.com:4318"
private const val URL_METRICS = URL + "/v1/metrics"
private const val URL_LOGS = URL + "/v1/logs"
private const val URL_TRACES = URL + "/v1/traces"
private const val METRICS_PATH = "/v1/metrics"
private const val LOGS_PATH = "/v1/logs"
private const val TRACES_PATH = "/v1/traces"
private const val INSTRUMENTATION_SCOPE_NAME = "com.launchdarkly.observability"

/**
Expand All @@ -34,21 +38,25 @@ class InstrumentationManager(
private val application: Application,
private val sdkKey: String,
private val resources: Resource,
private val logger: LDLogger,
options: Options,
) {
private val otelRUM: OpenTelemetryRum
private var otelMeter: Meter
private var otelLogger: Logger
private var otelTracer: Tracer

init {

otelRUM = OpenTelemetryRum.builder(application)
val otelRumConfig = OtelRumConfig().setSessionConfig(
SessionConfig(backgroundInactivityTimeout = options.sessionBackgroundTimeout)
)
otelRUM = OpenTelemetryRum.builder(application, otelRumConfig)
.addLoggerProviderCustomizer { sdkLoggerProviderBuilder, application ->
val logExporter = OtlpHttpLogRecordExporter.builder()
.setEndpoint(URL_LOGS)
.setEndpoint(options.otlpEndpoint + LOGS_PATH)
.setHeaders { options.customHeaders }
.build()

// TODO: support configuring these options via parameters
val processor = BatchLogRecordProcessor.builder(logExporter)
.setMaxQueueSize(100)
.setScheduleDelay(1000, TimeUnit.MILLISECONDS)
Expand All @@ -59,10 +67,38 @@ class InstrumentationManager(
sdkLoggerProviderBuilder
.setResource(resources)
.addLogRecordProcessor(processor)

if (options.debug) {
val adapterLogExporter = object : io.opentelemetry.sdk.logs.export.LogRecordExporter {
override fun export(logRecords: Collection<io.opentelemetry.sdk.logs.data.LogRecordData>): io.opentelemetry.sdk.common.CompletableResultCode {
for (record in logRecords) {
logger.info(record.toString()) // TODO: Figure out why logger.debug is being blocked by Log.isLoggable is adapter.
}
return io.opentelemetry.sdk.common.CompletableResultCode.ofSuccess()
}
override fun flush(): io.opentelemetry.sdk.common.CompletableResultCode {
return io.opentelemetry.sdk.common.CompletableResultCode.ofSuccess()
}
override fun shutdown(): io.opentelemetry.sdk.common.CompletableResultCode {
return io.opentelemetry.sdk.common.CompletableResultCode.ofSuccess()
}
}

val adapterProcessor = BatchLogRecordProcessor.builder(adapterLogExporter)
.setMaxQueueSize(100)
.setScheduleDelay(1000, TimeUnit.MILLISECONDS)
.setExporterTimeout(5000, TimeUnit.MILLISECONDS)
.setMaxExportBatchSize(10)
.build()
sdkLoggerProviderBuilder.addLogRecordProcessor(adapterProcessor)
}

sdkLoggerProviderBuilder
}
.addTracerProviderCustomizer { sdkTracerProviderBuilder, application ->
val spanExporter = OtlpHttpSpanExporter.builder()
.setEndpoint(URL_TRACES)
.setEndpoint(options.otlpEndpoint + TRACES_PATH)
.setHeaders { options.customHeaders }
.build()

val spanProcessor = BatchSpanProcessor.builder(spanExporter)
Expand All @@ -78,7 +114,8 @@ class InstrumentationManager(
}
.addMeterProviderCustomizer { sdkMeterProviderBuilder, application ->
val metricExporter: MetricExporter = OtlpHttpMetricExporter.builder()
.setEndpoint(URL_METRICS)
.setEndpoint(options.otlpEndpoint + METRICS_PATH)
.setHeaders { options.customHeaders }
.build()

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


Expand Down Expand Up @@ -126,16 +162,16 @@ class InstrumentationManager(
.build().add(metric.value.toLong(), metric.attributes)
}

fun recordLog(message: String, level: String, attributes: Attributes) {
fun recordLog(message: String, severity: Severity, attributes: Attributes) {
otelLogger.logRecordBuilder()
.setBody(message)
.setTimestamp(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setSeverityText(level)
.setSeverity(severity)
.setSeverityText(severity.toString())
.setAllAttributes(attributes)
.emit()
}

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

// TODO: should exception.cause be added here? At least one other SDK is doing this
// error.cause?.let {
// span.setAttribute("exception.cause", it.message)
// }

span.recordException(error, attrBuilder.build())
span.end()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package com.launchdarkly.observability.client

import android.app.Application
import com.launchdarkly.observability.client.InstrumentationManager
import com.launchdarkly.logging.LDLogger
import com.launchdarkly.observability.api.Options
import com.launchdarkly.observability.interfaces.Metric
import com.launchdarkly.observability.interfaces.Observe
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.logs.Severity
import io.opentelemetry.api.trace.Span
import io.opentelemetry.sdk.resources.Resource

public class ObservabilityClient: Observe {
class ObservabilityClient: Observe {
private val instrumentationManager: InstrumentationManager

constructor(
application: Application,
sdkKey: String,
resource: Resource
resource: Resource,
logger: LDLogger,
options: Options
) {
this.instrumentationManager = InstrumentationManager(application, sdkKey, resource)
this.instrumentationManager = InstrumentationManager(application, sdkKey, resource, logger, options)
}

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

override fun recordLog(message: String, level: String, attributes: Attributes) {
instrumentationManager.recordLog(message, level, attributes)
override fun recordLog(message: String, severity: Severity, attributes: Attributes) {
instrumentationManager.recordLog(message, severity, attributes)
}

override fun startSpan(name: String, attributes: Attributes): Span {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.launchdarkly.observability.interfaces

import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.logs.Severity
import io.opentelemetry.api.trace.Span

/**
Expand Down Expand Up @@ -49,10 +50,10 @@ interface Observe {
/**
* Record a log message.
* @param message The log message to record
* @param level The level of the log message
* @param severity The severity of the log message
* @param attributes The attributes to record with the log message
*/
fun recordLog(message: String, level: String, attributes: Attributes)
fun recordLog(message: String, severity: Severity, attributes: Attributes)

/**
* Start a span.
Expand Down
Loading
Loading