diff --git a/e2e/android/app/src/main/java/com/example/androidobservability/BaseApplication.kt b/e2e/android/app/src/main/java/com/example/androidobservability/BaseApplication.kt index d81fed354..17ab3dacf 100644 --- a/e2e/android/app/src/main/java/com/example/androidobservability/BaseApplication.kt +++ b/e2e/android/app/src/main/java/com/example/androidobservability/BaseApplication.kt @@ -1,7 +1,7 @@ package com.example.androidobservability import android.app.Application -import com.launchdarkly.observability.api.Options +import com.launchdarkly.observability.api.ObservabilityOptions import com.launchdarkly.observability.client.TelemetryInspector import com.launchdarkly.observability.plugin.Observability import com.launchdarkly.observability.replay.PrivacyProfile @@ -24,11 +24,16 @@ open class BaseApplication : Application() { const val LAUNCHDARKLY_MOBILE_KEY = "MOBILE_KEY_GOES_HERE" } - var pluginOptions = Options( + var observabilityOptions = ObservabilityOptions( resourceAttributes = Attributes.of( AttributeKey.stringKey("example"), "value" ), debug = true, + tracesApi = ObservabilityOptions.TracesApi(includeErrors = false, includeSpans = false), + metricsApi = ObservabilityOptions.MetricsApi.disabled(), + instrumentations = ObservabilityOptions.Instrumentations( + crashReporting = true, launchTime = true, activityLifecycle = false + ), logAdapter = LDAndroidLogging.adapter(), ) @@ -39,7 +44,7 @@ open class BaseApplication : Application() { val observabilityPlugin = Observability( application = this@BaseApplication, mobileKey = LAUNCHDARKLY_MOBILE_KEY, - options = testUrl?.let { pluginOptions.copy(backendUrl = it, otlpEndpoint = it) } ?: pluginOptions + options = testUrl?.let { observabilityOptions.copy(backendUrl = it, otlpEndpoint = it) } ?: observabilityOptions ) val sessionReplayPlugin = SessionReplay( diff --git a/e2e/android/app/src/main/java/com/example/androidobservability/ViewModel.kt b/e2e/android/app/src/main/java/com/example/androidobservability/ViewModel.kt index ca83700bf..453338bea 100644 --- a/e2e/android/app/src/main/java/com/example/androidobservability/ViewModel.kt +++ b/e2e/android/app/src/main/java/com/example/androidobservability/ViewModel.kt @@ -54,7 +54,7 @@ class ViewModel(application: Application) : AndroidViewModel(application) { fun triggerLog() { LDObserve.recordLog( "Test Log", - Severity.DEBUG, + Severity.INFO, Attributes.of(AttributeKey.stringKey("FakeAttribute"), "FakeVal") ) } diff --git a/e2e/android/app/src/test/java/com/example/androidobservability/DisablingConfigOptionsE2ETest.kt b/e2e/android/app/src/test/java/com/example/androidobservability/DisablingConfigOptionsE2ETest.kt index 39eca3fa2..5b32dbb43 100644 --- a/e2e/android/app/src/test/java/com/example/androidobservability/DisablingConfigOptionsE2ETest.kt +++ b/e2e/android/app/src/test/java/com/example/androidobservability/DisablingConfigOptionsE2ETest.kt @@ -4,7 +4,7 @@ import android.app.Application import androidx.test.core.app.ApplicationProvider import com.example.androidobservability.TestUtils.TelemetryType import com.example.androidobservability.TestUtils.waitForTelemetryData -import com.launchdarkly.observability.api.Options +import com.launchdarkly.observability.api.ObservabilityOptions import com.launchdarkly.observability.interfaces.Metric import com.launchdarkly.observability.sdk.LDObserve import io.opentelemetry.api.common.AttributeKey @@ -27,12 +27,12 @@ class DisablingConfigOptionsE2ETest { private val application = ApplicationProvider.getApplicationContext() as TestApplication @Test - fun `Logs should NOT be exported when disableLogs is set to true`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableLogs = true) + fun `Logs should NOT be exported when logsApiLevel is NONE`() { + application.observabilityOptions = getOptionsAllEnabled().copy(logsApiLevel = ObservabilityOptions.LogLevel.NONE) application.initForTest() val logsUrl = "http://localhost:${application.mockWebServer?.port}/v1/logs" - triggerTestLog() + triggerTestLog(severity = Severity.TRACE) LDObserve.flush() waitForTelemetryData(telemetryInspector = application.telemetryInspector, telemetryType = TelemetryType.LOGS) val logsExported = application.telemetryInspector?.logExporter?.finishedLogRecordItems @@ -42,12 +42,27 @@ class DisablingConfigOptionsE2ETest { } @Test - fun `Logs should be exported when disableLogs is set to false`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableLogs = false) + fun `Logs should NOT be exported when log severity is lower than logsApiLevel`() { + application.observabilityOptions = getOptionsAllEnabled().copy(logsApiLevel = ObservabilityOptions.LogLevel.INFO) application.initForTest() val logsUrl = "http://localhost:${application.mockWebServer?.port}/v1/logs" - triggerTestLog() + triggerTestLog(severity = Severity.TRACE) + LDObserve.flush() + waitForTelemetryData(telemetryInspector = application.telemetryInspector, telemetryType = TelemetryType.LOGS) + val logsExported = application.telemetryInspector?.logExporter?.finishedLogRecordItems + + assertTrue(logsExported?.isEmpty() == true) + assertFalse(requestsContainsUrl(logsUrl)) + } + + @Test + fun `Logs should be exported when log severity is higher than logsApiLevel`() { + application.observabilityOptions = getOptionsAllEnabled().copy(logsApiLevel = ObservabilityOptions.LogLevel.INFO) + application.initForTest() + val logsUrl = "http://localhost:${application.mockWebServer?.port}/v1/logs" + + triggerTestLog(severity = Severity.WARN) LDObserve.flush() waitForTelemetryData(telemetryInspector = application.telemetryInspector, telemetryType = TelemetryType.LOGS) @@ -55,9 +70,28 @@ class DisablingConfigOptionsE2ETest { assertTrue(requestsContainsUrl(logsUrl)) } + + @Test + fun `Spans should NOT be exported when TracesApi is disabled`() { + application.observabilityOptions = getOptionsAllEnabled().copy(tracesApi = ObservabilityOptions.TracesApi.disabled()) + application.initForTest() + val tracesUrl = "http://localhost:${application.mockWebServer?.port}/v1/traces" + + triggerTestSpan() + LDObserve.flush() + + waitForTelemetryData(telemetryInspector = application.telemetryInspector, telemetryType = TelemetryType.SPANS) + val spansExported = application.telemetryInspector?.spanExporter?.finishedSpanItems + + assertTrue(spansExported?.isEmpty() == true) + assertFalse(requestsContainsUrl(tracesUrl)) + } + @Test - fun `Spans should NOT be exported when disableTraces is set to true`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableTraces = true) + fun `Spans should NOT be exported when TracesApi does not include spans`() { + application.observabilityOptions = getOptionsAllEnabled().copy( + tracesApi = ObservabilityOptions.TracesApi(includeSpans = false) + ) application.initForTest() val tracesUrl = "http://localhost:${application.mockWebServer?.port}/v1/traces" @@ -72,8 +106,8 @@ class DisablingConfigOptionsE2ETest { } @Test - fun `Spans should be exported when disableTraces is set to false`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableTraces = false) + fun `Spans should be exported when TracesApi is enabled`() { + application.observabilityOptions = getOptionsAllEnabled().copy(tracesApi = ObservabilityOptions.TracesApi.enabled()) application.initForTest() val tracesUrl = "http://localhost:${application.mockWebServer?.port}/v1/traces" @@ -86,8 +120,9 @@ class DisablingConfigOptionsE2ETest { } @Test - fun `Metrics should NOT be exported when disableMetrics is set to true`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableMetrics = true) + fun `Metrics should NOT be exported when disabled`() { + application.observabilityOptions = + getOptionsAllEnabled().copy(metricsApi = ObservabilityOptions.MetricsApi.disabled()) application.initForTest() val metricsUrl = "http://localhost:${application.mockWebServer?.port}/v1/metrics" @@ -99,8 +134,9 @@ class DisablingConfigOptionsE2ETest { } @Test - fun `Metrics should be exported when disableMetrics is set to false`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableMetrics = false) + fun `Metrics should be exported when enabled`() { + application.observabilityOptions = + getOptionsAllEnabled().copy(metricsApi = ObservabilityOptions.MetricsApi.enabled()) application.initForTest() val metricsUrl = "http://localhost:${application.mockWebServer?.port}/v1/metrics" @@ -112,8 +148,10 @@ class DisablingConfigOptionsE2ETest { } @Test - fun `Errors should NOT be exported when disableErrorTracking is set to true`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableErrorTracking = true) + fun `Errors should NOT be exported when TracesApi does not include errors`() { + application.observabilityOptions = getOptionsAllEnabled().copy( + tracesApi = ObservabilityOptions.TracesApi(includeErrors = false) + ) application.initForTest() val tracesUrl = "http://localhost:${application.mockWebServer?.port}/v1/traces" @@ -128,8 +166,10 @@ class DisablingConfigOptionsE2ETest { } @Test - fun `Errors should be exported as spans when disableErrorTracking is set to false and disableTraces set to true`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableTraces = true, disableErrorTracking = false) + fun `Errors should be exported as spans when TracesApi include errors but not spans`() { + application.observabilityOptions = getOptionsAllEnabled().copy( + tracesApi = ObservabilityOptions.TracesApi(includeErrors = true, includeSpans = false) + ) application.initForTest() val tracesUrl = "http://localhost:${application.mockWebServer?.port}/v1/traces" @@ -148,8 +188,10 @@ class DisablingConfigOptionsE2ETest { } @Test - fun `Crashes should NOT be exported when disableErrorTracking is set to true`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableErrorTracking = true) + fun `Crashes should NOT be exported when crashReporting instrumentation is disabled`() { + application.observabilityOptions = getOptionsAllEnabled().copy( + instrumentations = ObservabilityOptions.Instrumentations(crashReporting = false) + ) application.initForTest() val logsUrl = "http://localhost:${application.mockWebServer?.port}/v1/logs" @@ -163,8 +205,11 @@ class DisablingConfigOptionsE2ETest { } @Test - fun `Crashes should be exported as logs when disableErrorTracking is set to false and disableLogs set to true`() { - application.pluginOptions = getOptionsAllEnabled().copy(disableLogs = true, disableErrorTracking = false) + fun `Crashes should be exported as logs when crashReporting instrumentation is enabled logsApiLevel is NONE`() { + application.observabilityOptions = getOptionsAllEnabled().copy( + logsApiLevel = ObservabilityOptions.LogLevel.NONE, + instrumentations = ObservabilityOptions.Instrumentations(crashReporting = true) + ) application.initForTest() val logsUrl = "http://localhost:${application.mockWebServer?.port}/v1/logs" val exceptionMessage = "Exception for testing" @@ -189,10 +234,10 @@ class DisablingConfigOptionsE2ETest { } } - private fun triggerTestLog() { + private fun triggerTestLog(severity: Severity = Severity.INFO) { LDObserve.recordLog( message = "test-log", - severity = Severity.INFO, + severity = severity, attributes = Attributes.empty() ) } @@ -216,13 +261,17 @@ class DisablingConfigOptionsE2ETest { LDObserve.recordMetric(Metric("test", 50.0)) } - private fun getOptionsAllEnabled(): Options { - return Options( + private fun getOptionsAllEnabled(): ObservabilityOptions { + return ObservabilityOptions( debug = true, - disableTraces = false, - disableLogs = false, - disableMetrics = false, - disableErrorTracking = false + logsApiLevel = ObservabilityOptions.LogLevel.TRACE, + tracesApi = ObservabilityOptions.TracesApi.enabled(), + metricsApi = ObservabilityOptions.MetricsApi.enabled(), + instrumentations = ObservabilityOptions.Instrumentations( + crashReporting = true, + activityLifecycle = true, + launchTime = true + ) ) } } diff --git a/e2e/android/app/src/test/java/com/example/androidobservability/SamplingE2ETest.kt b/e2e/android/app/src/test/java/com/example/androidobservability/SamplingE2ETest.kt index 5cf672971..ca0fd6756 100644 --- a/e2e/android/app/src/test/java/com/example/androidobservability/SamplingE2ETest.kt +++ b/e2e/android/app/src/test/java/com/example/androidobservability/SamplingE2ETest.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.test.core.app.ApplicationProvider import com.example.androidobservability.TestUtils.TelemetryType import com.example.androidobservability.TestUtils.waitForTelemetryData +import com.launchdarkly.observability.api.ObservabilityOptions import com.launchdarkly.observability.client.TelemetryInspector import com.launchdarkly.observability.sdk.LDObserve import io.opentelemetry.api.common.AttributeKey @@ -49,6 +50,7 @@ class SamplingE2ETest { @Before fun setUp() { + application.observabilityOptions = getOptionsAllEnabled() application.initForTest() telemetryInspector = application.telemetryInspector } @@ -203,4 +205,18 @@ class SamplingE2ETest { ) span7.end() } + + private fun getOptionsAllEnabled(): ObservabilityOptions { + return ObservabilityOptions( + debug = true, + logsApiLevel = ObservabilityOptions.LogLevel.TRACE, + tracesApi = ObservabilityOptions.TracesApi.enabled(), + metricsApi = ObservabilityOptions.MetricsApi.enabled(), + instrumentations = ObservabilityOptions.Instrumentations( + crashReporting = true, + activityLifecycle = true, + launchTime = true + ) + ) + } } diff --git a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts index 21734786b..a909e5d1c 100644 --- a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts +++ b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts @@ -47,13 +47,13 @@ dependencies { // Testing exporters for telemetry inspection implementation("io.opentelemetry:opentelemetry-sdk-testing:1.51.0") - // Android instrumentation + // OTEL Android 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") - // Android crash instrumentation + // OTEL Android Instrumentations implementation("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") + implementation("io.opentelemetry.android.instrumentation:activity:0.11.0-alpha") // TODO: O11Y-626 - move replay instrumentation and associated compose dependencies into dedicated package // Compose dependencies for capture functionality diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/api/ObservabilityOptions.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/api/ObservabilityOptions.kt new file mode 100644 index 000000000..128c13cb2 --- /dev/null +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/api/ObservabilityOptions.kt @@ -0,0 +1,131 @@ +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 + +const val DEFAULT_SERVICE_NAME = "observability-android" +const val DEFAULT_OTLP_ENDPOINT = "https://otel.observability.app.launchdarkly.com:4318" +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 [DEFAULT_SERVICE_NAME]. + * @property serviceVersion The version of the service. Defaults to the SDK version. + * @property otlpEndpoint The OTLP exporter endpoint. Defaults to LaunchDarkly endpoint [DEFAULT_OTLP_ENDPOINT]. + * @property backendUrl The backend URL for non-OTLP operations. Defaults to LaunchDarkly url [DEFAULT_BACKEND_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 internal logging if true as well as other debug functionality. Defaults to false. + * @property logsApiLevel Level for logs to be exported. Defaults to INFO. Set to NONE to disable log exporting. + * @property tracesApi Options for configuring traces. See [TracesApi]. Tracing is enabled by default. + * @property metricsApi Options for configuring metrics. See [MetricsApi]. Metrics are enabled by default. + * @property instrumentations Options for configuring automatic instrumentations. See [Instrumentations]. + * @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 ObservabilityOptions( + val serviceName: String = DEFAULT_SERVICE_NAME, + 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 = emptyMap(), + val sessionBackgroundTimeout: Duration = 15.minutes, + val debug: Boolean = false, + val logsApiLevel: LogLevel = LogLevel.INFO, + val tracesApi: TracesApi = TracesApi.enabled(), + val metricsApi: MetricsApi = MetricsApi.enabled(), + val instrumentations: Instrumentations = Instrumentations(), + val logAdapter: LDLogAdapter = LDTimberLogging.adapter(), // This follows the LaunchDarkly SDK's default log adapter + val loggerName: String = "LaunchDarklyObservabilityPlugin", +){ + /** + * Options for configuring traces. + * + * @property includeErrors Whether to automatically instrument for and record errors and exceptions as spans. + * @property includeSpans Whether to automatically instrument for and record UI performance and other events as spans. + */ + data class TracesApi( + val includeErrors: Boolean = true, + val includeSpans: Boolean = true + ) { + companion object { + fun enabled() = TracesApi() + fun disabled() = TracesApi(includeErrors = false, includeSpans = false) + } + } + + /** + * Options for configuring metrics. + * + * @property enabled Whether to enable metrics. + */ + data class MetricsApi(val enabled: Boolean = true) { + companion object { + fun enabled() = MetricsApi(true) + fun disabled() = MetricsApi(false) + } + } + + /** + * This class allows enabling or disabling specific automatic instrumentations. + * + * @property crashReporting If `true`, the plugin will automatically report any uncaught exceptions as errors. + * @property launchTime If `true`, the plugin will automatically measure and report the application's startup time as metrics. + */ + data class Instrumentations( + val crashReporting: Boolean = true, + val activityLifecycle: Boolean = true, + val launchTime: Boolean = false, + ) + + /** + * Defines the logging levels for telemetry data. These levels correspond to the OpenTelemetry Log Severity. + * + * The levels are ordered by severity, from `TRACE` (least severe) to `FATAL` (most severe). + * Setting a `logsApiLevel` in [ObservabilityOptions] to a specific level means that + * logs of that level and all higher severity levels will be exported. + * + * For instance, setting the level to `INFO` will cause `INFO`, `WARN`, `ERROR`, and `FATAL` + * logs (and their variants) to be exported, while `TRACE` and `DEBUG` logs will be ignored. + * + * The `NONE` level can be used to disable log exporting entirely. + * + * @see OpenTelemetry Log Data Model - Severity + * + * @property level The integer representation of the log level, as defined by OpenTelemetry. + */ + enum class LogLevel(val level: Int) { + TRACE(1), + TRACE2(2), + TRACE3(3), + TRACE4(4), + DEBUG(5), + DEBUG2(6), + DEBUG3(7), + DEBUG4(8), + INFO(9), + INFO2(10), + INFO3(11), + INFO4(12), + WARN(13), + WARN2(14), + WARN3(15), + WARN4(16), + ERROR(17), + ERROR2(18), + ERROR3(19), + ERROR4(20), + FATAL(21), + FATAL2(22), + FATAL3(23), + FATAL4(24), + NONE(Int.MAX_VALUE) + } +} diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/api/Options.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/api/Options.kt deleted file mode 100644 index 4c3693e00..000000000 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/api/Options.kt +++ /dev/null @@ -1,47 +0,0 @@ -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 - -const val DEFAULT_SERVICE_NAME = "observability-android" -const val DEFAULT_OTLP_ENDPOINT = "https://otel.observability.app.launchdarkly.com:4318" -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 = DEFAULT_SERVICE_NAME, - 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 = emptyMap(), - val sessionBackgroundTimeout: Duration = 15.minutes, - val debug: Boolean = false, - 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", -) diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/InstrumentationManager.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/InstrumentationManager.kt index e2a955304..cdc762ce5 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/InstrumentationManager.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/InstrumentationManager.kt @@ -2,7 +2,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.api.ObservabilityOptions import com.launchdarkly.observability.coroutines.DispatcherProviderHolder import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation import com.launchdarkly.observability.interfaces.Metric @@ -13,8 +13,8 @@ import com.launchdarkly.observability.sampling.ExportSampler 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 io.opentelemetry.android.OpenTelemetryRum +import io.opentelemetry.android.OpenTelemetryRumBuilder import io.opentelemetry.android.config.OtelRumConfig import io.opentelemetry.android.session.SessionConfig import io.opentelemetry.api.common.Attributes @@ -63,7 +63,7 @@ import java.util.concurrent.TimeUnit * @param sdkKey The SDK key for authentication. * @param resources The OpenTelemetry resource describing this service. * @param logger The logger for internal logging. - * @param options Additional configuration options for the SDK. + * @param observabilityOptions Additional configuration options for the SDK. * @param instrumentations A list of custom instrumentations to be added. */ class InstrumentationManager( @@ -71,7 +71,7 @@ class InstrumentationManager( private val sdkKey: String, private val resources: Resource, private val logger: LDLogger, - private val options: Options, + private val observabilityOptions: ObservabilityOptions, private val instrumentations: List, ) { private val otelRUM: OpenTelemetryRum @@ -79,13 +79,12 @@ class InstrumentationManager( private var otelLogger: Logger private var otelTracer: Tracer private var customSampler = CustomSampler() - private val graphqlClient = GraphQLClient(options.backendUrl) + private val graphqlClient = GraphQLClient(observabilityOptions.backendUrl) private val samplingApiService = SamplingApiService(graphqlClient) private var telemetryInspector: TelemetryInspector? = null private var spanProcessor: SpanProcessor? = null private var logProcessor: LogRecordProcessor? = null private var metricsReader: PeriodicMetricReader? = null - private var launchTimeInstrumentation: LaunchTimeInstrumentation? = null private val gaugeCache = ConcurrentHashMap() private val counterCache = ConcurrentHashMap() private val histogramCache = ConcurrentHashMap() @@ -100,50 +99,32 @@ class InstrumentationManager( val rumBuilder = OpenTelemetryRum.builder(application, otelRumConfig) .addLoggerProviderCustomizer { sdkLoggerProviderBuilder, _ -> - // TODO: O11Y-627 - need to refactor this so that the disableLogs option is specific to core logging functionality. when logs are disabled, session replay logs should not be blocked - return@addLoggerProviderCustomizer if (options.disableLogs && options.disableErrorTracking) { - sdkLoggerProviderBuilder - } else { - val processor = createLoggerProcessor( - sdkLoggerProviderBuilder, - customSampler, - sdkKey, - resources, - logger, - telemetryInspector, - options, - instrumentations - ) - logProcessor = processor - sdkLoggerProviderBuilder.addLogRecordProcessor(processor) - } + val processor = createLoggerProcessor( + sdkLoggerProviderBuilder, + customSampler, + sdkKey, + resources, + logger, + telemetryInspector, + observabilityOptions, + instrumentations + ) + logProcessor = processor + return@addLoggerProviderCustomizer sdkLoggerProviderBuilder.addLogRecordProcessor(processor) } .addTracerProviderCustomizer { sdkTracerProviderBuilder, _ -> - return@addTracerProviderCustomizer if (options.disableTraces && options.disableErrorTracking) { - sdkTracerProviderBuilder - } else { - configureTracerProvider(sdkTracerProviderBuilder) - } + return@addTracerProviderCustomizer configureTracerProvider(sdkTracerProviderBuilder) } .addMeterProviderCustomizer { sdkMeterProviderBuilder, _ -> - return@addMeterProviderCustomizer if (options.disableMetrics) { - sdkMeterProviderBuilder - } else { - configureMeterProvider(sdkMeterProviderBuilder) - } + return@addMeterProviderCustomizer configureMeterProvider(sdkMeterProviderBuilder) } for (instrumentation in instrumentations) { rumBuilder.addInstrumentation(instrumentation) } - if (!options.disableMetrics) { - launchTimeInstrumentation = LaunchTimeInstrumentation( - application = application, - metricRecorder = this::recordHistogram - ).also { - rumBuilder.addInstrumentation(it) - } + if (observabilityOptions.instrumentations.launchTime) { + addLaunchTimeInstrumentation(rumBuilder) } otelRUM = rumBuilder.build() @@ -156,27 +137,37 @@ class InstrumentationManager( private fun createOtelRumConfig(): OtelRumConfig { val config = OtelRumConfig() - .setSessionConfig(SessionConfig(backgroundInactivityTimeout = options.sessionBackgroundTimeout)) + .setSessionConfig(SessionConfig(backgroundInactivityTimeout = observabilityOptions.sessionBackgroundTimeout)) - if (options.disableErrorTracking) { - // Disables Crash reporter - // "crash" is the instrumentation name defined in [io.opentelemetry.android.instrumentation.crash.CrashReporterInstrumentation.java] + if (!observabilityOptions.instrumentations.crashReporting) { + // Disables [io.opentelemetry.android.instrumentation.crash.CrashReporterInstrumentation.java] config.suppressInstrumentation("crash") } + if(!observabilityOptions.instrumentations.activityLifecycle) { + // Disables [io.opentelemetry.android.instrumentation.activity.ActivityLifecycleInstrumentation.java] + config.suppressInstrumentation("activity") + } + return config } + private fun addLaunchTimeInstrumentation(rumBuilder: OpenTelemetryRumBuilder) { + val launchTimeInstrumentation = LaunchTimeInstrumentation( + application = application, + metricRecorder = { metric -> + val histogram = histogramCache.getOrPut(metric.name) { + otelMeter.histogramBuilder(metric.name).build() + } + histogram.record(metric.value, metric.attributes.addSessionId()) + } + ) + rumBuilder.addInstrumentation(launchTimeInstrumentation) + } + private fun configureTracerProvider(sdkTracerProviderBuilder: SdkTracerProviderBuilder): SdkTracerProviderBuilder { val primarySpanExporter = createOtlpSpanExporter() - sdkTracerProviderBuilder - .setResource(resources) - .setSampler( - SpansSampler( - allowNormalSpans = !options.disableTraces, - allowErrorSpans = !options.disableErrorTracking - ) - ) + sdkTracerProviderBuilder.setResource(resources) val finalExporter = createSpanExporter(primarySpanExporter) val processor = createBatchSpanProcessor(finalExporter) @@ -199,21 +190,21 @@ class InstrumentationManager( private fun createOtlpSpanExporter(): SpanExporter { return OtlpHttpSpanExporter.builder() - .setEndpoint(options.otlpEndpoint + TRACES_PATH) - .setHeaders { options.customHeaders } + .setEndpoint(observabilityOptions.otlpEndpoint + TRACES_PATH) + .setHeaders { observabilityOptions.customHeaders } .build() } private fun createOtlpMetricExporter(): MetricExporter { return OtlpHttpMetricExporter.builder() - .setEndpoint(options.otlpEndpoint + METRICS_PATH) - .setHeaders { options.customHeaders } + .setEndpoint(observabilityOptions.otlpEndpoint + METRICS_PATH) + .setHeaders { observabilityOptions.customHeaders } .setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred()) .build() } private fun createSpanExporter(primaryExporter: SpanExporter): SpanExporter { - val baseExporter = if (options.debug) { + val baseExporter = if (observabilityOptions.debug) { SpanExporter.composite( buildList { add(primaryExporter) @@ -229,7 +220,7 @@ class InstrumentationManager( } private fun createMetricExporter(primaryExporter: MetricExporter): MetricExporter { - return if (options.debug) { + return if (observabilityOptions.debug) { CompositeMetricExporter( buildList { add(primaryExporter) @@ -250,7 +241,7 @@ class InstrumentationManager( } private fun initializeTelemetryInspector() { - if (options.debug) { + if (observabilityOptions.debug) { telemetryInspector = TelemetryInspector() } } @@ -275,6 +266,8 @@ class InstrumentationManager( } fun recordMetric(metric: Metric) { + if (!observabilityOptions.metricsApi.enabled) return + val gauge = gaugeCache.getOrPut(metric.name) { otelMeter.gaugeBuilder(metric.name).build() } @@ -282,6 +275,8 @@ class InstrumentationManager( } fun recordCount(metric: Metric) { + if (!observabilityOptions.metricsApi.enabled) return + // TODO: handle double casting to long better val counter = counterCache.getOrPut(metric.name) { otelMeter.counterBuilder(metric.name).build() @@ -290,6 +285,8 @@ class InstrumentationManager( } fun recordIncr(metric: Metric) { + if (!observabilityOptions.metricsApi.enabled) return + val counter = counterCache.getOrPut(metric.name) { otelMeter.counterBuilder(metric.name).build() } @@ -298,6 +295,8 @@ class InstrumentationManager( } fun recordHistogram(metric: Metric) { + if (!observabilityOptions.metricsApi.enabled) return + val histogram = histogramCache.getOrPut(metric.name) { otelMeter.histogramBuilder(metric.name).build() } @@ -305,13 +304,21 @@ class InstrumentationManager( } fun recordUpDownCounter(metric: Metric) { + if (!observabilityOptions.metricsApi.enabled) return + val upDownCounter = upDownCounterCache.getOrPut(metric.name) { otelMeter.upDownCounterBuilder(metric.name).build() } upDownCounter.add(metric.value.toLong(), metric.attributes.addSessionId()) } - fun recordLog(message: String, severity: Severity, attributes: Attributes) { + fun recordLog( + message: String, + severity: Severity, + attributes: Attributes + ) { + if (observabilityOptions.logsApiLevel.level > severity.severityNumber) return + otelLogger.logRecordBuilder() .setBody(message) .setTimestamp(System.currentTimeMillis(), TimeUnit.MILLISECONDS) @@ -322,21 +329,23 @@ class InstrumentationManager( } fun recordError(error: Error, attributes: Attributes) { - if (!options.disableErrorTracking) { - val span = otelTracer - .spanBuilder(ERROR_SPAN_NAME) - .setParent(Context.current().with(Span.current())) - .startSpan() + if (!observabilityOptions.tracesApi.includeErrors) return - val attrBuilder = Attributes.builder() - attrBuilder.putAll(attributes) + val span = otelTracer + .spanBuilder(ERROR_SPAN_NAME) + .setParent(Context.current().with(Span.current())) + .startSpan() - span.recordException(error, attrBuilder.build()) - span.end() - } + val attrBuilder = Attributes.builder() + attrBuilder.putAll(attributes) + + span.recordException(error, attrBuilder.build()) + span.end() } fun startSpan(name: String, attributes: Attributes): Span { + if (!observabilityOptions.tracesApi.includeSpans) return Span.getInvalid() + return otelTracer.spanBuilder(name) .setAllAttributes(attributes) .startSpan() @@ -409,28 +418,22 @@ class InstrumentationManager( resource: Resource, logger: LDLogger, telemetryInspector: TelemetryInspector?, - options: Options, + observabilityOptions: ObservabilityOptions, instrumentations: List, ): LogRecordProcessor { - val primaryLogExporter = createOtlpLogExporter(options) + val primaryLogExporter = createOtlpLogExporter(observabilityOptions) sdkLoggerProviderBuilder.setResource(resource) val finalExporter = createLogExporter( - primaryLogExporter, - logger, - telemetryInspector, - options + primaryExporter = primaryLogExporter, + logger = logger, + telemetryInspector = telemetryInspector, + observabilityOptions = observabilityOptions ) val samplingProcessor = SamplingLogProcessor( - createBatchLogRecordProcessor(finalExporter), - exportSampler - ) - - val baseProcessor = ConditionalLogRecordProcessor( - delegate = samplingProcessor, - allowNormalLogs = !options.disableLogs, - allowCrashes = !options.disableErrorTracking + delegate = createBatchLogRecordProcessor(finalExporter), + sampler = exportSampler ) /* @@ -440,7 +443,7 @@ class InstrumentationManager( originally added to route replay instrumentation logs through a separate log processing pipeline to provide instrumentation specific caching and export. */ - val routingLogRecordProcessor = RoutingLogRecordProcessor(fallthroughProcessor = baseProcessor) + val routingLogRecordProcessor = RoutingLogRecordProcessor(fallthroughProcessor = samplingProcessor) instrumentations.forEach { instrumentation -> instrumentation.getLogRecordProcessor(credential = sdkKey)?.let { processor -> instrumentation.getLoggerScopeName().let { scopeName -> @@ -452,10 +455,10 @@ class InstrumentationManager( return routingLogRecordProcessor } - private fun createOtlpLogExporter(options: Options): LogRecordExporter { + private fun createOtlpLogExporter(observabilityOptions: ObservabilityOptions): LogRecordExporter { return OtlpHttpLogRecordExporter.builder() - .setEndpoint(options.otlpEndpoint + LOGS_PATH) - .setHeaders { options.customHeaders } + .setEndpoint(observabilityOptions.otlpEndpoint + LOGS_PATH) + .setHeaders { observabilityOptions.customHeaders } .build() } @@ -463,9 +466,9 @@ class InstrumentationManager( primaryExporter: LogRecordExporter, logger: LDLogger, telemetryInspector: TelemetryInspector?, - options: Options + observabilityOptions: ObservabilityOptions ): LogRecordExporter { - return if (options.debug) { + return if (observabilityOptions.debug) { LogRecordExporter.composite( buildList { add(primaryExporter) diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityClient.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityClient.kt index e906b51d5..770de0e7f 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityClient.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityClient.kt @@ -2,7 +2,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.api.ObservabilityOptions import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation import com.launchdarkly.observability.interfaces.Metric import com.launchdarkly.observability.interfaces.Observe @@ -37,7 +37,7 @@ class ObservabilityClient : Observe { sdkKey: String, resource: Resource, logger: LDLogger, - options: Options, + options: ObservabilityOptions, instrumentations: List ) { this.instrumentationManager = InstrumentationManager( diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt index acbbe0e84..4a5f35800 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt @@ -2,14 +2,14 @@ package com.launchdarkly.observability.client import android.app.Application import com.launchdarkly.logging.LDLogger -import com.launchdarkly.observability.api.Options +import com.launchdarkly.observability.api.ObservabilityOptions /** * Shared information between plugins. */ data class ObservabilityContext( val sdkKey: String, - val options: Options, + val options: ObservabilityOptions, val application: Application, val logger: LDLogger, ) diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/plugin/Observability.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/plugin/Observability.kt index 913870771..47479fb34 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/plugin/Observability.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/plugin/Observability.kt @@ -5,7 +5,7 @@ import com.launchdarkly.logging.LDLogLevel import com.launchdarkly.logging.LDLogger import com.launchdarkly.logging.Logs import com.launchdarkly.observability.BuildConfig -import com.launchdarkly.observability.api.Options +import com.launchdarkly.observability.api.ObservabilityOptions import com.launchdarkly.observability.client.ObservabilityClient import com.launchdarkly.observability.client.ObservabilityContext import com.launchdarkly.observability.client.TelemetryInspector @@ -52,7 +52,7 @@ import java.util.Collections class Observability( private val application: Application, private val mobileKey: String, - private val options: Options = Options() // new instance has reasonable defaults + private val options: ObservabilityOptions = ObservabilityOptions() // new instance has reasonable defaults ) : Plugin() { private val logger: LDLogger private var observabilityClient: ObservabilityClient? = null diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/client/InstrumentationManagerTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/client/InstrumentationManagerTest.kt index 1fbece339..1c1cf8d9a 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/client/InstrumentationManagerTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/client/InstrumentationManagerTest.kt @@ -1,7 +1,7 @@ package com.launchdarkly.observability.client import com.launchdarkly.logging.LDLogger -import com.launchdarkly.observability.api.Options +import com.launchdarkly.observability.api.ObservabilityOptions import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation import com.launchdarkly.observability.sampling.ExportSampler import io.mockk.every @@ -26,7 +26,7 @@ class InstrumentationManagerTest { private lateinit var mockLogger: LDLogger private lateinit var testResource: Resource private lateinit var testSdkKey: String - private lateinit var testOptions: Options + private lateinit var testObservabilityOptions: ObservabilityOptions @BeforeEach fun setup() { @@ -35,7 +35,7 @@ class InstrumentationManagerTest { mockLogger = mockk(relaxed = true) testResource = Resource.create(Attributes.empty()) testSdkKey = "test-sdk-key" - testOptions = Options() + testObservabilityOptions = ObservabilityOptions() } @Test @@ -54,7 +54,7 @@ class InstrumentationManagerTest { every { mockInstrumentation2.getLoggerScopeName() } returns scopeName2 every { mockInstrumentation2.getLogRecordProcessor(testSdkKey) } returns mockLogRecordProcessor2 - testOptions = Options() + testObservabilityOptions = ObservabilityOptions() // Act val logProcessor = InstrumentationManager.createLoggerProcessor( @@ -64,7 +64,7 @@ class InstrumentationManagerTest { resource = testResource, logger = mockLogger, telemetryInspector = null, - options = testOptions, + observabilityOptions = testObservabilityOptions, instrumentations = listOf(mockInstrumentation1, mockInstrumentation2) ) @@ -90,7 +90,7 @@ class InstrumentationManagerTest { every { mockInstrumentation.getLoggerScopeName() } returns scopeName every { mockInstrumentation.getLogRecordProcessor(testSdkKey) } returns null - testOptions = Options() + testObservabilityOptions = ObservabilityOptions() // Act val logProcessor = InstrumentationManager.createLoggerProcessor( @@ -100,7 +100,7 @@ class InstrumentationManagerTest { resource = testResource, logger = mockLogger, telemetryInspector = null, - options = testOptions, + observabilityOptions = testObservabilityOptions, instrumentations = listOf(mockInstrumentation) ) @@ -119,7 +119,7 @@ class InstrumentationManagerTest { @Test fun `createLoggerProcessor should handle empty instrumentations list`() { // Arrange - testOptions = Options() + testObservabilityOptions = ObservabilityOptions() // Act val logProcessor = InstrumentationManager.createLoggerProcessor( @@ -129,7 +129,7 @@ class InstrumentationManagerTest { resource = testResource, logger = mockLogger, telemetryInspector = null, - options = testOptions, + observabilityOptions = testObservabilityOptions, instrumentations = listOf() ) diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt index 606296a12..c78ce13d9 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt @@ -1,7 +1,7 @@ package com.launchdarkly.observability.replay import com.launchdarkly.logging.LDLogger -import com.launchdarkly.observability.api.Options +import com.launchdarkly.observability.api.ObservabilityOptions import com.launchdarkly.observability.client.ObservabilityContext import com.launchdarkly.observability.plugin.InstrumentationContributorManager import com.launchdarkly.observability.sdk.LDObserve @@ -36,7 +36,7 @@ class SessionReplayTest { fun `register adds session replay when observability is initialized`() { LDObserve.context = ObservabilityContext( sdkKey = "test-sdk-key", - options = Options(), + options = ObservabilityOptions(), application = mockk(), logger = mockk(relaxed = true), ) @@ -61,7 +61,7 @@ class SessionReplayTest { fun `provideInstrumentations returns replay instrumentation if observability is initialized`() { LDObserve.context = ObservabilityContext( sdkKey = "test-sdk-key", - options = Options(), + options = ObservabilityOptions(), application = mockk(), logger = mockk(relaxed = true), )