diff --git a/android-agent/api/android-agent.api b/android-agent/api/android-agent.api index 082e6f709..1cd2afff3 100644 --- a/android-agent/api/android-agent.api +++ b/android-agent/api/android-agent.api @@ -14,6 +14,13 @@ public final class io/opentelemetry/android/agent/connectivity/Compression : jav public static fun values ()[Lio/opentelemetry/android/agent/connectivity/Compression; } +public final class io/opentelemetry/android/agent/connectivity/ExportProtocol : java/lang/Enum { + public static final field GRPC Lio/opentelemetry/android/agent/connectivity/ExportProtocol; + public static final field HTTP Lio/opentelemetry/android/agent/connectivity/ExportProtocol; + public static fun valueOf (Ljava/lang/String;)Lio/opentelemetry/android/agent/connectivity/ExportProtocol; + public static fun values ()[Lio/opentelemetry/android/agent/connectivity/ExportProtocol; +} + public final class io/opentelemetry/android/agent/dsl/DiskBufferingConfigurationSpec : io/opentelemetry/android/agent/dsl/instrumentation/CanBeEnabledAndDisabled { public fun enabled (Z)V } @@ -27,6 +34,32 @@ public final class io/opentelemetry/android/agent/dsl/EndpointConfiguration { public final fun setUrl (Ljava/lang/String;)V } +public final class io/opentelemetry/android/agent/dsl/ExportConfiguration { + public final fun getCompression ()Lio/opentelemetry/android/agent/connectivity/Compression; + public final fun getEndpoint ()Ljava/lang/String; + public final fun getHeaders ()Ljava/util/Map; + public final fun getProtocol ()Lio/opentelemetry/android/agent/connectivity/ExportProtocol; + public final fun logs (Lkotlin/jvm/functions/Function1;)V + public final fun metrics (Lkotlin/jvm/functions/Function1;)V + public final fun setCompression (Lio/opentelemetry/android/agent/connectivity/Compression;)V + public final fun setEndpoint (Ljava/lang/String;)V + public final fun setHeaders (Ljava/util/Map;)V + public final fun setProtocol (Lio/opentelemetry/android/agent/connectivity/ExportProtocol;)V + public final fun spans (Lkotlin/jvm/functions/Function1;)V +} + +public final class io/opentelemetry/android/agent/dsl/GrpcExportConfiguration { + public final fun getCompression ()Lio/opentelemetry/android/agent/connectivity/Compression; + public final fun getEndpoint ()Ljava/lang/String; + public final fun getHeaders ()Ljava/util/Map; + public final fun logs (Lkotlin/jvm/functions/Function1;)V + public final fun metrics (Lkotlin/jvm/functions/Function1;)V + public final fun setCompression (Lio/opentelemetry/android/agent/connectivity/Compression;)V + public final fun setEndpoint (Ljava/lang/String;)V + public final fun setHeaders (Ljava/util/Map;)V + public final fun spans (Lkotlin/jvm/functions/Function1;)V +} + public final class io/opentelemetry/android/agent/dsl/HttpExportConfiguration { public final fun getBaseHeaders ()Ljava/util/Map; public final fun getBaseUrl ()Ljava/lang/String; @@ -42,8 +75,10 @@ public final class io/opentelemetry/android/agent/dsl/HttpExportConfiguration { public final class io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration { public fun ()V public final fun diskBuffering (Lkotlin/jvm/functions/Function1;)V + public final fun export (Lkotlin/jvm/functions/Function1;)V public final fun getClock ()Lio/opentelemetry/sdk/common/Clock; public final fun globalAttributes (Lkotlin/jvm/functions/Function0;)V + public final fun grpcExport (Lkotlin/jvm/functions/Function1;)V public final fun httpExport (Lkotlin/jvm/functions/Function1;)V public final fun instrumentations (Lkotlin/jvm/functions/Function1;)V public final fun resource (Lkotlin/jvm/functions/Function1;)V diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt index e6d7ac1ab..1d34fa06f 100644 --- a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt @@ -12,6 +12,8 @@ import io.opentelemetry.android.Incubating import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.RumBuilder import io.opentelemetry.android.agent.connectivity.Compression +import io.opentelemetry.android.agent.connectivity.EndpointConnectivity +import io.opentelemetry.android.agent.connectivity.ExportProtocol import io.opentelemetry.android.agent.dsl.OpenTelemetryConfiguration import io.opentelemetry.android.agent.session.SessionConfig import io.opentelemetry.android.agent.session.SessionIdTimeoutHandler @@ -21,19 +23,15 @@ import io.opentelemetry.android.session.SessionProvider import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter +import io.opentelemetry.sdk.logs.export.LogRecordExporter +import io.opentelemetry.sdk.metrics.export.MetricExporter +import io.opentelemetry.sdk.trace.export.SpanExporter @OptIn(Incubating::class) object OpenTelemetryRumInitializer { - /** - * Opinionated [io.opentelemetry.android.OpenTelemetryRum] initialization. - * - * @param context Your android app's application context. This should be from your Application - * subclass or an appropriate context that allows retrieving the application context. If you - * supply an inappropriate context (e.g. from attachBaseContext) then instrumentation relying - * on activity lifecycle callbacks will not function correctly. - * @param configuration Type-safe config DSL that controls how OpenTelemetry - * should behave. - */ @JvmStatic fun initialize( context: Context, @@ -42,18 +40,13 @@ object OpenTelemetryRumInitializer { val cfg = OpenTelemetryConfiguration() configuration(cfg) - // ensure we're using the Application Context to prevent potential leaks. - // if context.applicationContext is null (e.g. called from within attachBaseContext), - // fallback to the supplied context. val ctx = when (context) { is Application -> context else -> context.applicationContext ?: context } - val spansEndpoint = cfg.exportConfig.spansEndpoint() - val logsEndpoints = cfg.exportConfig.logsEndpoint() - val metricsEndpoint = cfg.exportConfig.metricsEndpoint() + val exportEndpoints = resolveExportEndpoints(cfg) val resourceBuilder = AndroidResource.createDefault(ctx).toBuilder() cfg.resourceAction(resourceBuilder) @@ -65,28 +58,119 @@ object OpenTelemetryRumInitializer { .setResource(resource) .setClock(cfg.clock) .addSpanExporterCustomizer { + createSpanExporter(exportEndpoints.spans, exportEndpoints.protocol) + }.addLogRecordExporterCustomizer { + createLogRecordExporter(exportEndpoints.logs, exportEndpoints.protocol) + }.addMetricExporterCustomizer { + createMetricExporter(exportEndpoints.metrics, exportEndpoints.protocol) + }.build() + } + + private data class ExportEndpoints( + val spans: EndpointConnectivity, + val logs: EndpointConnectivity, + val metrics: EndpointConnectivity, + val protocol: ExportProtocol, + ) + + private fun resolveExportEndpoints(cfg: OpenTelemetryConfiguration): ExportEndpoints { + cfg.unifiedExportConfig?.let { unified -> + return ExportEndpoints( + spans = unified.spansEndpoint(), + logs = unified.logsEndpoint(), + metrics = unified.metricsEndpoint(), + protocol = unified.protocol, + ) + } + + cfg.grpcExportConfig?.let { grpc -> + return ExportEndpoints( + spans = grpc.spansEndpoint(), + logs = grpc.logsEndpoint(), + metrics = grpc.metricsEndpoint(), + protocol = ExportProtocol.GRPC, + ) + } + + return ExportEndpoints( + spans = cfg.exportConfig.spansEndpoint(), + logs = cfg.exportConfig.logsEndpoint(), + metrics = cfg.exportConfig.metricsEndpoint(), + protocol = ExportProtocol.HTTP, + ) + } + + private fun createSpanExporter( + endpoint: EndpointConnectivity, + protocol: ExportProtocol, + ): SpanExporter = + when (protocol) { + ExportProtocol.HTTP -> { OtlpHttpSpanExporter .builder() - .setEndpoint(spansEndpoint.getUrl()) - .setHeaders(spansEndpoint::getHeaders) - .setCompression(spansEndpoint.getCompression().getUpstreamName()) + .setEndpoint(endpoint.getUrl()) + .setHeaders(endpoint::getHeaders) + .setCompression(endpoint.getCompression().getUpstreamName()) .build() - }.addLogRecordExporterCustomizer { + } + + ExportProtocol.GRPC -> { + OtlpGrpcSpanExporter + .builder() + .setEndpoint(endpoint.getUrl()) + .setHeaders(endpoint::getHeaders) + .setCompression(endpoint.getCompression().getUpstreamName()) + .build() + } + } + + private fun createLogRecordExporter( + endpoint: EndpointConnectivity, + protocol: ExportProtocol, + ): LogRecordExporter = + when (protocol) { + ExportProtocol.HTTP -> { OtlpHttpLogRecordExporter .builder() - .setEndpoint(logsEndpoints.getUrl()) - .setHeaders(logsEndpoints::getHeaders) - .setCompression(logsEndpoints.getCompression().getUpstreamName()) + .setEndpoint(endpoint.getUrl()) + .setHeaders(endpoint::getHeaders) + .setCompression(endpoint.getCompression().getUpstreamName()) .build() - }.addMetricExporterCustomizer { + } + + ExportProtocol.GRPC -> { + OtlpGrpcLogRecordExporter + .builder() + .setEndpoint(endpoint.getUrl()) + .setHeaders(endpoint::getHeaders) + .setCompression(endpoint.getCompression().getUpstreamName()) + .build() + } + } + + private fun createMetricExporter( + endpoint: EndpointConnectivity, + protocol: ExportProtocol, + ): MetricExporter = + when (protocol) { + ExportProtocol.HTTP -> { OtlpHttpMetricExporter .builder() - .setEndpoint(metricsEndpoint.getUrl()) - .setHeaders(metricsEndpoint::getHeaders) - .setCompression(metricsEndpoint.getCompression().getUpstreamName()) + .setEndpoint(endpoint.getUrl()) + .setHeaders(endpoint::getHeaders) + .setCompression(endpoint.getCompression().getUpstreamName()) .build() - }.build() - } + } + + ExportProtocol.GRPC -> { + OtlpGrpcMetricExporter + .builder() + .setEndpoint(endpoint.getUrl()) + .setHeaders(endpoint::getHeaders) + .setCompression(endpoint.getCompression().getUpstreamName()) + .build() + } + } private fun Compression.getUpstreamName(): String = when (this) { diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/connectivity/ExportProtocol.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/connectivity/ExportProtocol.kt new file mode 100644 index 000000000..a712dcdbc --- /dev/null +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/connectivity/ExportProtocol.kt @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.connectivity + +enum class ExportProtocol { + HTTP, + GRPC, +} diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/connectivity/GrpcEndpointConnectivity.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/connectivity/GrpcEndpointConnectivity.kt new file mode 100644 index 000000000..bc1815088 --- /dev/null +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/connectivity/GrpcEndpointConnectivity.kt @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.connectivity + +internal class GrpcEndpointConnectivity private constructor( + private val endpoint: String, + private val headers: Map, + private val compression: Compression, +) : EndpointConnectivity { + companion object { + fun create( + endpoint: String, + headers: Map, + compression: Compression, + ): GrpcEndpointConnectivity = GrpcEndpointConnectivity(endpoint, headers, compression) + } + + override fun getUrl(): String = endpoint + + override fun getHeaders(): Map = headers + + override fun getCompression(): Compression = compression +} diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/ExportConfiguration.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/ExportConfiguration.kt new file mode 100644 index 000000000..8727d3a97 --- /dev/null +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/ExportConfiguration.kt @@ -0,0 +1,149 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.dsl + +import io.opentelemetry.android.agent.connectivity.Compression +import io.opentelemetry.android.agent.connectivity.EndpointConnectivity +import io.opentelemetry.android.agent.connectivity.ExportProtocol +import io.opentelemetry.android.agent.connectivity.GrpcEndpointConnectivity +import io.opentelemetry.android.agent.connectivity.HttpEndpointConnectivity + +/** + * Unified configuration for exporting telemetry that supports both HTTP and gRPC protocols. + * + * This is the preferred way to configure telemetry export as it provides a single entry point + * for both transport protocols. Use [protocol] to select the transport, and [endpoint], [headers], + * and [compression] for common settings. Use [spans], [logs], and [metrics] to override + * per-signal configuration as needed. + */ +@OpenTelemetryDslMarker +class ExportConfiguration internal constructor() { + /** + * Export protocol to use for all telemetry signals. + * + * This value controls whether HTTP or gRPC exporters are created. Defaults to [ExportProtocol.HTTP]. + */ + var protocol: ExportProtocol = ExportProtocol.HTTP + + /** + * Default endpoint URL used for all telemetry signals. + * + * Each signal ([spans], [logs], [metrics]) can define its own endpoint. + * If a signal-specific endpoint is blank, this global endpoint is used instead. + */ + var endpoint: String = "" + + /** + * Global headers applied to all telemetry exports. + * + * These headers are merged with signal-specific headers configured via + * [EndpointConfiguration.headers]. + */ + var headers: Map = emptyMap() + + /** + * Default compression algorithm for all telemetry signals. + * + * A signal can override this by setting a non-null compression in its + * [EndpointConfiguration]. + */ + var compression: Compression = Compression.GZIP + + private val spansConfig: EndpointConfiguration = EndpointConfiguration("") + private val logsConfig: EndpointConfiguration = EndpointConfiguration("") + private val metricsConfig: EndpointConfiguration = EndpointConfiguration("") + + internal fun spansEndpoint(): EndpointConnectivity = + when (protocol) { + ExportProtocol.HTTP -> { + HttpEndpointConnectivity.forTraces( + chooseEndpoint(spansConfig), + spansConfig.headers + headers, + chooseCompression(spansConfig.compression), + ) + } + + ExportProtocol.GRPC -> { + GrpcEndpointConnectivity.create( + chooseEndpoint(spansConfig), + spansConfig.headers + headers, + chooseCompression(spansConfig.compression), + ) + } + } + + internal fun logsEndpoint(): EndpointConnectivity = + when (protocol) { + ExportProtocol.HTTP -> { + HttpEndpointConnectivity.forLogs( + chooseEndpoint(logsConfig), + logsConfig.headers + headers, + chooseCompression(logsConfig.compression), + ) + } + + ExportProtocol.GRPC -> { + GrpcEndpointConnectivity.create( + chooseEndpoint(logsConfig), + logsConfig.headers + headers, + chooseCompression(logsConfig.compression), + ) + } + } + + internal fun metricsEndpoint(): EndpointConnectivity = + when (protocol) { + ExportProtocol.HTTP -> { + HttpEndpointConnectivity.forMetrics( + chooseEndpoint(metricsConfig), + metricsConfig.headers + headers, + chooseCompression(metricsConfig.compression), + ) + } + + ExportProtocol.GRPC -> { + GrpcEndpointConnectivity.create( + chooseEndpoint(metricsConfig), + metricsConfig.headers + headers, + chooseCompression(metricsConfig.compression), + ) + } + } + + private fun chooseEndpoint(cfg: EndpointConfiguration): String = cfg.url.ifBlank { endpoint } + + private fun chooseCompression(signalConfigCompression: Compression?): Compression = signalConfigCompression ?: this.compression + + /** + * Configures export options specific to span data. + * + * Values set in this configuration override the top-level [endpoint], + * [headers], and [compression] for span exports. + */ + fun spans(action: EndpointConfiguration.() -> Unit) { + spansConfig.action() + } + + /** + * Configures export options specific to log data. + * + * Values set in this configuration override the top-level [endpoint], + * [headers], and [compression] for log exports. + */ + fun logs(action: EndpointConfiguration.() -> Unit) { + logsConfig.action() + } + + /** + * Configures export options specific to metric data. + * + * Values set in this configuration override the top-level [endpoint], + * [headers], and [compression] for metric exports. + */ + fun metrics(action: EndpointConfiguration.() -> Unit) { + metricsConfig.action() + } +} diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/GrpcExportConfiguration.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/GrpcExportConfiguration.kt new file mode 100644 index 000000000..cbf5a47ad --- /dev/null +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/GrpcExportConfiguration.kt @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.dsl + +import io.opentelemetry.android.agent.connectivity.Compression +import io.opentelemetry.android.agent.connectivity.GrpcEndpointConnectivity + +/** + * Configuration for exporting telemetry over gRPC. + * + * Use [endpoint], [headers], and [compression] for common settings, and + * [spans], [logs], and [metrics] to override per-signal configuration as needed. + */ +@OpenTelemetryDslMarker +class GrpcExportConfiguration internal constructor() { + /** + * The base gRPC endpoint to which telemetry will be exported. + * + * This value is used for all signals (spans, logs, metrics) unless a + * signal-specific endpoint is configured via [spans], [logs], or [metrics]. + */ + var endpoint: String = "" + + /** + * Headers that will be sent with every gRPC export request. + * + * These headers are combined with any headers configured on the per-signal + * [EndpointConfiguration] instances used by [spans], [logs], and [metrics]. + */ + var headers: Map = emptyMap() + + /** + * The compression algorithm to use for gRPC export requests. + * + * This acts as the default compression for all signals and may be overridden + * in the per-signal [EndpointConfiguration]. + */ + var compression: Compression = Compression.GZIP + + private val spansConfig: EndpointConfiguration = EndpointConfiguration("") + private val logsConfig: EndpointConfiguration = EndpointConfiguration("") + private val metricsConfig: EndpointConfiguration = EndpointConfiguration("") + + internal fun spansEndpoint(): GrpcEndpointConnectivity = + GrpcEndpointConnectivity.create( + chooseEndpoint(spansConfig), + spansConfig.headers + headers, + chooseCompression(spansConfig.compression), + ) + + internal fun logsEndpoint(): GrpcEndpointConnectivity = + GrpcEndpointConnectivity.create( + chooseEndpoint(logsConfig), + logsConfig.headers + headers, + chooseCompression(logsConfig.compression), + ) + + internal fun metricsEndpoint(): GrpcEndpointConnectivity = + GrpcEndpointConnectivity.create( + chooseEndpoint(metricsConfig), + metricsConfig.headers + headers, + chooseCompression(metricsConfig.compression), + ) + + private fun chooseEndpoint(cfg: EndpointConfiguration): String = cfg.url.ifBlank { endpoint } + + private fun chooseCompression(signalConfigCompression: Compression?): Compression = signalConfigCompression ?: this.compression + + /** + * Configures export options specific to span data. + * + * Values set in this configuration override the top-level [endpoint], + * [headers], and [compression] for span exports. + */ + fun spans(action: EndpointConfiguration.() -> Unit) { + spansConfig.action() + } + + /** + * Configures export options specific to log data. + * + * Values set in this configuration override the top-level [endpoint], + * [headers], and [compression] for log exports. + */ + fun logs(action: EndpointConfiguration.() -> Unit) { + logsConfig.action() + } + + /** + * Configures export options specific to metric data. + * + * Values set in this configuration override the top-level [endpoint], + * [headers], and [compression] for metric exports. + */ + fun metrics(action: EndpointConfiguration.() -> Unit) { + metricsConfig.action() + } +} diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration.kt index 1c71402c7..2834cec28 100644 --- a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration.kt +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfiguration.kt @@ -21,23 +21,47 @@ import io.opentelemetry.sdk.resources.ResourceBuilder class OpenTelemetryConfiguration internal constructor( internal val rumConfig: OtelRumConfig = OtelRumConfig(), internal val diskBufferingConfig: DiskBufferingConfigurationSpec = DiskBufferingConfigurationSpec(rumConfig), - /** - * Configures the [Clock] used for capturing telemetry. - */ var clock: Clock = OtelAndroidClock(), ) { internal val exportConfig = HttpExportConfiguration() + internal var grpcExportConfig: GrpcExportConfiguration? = null + internal var unifiedExportConfig: ExportConfiguration? = null internal val sessionConfig = SessionConfiguration() internal val instrumentations = InstrumentationConfiguration(rumConfig) internal var resourceAction: ResourceBuilder.() -> Unit = {} /** - * Configures how OpenTelemetry should export telemetry over HTTP. + * Configures exporting of telemetry over HTTP using [HttpExportConfiguration]. + * + * This is the default export configuration. If [export] or [grpcExport] is also called, + * the precedence order is: [export] > [grpcExport] > [httpExport]. */ fun httpExport(action: HttpExportConfiguration.() -> Unit) { exportConfig.action() } + /** + * Configures exporting of telemetry over gRPC using [GrpcExportConfiguration]. + * + * If [export] is also called, it takes precedence over this configuration. + * This configuration takes precedence over [httpExport]. + */ + fun grpcExport(action: GrpcExportConfiguration.() -> Unit) { + grpcExportConfig = GrpcExportConfiguration().apply(action) + } + + /** + * Configures export settings using a unified DSL that supports both HTTP and gRPC protocols. + * + * This is the preferred way to configure telemetry export. Use [ExportConfiguration.protocol] + * to select the transport (HTTP or gRPC). + * + * This configuration takes precedence over both [httpExport] and [grpcExport]. + */ + fun export(action: ExportConfiguration.() -> Unit) { + unifiedExportConfig = ExportConfiguration().apply(action) + } + /** * Configures individual instrumentations. */ diff --git a/android-agent/src/test/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializerTest.kt b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializerTest.kt index 1b66f4ad4..f95e517fc 100644 --- a/android-agent/src/test/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializerTest.kt +++ b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializerTest.kt @@ -10,6 +10,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import io.opentelemetry.android.Incubating +import io.opentelemetry.android.agent.connectivity.ExportProtocol import io.opentelemetry.android.agent.session.SessionIdTimeoutHandler import io.opentelemetry.android.internal.services.Services import io.opentelemetry.android.internal.services.applifecycle.AppLifecycle @@ -30,7 +31,7 @@ class OpenTelemetryRumInitializerTest { } @Test - fun `Verify timeoutHandler initialization 2`() { + fun `Verify timeoutHandler initialization with HTTP export`() { createAndSetServiceManager() OpenTelemetryRumInitializer.initialize( @@ -47,6 +48,151 @@ class OpenTelemetryRumInitializerTest { } } + @Test + fun `Verify initialization with gRPC export`() { + createAndSetServiceManager() + + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + configuration = { + grpcExport { + endpoint = "http://127.0.0.1:4317" + } + }, + ) + + verify { + appLifecycle.registerListener(any()) + } + } + + @Test + fun `Verify initialization with unified export using HTTP`() { + createAndSetServiceManager() + + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + configuration = { + export { + protocol = ExportProtocol.HTTP + endpoint = "http://127.0.0.1:4318" + } + }, + ) + + verify { + appLifecycle.registerListener(any()) + } + } + + @Test + fun `Verify initialization with unified export using GRPC`() { + createAndSetServiceManager() + + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + configuration = { + export { + protocol = ExportProtocol.GRPC + endpoint = "http://127.0.0.1:4317" + } + }, + ) + + verify { + appLifecycle.registerListener(any()) + } + } + + @Test + fun `Verify initialization with default configuration`() { + createAndSetServiceManager() + + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + ) + + verify { + appLifecycle.registerListener(any()) + } + } + + @Test + fun `Verify initialization with application context`() { + createAndSetServiceManager() + + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication().applicationContext, + configuration = { + httpExport { + baseUrl = "http://127.0.0.1:4318" + } + }, + ) + + verify { + appLifecycle.registerListener(any()) + } + } + + @Test + fun `Verify initialization with NONE compression for HTTP`() { + createAndSetServiceManager() + + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + configuration = { + httpExport { + baseUrl = "http://127.0.0.1:4318" + compression = io.opentelemetry.android.agent.connectivity.Compression.NONE + } + }, + ) + + verify { + appLifecycle.registerListener(any()) + } + } + + @Test + fun `Verify initialization with NONE compression for gRPC`() { + createAndSetServiceManager() + + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + configuration = { + grpcExport { + endpoint = "http://127.0.0.1:4317" + compression = io.opentelemetry.android.agent.connectivity.Compression.NONE + } + }, + ) + + verify { + appLifecycle.registerListener(any()) + } + } + + @Test + fun `Verify initialization with unified export and NONE compression`() { + createAndSetServiceManager() + + OpenTelemetryRumInitializer.initialize( + context = RuntimeEnvironment.getApplication(), + configuration = { + export { + protocol = ExportProtocol.GRPC + endpoint = "http://127.0.0.1:4317" + compression = io.opentelemetry.android.agent.connectivity.Compression.NONE + } + }, + ) + + verify { + appLifecycle.registerListener(any()) + } + } + @After fun tearDown() { Services.set(null) diff --git a/android-agent/src/test/kotlin/io/opentelemetry/android/agent/connectivity/ExportProtocolTest.kt b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/connectivity/ExportProtocolTest.kt new file mode 100644 index 000000000..20772a280 --- /dev/null +++ b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/connectivity/ExportProtocolTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.connectivity + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ExportProtocolTest { + @Test + fun `HTTP protocol has correct name`() { + assertThat(ExportProtocol.HTTP.name).isEqualTo("HTTP") + } + + @Test + fun `GRPC protocol has correct name`() { + assertThat(ExportProtocol.GRPC.name).isEqualTo("GRPC") + } + + @Test + fun `values returns both protocols`() { + val values = ExportProtocol.values() + + assertThat(values).hasSize(2) + assertThat(values).contains(ExportProtocol.HTTP) + assertThat(values).contains(ExportProtocol.GRPC) + } + + @Test + fun `valueOf HTTP returns HTTP`() { + assertThat(ExportProtocol.valueOf("HTTP")).isEqualTo(ExportProtocol.HTTP) + } + + @Test + fun `valueOf GRPC returns GRPC`() { + assertThat(ExportProtocol.valueOf("GRPC")).isEqualTo(ExportProtocol.GRPC) + } +} diff --git a/android-agent/src/test/kotlin/io/opentelemetry/android/agent/connectivity/GrpcEndpointConnectivityTest.kt b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/connectivity/GrpcEndpointConnectivityTest.kt new file mode 100644 index 000000000..5d809e2bd --- /dev/null +++ b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/connectivity/GrpcEndpointConnectivityTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.connectivity + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class GrpcEndpointConnectivityTest { + @Test + fun `create returns connectivity with provided endpoint`() { + val endpoint = "https://collector.example.com:4317" + val headers = mapOf("Authorization" to "Bearer token") + val compression = Compression.GZIP + + val connectivity = GrpcEndpointConnectivity.create(endpoint, headers, compression) + + assertThat(connectivity.getUrl()).isEqualTo(endpoint) + } + + @Test + fun `create returns connectivity with provided headers`() { + val endpoint = "https://collector.example.com:4317" + val headers = mapOf("Authorization" to "Bearer token", "X-Custom" to "value") + val compression = Compression.GZIP + + val connectivity = GrpcEndpointConnectivity.create(endpoint, headers, compression) + + assertThat(connectivity.getHeaders()).isEqualTo(headers) + assertThat(connectivity.getHeaders()).containsEntry("Authorization", "Bearer token") + assertThat(connectivity.getHeaders()).containsEntry("X-Custom", "value") + } + + @Test + fun `create with GZIP compression returns GZIP`() { + val endpoint = "https://collector.example.com:4317" + val headers = emptyMap() + val compression = Compression.GZIP + + val connectivity = GrpcEndpointConnectivity.create(endpoint, headers, compression) + + assertThat(connectivity.getCompression()).isEqualTo(Compression.GZIP) + } + + @Test + fun `create with NONE compression returns NONE`() { + val endpoint = "https://collector.example.com:4317" + val headers = emptyMap() + val compression = Compression.NONE + + val connectivity = GrpcEndpointConnectivity.create(endpoint, headers, compression) + + assertThat(connectivity.getCompression()).isEqualTo(Compression.NONE) + } + + @Test + fun `create with empty headers returns empty map`() { + val endpoint = "https://collector.example.com:4317" + val headers = emptyMap() + val compression = Compression.GZIP + + val connectivity = GrpcEndpointConnectivity.create(endpoint, headers, compression) + + assertThat(connectivity.getHeaders()).isEmpty() + } + + @Test + fun `create with empty endpoint returns empty string`() { + val endpoint = "" + val headers = emptyMap() + val compression = Compression.GZIP + + val connectivity = GrpcEndpointConnectivity.create(endpoint, headers, compression) + + assertThat(connectivity.getUrl()).isEmpty() + } + + @Test + fun `getUrl returns endpoint unchanged without path modification`() { + val endpoint = "https://collector.example.com:4317/custom/path" + val connectivity = GrpcEndpointConnectivity.create(endpoint, emptyMap(), Compression.GZIP) + + assertThat(connectivity.getUrl()).isEqualTo(endpoint) + } +} diff --git a/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/ExportConfigurationTest.kt b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/ExportConfigurationTest.kt new file mode 100644 index 000000000..54815ad33 --- /dev/null +++ b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/ExportConfigurationTest.kt @@ -0,0 +1,349 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.dsl + +import io.opentelemetry.android.agent.FakeClock +import io.opentelemetry.android.agent.connectivity.Compression +import io.opentelemetry.android.agent.connectivity.EndpointConnectivity +import io.opentelemetry.android.agent.connectivity.ExportProtocol +import io.opentelemetry.android.agent.connectivity.GrpcEndpointConnectivity +import io.opentelemetry.android.agent.connectivity.HttpEndpointConnectivity +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue + +internal class ExportConfigurationTest { + private lateinit var otelConfig: OpenTelemetryConfiguration + + @Before + fun setUp() { + otelConfig = OpenTelemetryConfiguration(clock = FakeClock()) + } + + @Test + fun testDefaultsToHttp() { + otelConfig.export { } + val config = otelConfig.unifiedExportConfig!! + + assertEquals(ExportProtocol.HTTP, config.protocol) + assertEquals("", config.endpoint) + assertEquals(emptyMap(), config.headers) + assertEquals(Compression.GZIP, config.compression) + } + + @Test + fun testHttpProtocolConfiguration() { + val endpoint = "https://collector.example.com:4318" + val headers = mapOf("Authorization" to "Bearer token") + + otelConfig.export { + protocol = ExportProtocol.HTTP + this.endpoint = endpoint + this.headers = headers + } + val config = otelConfig.unifiedExportConfig!! + + assertEquals(ExportProtocol.HTTP, config.protocol) + assertTrue(config.spansEndpoint() is HttpEndpointConnectivity) + config.spansEndpoint().assertEndpointConfig("$endpoint/v1/traces", headers, Compression.GZIP) + config.logsEndpoint().assertEndpointConfig("$endpoint/v1/logs", headers, Compression.GZIP) + config.metricsEndpoint().assertEndpointConfig("$endpoint/v1/metrics", headers, Compression.GZIP) + } + + @Test + fun testGrpcProtocolConfiguration() { + val endpoint = "https://collector.example.com:4317" + val headers = mapOf("Authorization" to "Bearer token") + + otelConfig.export { + protocol = ExportProtocol.GRPC + this.endpoint = endpoint + this.headers = headers + } + val config = otelConfig.unifiedExportConfig!! + + assertEquals(ExportProtocol.GRPC, config.protocol) + assertTrue(config.spansEndpoint() is GrpcEndpointConnectivity) + config.spansEndpoint().assertEndpointConfig(endpoint, headers, Compression.GZIP) + config.logsEndpoint().assertEndpointConfig(endpoint, headers, Compression.GZIP) + config.metricsEndpoint().assertEndpointConfig(endpoint, headers, Compression.GZIP) + } + + @Test + fun testHttpSpansEndpointWithOverride() { + val baseEndpoint = "https://collector.example.com:4318" + val spansEndpoint = "https://spans.collector.example.com:4318" + val spansHeaders = mapOf("spans-header" to "spans-value") + + otelConfig.export { + protocol = ExportProtocol.HTTP + endpoint = baseEndpoint + spans { + url = spansEndpoint + this.headers = spansHeaders + } + } + val config = otelConfig.unifiedExportConfig!! + + assertTrue(config.spansEndpoint() is HttpEndpointConnectivity) + config.spansEndpoint().assertEndpointConfig("$spansEndpoint/v1/traces", spansHeaders, Compression.GZIP) + } + + @Test + fun testHttpLogsEndpointWithOverride() { + val baseEndpoint = "https://collector.example.com:4318" + val logsEndpoint = "https://logs.collector.example.com:4318" + val logsHeaders = mapOf("logs-header" to "logs-value") + + otelConfig.export { + protocol = ExportProtocol.HTTP + endpoint = baseEndpoint + logs { + url = logsEndpoint + this.headers = logsHeaders + } + } + val config = otelConfig.unifiedExportConfig!! + + assertTrue(config.logsEndpoint() is HttpEndpointConnectivity) + config.logsEndpoint().assertEndpointConfig("$logsEndpoint/v1/logs", logsHeaders, Compression.GZIP) + } + + @Test + fun testHttpMetricsEndpointWithOverride() { + val baseEndpoint = "https://collector.example.com:4318" + val metricsEndpoint = "https://metrics.collector.example.com:4318" + val metricsHeaders = mapOf("metrics-header" to "metrics-value") + + otelConfig.export { + protocol = ExportProtocol.HTTP + endpoint = baseEndpoint + metrics { + url = metricsEndpoint + this.headers = metricsHeaders + } + } + val config = otelConfig.unifiedExportConfig!! + + assertTrue(config.metricsEndpoint() is HttpEndpointConnectivity) + config.metricsEndpoint().assertEndpointConfig("$metricsEndpoint/v1/metrics", metricsHeaders, Compression.GZIP) + } + + @Test + fun testGrpcSpansEndpointWithOverride() { + val baseEndpoint = "https://collector.example.com:4317" + val spansEndpoint = "https://spans.collector.example.com:4317" + + otelConfig.export { + protocol = ExportProtocol.GRPC + endpoint = baseEndpoint + spans { + url = spansEndpoint + } + } + val config = otelConfig.unifiedExportConfig!! + + assertTrue(config.spansEndpoint() is GrpcEndpointConnectivity) + config.spansEndpoint().assertEndpointConfig(spansEndpoint, emptyMap(), Compression.GZIP) + } + + @Test + fun testGrpcLogsEndpointWithOverride() { + val baseEndpoint = "https://collector.example.com:4317" + val logsEndpoint = "https://logs.collector.example.com:4317" + + otelConfig.export { + protocol = ExportProtocol.GRPC + endpoint = baseEndpoint + logs { + url = logsEndpoint + } + } + val config = otelConfig.unifiedExportConfig!! + + assertTrue(config.logsEndpoint() is GrpcEndpointConnectivity) + config.logsEndpoint().assertEndpointConfig(logsEndpoint, emptyMap(), Compression.GZIP) + } + + @Test + fun testGrpcMetricsEndpointWithOverride() { + val baseEndpoint = "https://collector.example.com:4317" + val metricsEndpoint = "https://metrics.collector.example.com:4317" + + otelConfig.export { + protocol = ExportProtocol.GRPC + endpoint = baseEndpoint + metrics { + url = metricsEndpoint + } + } + val config = otelConfig.unifiedExportConfig!! + + assertTrue(config.metricsEndpoint() is GrpcEndpointConnectivity) + config.metricsEndpoint().assertEndpointConfig(metricsEndpoint, emptyMap(), Compression.GZIP) + } + + @Test + fun testCompressionOverrideForHttp() { + otelConfig.export { + protocol = ExportProtocol.HTTP + endpoint = "https://collector.example.com:4318" + compression = Compression.NONE + } + val config = otelConfig.unifiedExportConfig!! + + config.spansEndpoint().assertEndpointConfig( + "https://collector.example.com:4318/v1/traces", + emptyMap(), + Compression.NONE, + ) + } + + @Test + fun testCompressionOverrideForGrpc() { + otelConfig.export { + protocol = ExportProtocol.GRPC + endpoint = "https://collector.example.com:4317" + compression = Compression.NONE + } + val config = otelConfig.unifiedExportConfig!! + + config.spansEndpoint().assertEndpointConfig( + "https://collector.example.com:4317", + emptyMap(), + Compression.NONE, + ) + } + + @Test + fun testSignalSpecificCompressionOverride() { + otelConfig.export { + protocol = ExportProtocol.GRPC + endpoint = "https://collector.example.com:4317" + compression = Compression.GZIP + spans { + compression = Compression.NONE + } + } + val config = otelConfig.unifiedExportConfig!! + + config.spansEndpoint().assertEndpointConfig( + "https://collector.example.com:4317", + emptyMap(), + Compression.NONE, + ) + config.logsEndpoint().assertEndpointConfig( + "https://collector.example.com:4317", + emptyMap(), + Compression.GZIP, + ) + } + + @Test + fun testBlankSignalEndpointFallsBackToBase() { + val baseEndpoint = "https://collector.example.com:4317" + + otelConfig.export { + protocol = ExportProtocol.GRPC + endpoint = baseEndpoint + spans { + url = "" + } + } + val config = otelConfig.unifiedExportConfig!! + + config.spansEndpoint().assertEndpointConfig(baseEndpoint, emptyMap(), Compression.GZIP) + } + + @Test + fun testNullSignalCompressionFallsBackToBase() { + otelConfig.export { + protocol = ExportProtocol.GRPC + endpoint = "https://collector.example.com:4317" + compression = Compression.NONE + logs { + compression = null + } + } + val config = otelConfig.unifiedExportConfig!! + + config.logsEndpoint().assertEndpointConfig( + "https://collector.example.com:4317", + emptyMap(), + Compression.NONE, + ) + } + + @Test + fun testHeadersMergeCorrectlyForHttp() { + val baseHeaders = mapOf("base" to "value1") + val signalHeaders = mapOf("signal" to "value2") + + otelConfig.export { + protocol = ExportProtocol.HTTP + endpoint = "https://collector.example.com:4318" + headers = baseHeaders + spans { + this.headers = signalHeaders + } + } + val config = otelConfig.unifiedExportConfig!! + + val spanEndpoint = config.spansEndpoint() + assertEquals(signalHeaders + baseHeaders, spanEndpoint.getHeaders()) + } + + @Test + fun testHeadersMergeCorrectlyForGrpc() { + val baseHeaders = mapOf("base" to "value1") + val signalHeaders = mapOf("signal" to "value2") + + otelConfig.export { + protocol = ExportProtocol.GRPC + endpoint = "https://collector.example.com:4317" + headers = baseHeaders + metrics { + this.headers = signalHeaders + } + } + val config = otelConfig.unifiedExportConfig!! + + val metricsEndpoint = config.metricsEndpoint() + assertEquals(signalHeaders + baseHeaders, metricsEndpoint.getHeaders()) + } + + @Test + fun testAllSignalsWithDifferentProtocol() { + otelConfig.export { + protocol = ExportProtocol.GRPC + endpoint = "https://collector.example.com:4317" + + spans { url = "https://spans:4317" } + logs { url = "https://logs:4317" } + metrics { url = "https://metrics:4317" } + } + val config = otelConfig.unifiedExportConfig!! + + assertTrue(config.spansEndpoint() is GrpcEndpointConnectivity) + assertTrue(config.logsEndpoint() is GrpcEndpointConnectivity) + assertTrue(config.metricsEndpoint() is GrpcEndpointConnectivity) + + assertEquals("https://spans:4317", config.spansEndpoint().getUrl()) + assertEquals("https://logs:4317", config.logsEndpoint().getUrl()) + assertEquals("https://metrics:4317", config.metricsEndpoint().getUrl()) + } + + private fun EndpointConnectivity.assertEndpointConfig( + expectedUrl: String, + expectedHeaders: Map, + expectedCompression: Compression, + ) { + assertEquals(expectedUrl, getUrl()) + assertEquals(expectedHeaders, getHeaders()) + assertEquals(expectedCompression, getCompression()) + } +} diff --git a/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/GrpcExportConfigurationTest.kt b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/GrpcExportConfigurationTest.kt new file mode 100644 index 000000000..a9436d214 --- /dev/null +++ b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/GrpcExportConfigurationTest.kt @@ -0,0 +1,293 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.dsl + +import io.opentelemetry.android.agent.FakeClock +import io.opentelemetry.android.agent.connectivity.Compression +import io.opentelemetry.android.agent.connectivity.EndpointConnectivity +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals + +internal class GrpcExportConfigurationTest { + private lateinit var otelConfig: OpenTelemetryConfiguration + + @Before + fun setUp() { + otelConfig = OpenTelemetryConfiguration(clock = FakeClock()) + } + + @Test + fun testDefaults() { + otelConfig.grpcExport { } + val config = otelConfig.grpcExportConfig!! + + val expectedHeaders = emptyMap() + val expectedCompression = Compression.GZIP + + config.spansEndpoint().assertEndpointConfig("", expectedHeaders, expectedCompression) + config.logsEndpoint().assertEndpointConfig("", expectedHeaders, expectedCompression) + config.metricsEndpoint().assertEndpointConfig("", expectedHeaders, expectedCompression) + assertEquals("", config.endpoint) + assertEquals(expectedHeaders, config.headers) + assertEquals(expectedCompression, config.compression) + } + + @Test + fun testEndpointConfiguration() { + val endpoint = "https://collector.example.com:4317" + val headers = mapOf("Authorization" to "Bearer token") + + otelConfig.grpcExport { + this.endpoint = endpoint + this.headers = headers + } + val config = otelConfig.grpcExportConfig!! + + config.spansEndpoint().assertEndpointConfig(endpoint, headers, Compression.GZIP) + config.logsEndpoint().assertEndpointConfig(endpoint, headers, Compression.GZIP) + config.metricsEndpoint().assertEndpointConfig(endpoint, headers, Compression.GZIP) + assertEquals(endpoint, config.endpoint) + assertEquals(headers, config.headers) + } + + @Test + fun testCompressionNone() { + otelConfig.grpcExport { + endpoint = "https://collector.example.com:4317" + compression = Compression.NONE + } + val config = otelConfig.grpcExportConfig!! + + config.spansEndpoint().assertEndpointConfig( + "https://collector.example.com:4317", + emptyMap(), + Compression.NONE, + ) + } + + @Test + fun testIndividualEndpointOverrides() { + val baseEndpoint = "https://collector.example.com:4317" + val baseHeaders = mapOf("base-header" to "base-value") + + val spansEndpoint = "https://spans.collector.example.com:4317" + val spansHeaders = mapOf("spans-header" to "spans-value") + + val logsEndpoint = "https://logs.collector.example.com:4317" + val logsHeaders = mapOf("logs-header" to "logs-value") + + val metricsEndpoint = "https://metrics.collector.example.com:4317" + val metricsHeaders = mapOf("metrics-header" to "metrics-value") + + otelConfig.grpcExport { + endpoint = baseEndpoint + headers = baseHeaders + + spans { + url = spansEndpoint + this.headers = spansHeaders + compression = Compression.NONE + } + logs { + url = logsEndpoint + this.headers = logsHeaders + compression = Compression.NONE + } + metrics { + url = metricsEndpoint + this.headers = metricsHeaders + compression = Compression.NONE + } + } + val config = otelConfig.grpcExportConfig!! + + config.spansEndpoint().assertEndpointConfig( + spansEndpoint, + spansHeaders + baseHeaders, + Compression.NONE, + ) + config.logsEndpoint().assertEndpointConfig( + logsEndpoint, + logsHeaders + baseHeaders, + Compression.NONE, + ) + config.metricsEndpoint().assertEndpointConfig( + metricsEndpoint, + metricsHeaders + baseHeaders, + Compression.NONE, + ) + } + + @Test + fun testSignalSpecificOverridesFallBackToBase() { + val baseEndpoint = "https://collector.example.com:4317" + val baseHeaders = mapOf("base-header" to "base-value") + val spansHeaders = mapOf("spans-header" to "spans-value") + + otelConfig.grpcExport { + endpoint = baseEndpoint + headers = baseHeaders + + spans { + this.headers = spansHeaders + } + } + val config = otelConfig.grpcExportConfig!! + + config.spansEndpoint().assertEndpointConfig( + baseEndpoint, + spansHeaders + baseHeaders, + Compression.GZIP, + ) + config.logsEndpoint().assertEndpointConfig( + baseEndpoint, + baseHeaders, + Compression.GZIP, + ) + config.metricsEndpoint().assertEndpointConfig( + baseEndpoint, + baseHeaders, + Compression.GZIP, + ) + } + + @Test + fun testSpansEndpointOverrideOnlyUrl() { + val baseEndpoint = "https://collector.example.com:4317" + val spansEndpoint = "https://spans.collector.example.com:4317" + + otelConfig.grpcExport { + endpoint = baseEndpoint + spans { + url = spansEndpoint + } + } + val config = otelConfig.grpcExportConfig!! + + config.spansEndpoint().assertEndpointConfig( + spansEndpoint, + emptyMap(), + Compression.GZIP, + ) + config.logsEndpoint().assertEndpointConfig( + baseEndpoint, + emptyMap(), + Compression.GZIP, + ) + } + + @Test + fun testLogsEndpointOverrideOnlyCompression() { + val baseEndpoint = "https://collector.example.com:4317" + + otelConfig.grpcExport { + endpoint = baseEndpoint + logs { + compression = Compression.NONE + } + } + val config = otelConfig.grpcExportConfig!! + + config.logsEndpoint().assertEndpointConfig( + baseEndpoint, + emptyMap(), + Compression.NONE, + ) + config.spansEndpoint().assertEndpointConfig( + baseEndpoint, + emptyMap(), + Compression.GZIP, + ) + } + + @Test + fun testMetricsEndpointOverrideOnlyHeaders() { + val baseEndpoint = "https://collector.example.com:4317" + val metricsHeaders = mapOf("metrics-header" to "metrics-value") + + otelConfig.grpcExport { + endpoint = baseEndpoint + metrics { + this.headers = metricsHeaders + } + } + val config = otelConfig.grpcExportConfig!! + + config.metricsEndpoint().assertEndpointConfig( + baseEndpoint, + metricsHeaders, + Compression.GZIP, + ) + } + + @Test + fun testBlankSignalUrlFallsBackToBase() { + val baseEndpoint = "https://collector.example.com:4317" + + otelConfig.grpcExport { + endpoint = baseEndpoint + spans { + url = "" + } + } + val config = otelConfig.grpcExportConfig!! + + config.spansEndpoint().assertEndpointConfig( + baseEndpoint, + emptyMap(), + Compression.GZIP, + ) + } + + @Test + fun testNullSignalCompressionFallsBackToBase() { + val baseEndpoint = "https://collector.example.com:4317" + + otelConfig.grpcExport { + endpoint = baseEndpoint + compression = Compression.NONE + spans { + compression = null + } + } + val config = otelConfig.grpcExportConfig!! + + config.spansEndpoint().assertEndpointConfig( + baseEndpoint, + emptyMap(), + Compression.NONE, + ) + } + + @Test + fun testHeadersMergeCorrectly() { + val baseHeaders = mapOf("base" to "value1") + val signalHeaders = mapOf("signal" to "value2") + + otelConfig.grpcExport { + endpoint = "https://collector.example.com:4317" + headers = baseHeaders + spans { + this.headers = signalHeaders + } + } + val config = otelConfig.grpcExportConfig!! + + val spanEndpoint = config.spansEndpoint() + assertEquals(signalHeaders + baseHeaders, spanEndpoint.getHeaders()) + } + + private fun EndpointConnectivity.assertEndpointConfig( + expectedUrl: String, + expectedHeaders: Map, + expectedCompression: Compression, + ) { + assertEquals(expectedUrl, getUrl()) + assertEquals(expectedHeaders, getHeaders()) + assertEquals(expectedCompression, getCompression()) + } +} diff --git a/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfigurationExportTest.kt b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfigurationExportTest.kt new file mode 100644 index 000000000..b3dd74525 --- /dev/null +++ b/android-agent/src/test/kotlin/io/opentelemetry/android/agent/dsl/OpenTelemetryConfigurationExportTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.agent.dsl + +import io.opentelemetry.android.agent.FakeClock +import io.opentelemetry.android.agent.connectivity.Compression +import io.opentelemetry.android.agent.connectivity.ExportProtocol +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull + +internal class OpenTelemetryConfigurationExportTest { + private lateinit var config: OpenTelemetryConfiguration + + @Before + fun setUp() { + config = OpenTelemetryConfiguration(clock = FakeClock()) + } + + @Test + fun testHttpExportDefaultConfig() { + assertNotNull(config.exportConfig) + assertEquals("", config.exportConfig.baseUrl) + assertEquals(emptyMap(), config.exportConfig.baseHeaders) + assertEquals(Compression.GZIP, config.exportConfig.compression) + } + + @Test + fun testGrpcExportConfigIsNullByDefault() { + assertNull(config.grpcExportConfig) + } + + @Test + fun testUnifiedExportConfigIsNullByDefault() { + assertNull(config.unifiedExportConfig) + } + + @Test + fun testHttpExportDslSetsConfig() { + config.httpExport { + baseUrl = "https://http.collector.example.com:4318" + baseHeaders = mapOf("Auth" to "token") + compression = Compression.NONE + } + + assertEquals("https://http.collector.example.com:4318", config.exportConfig.baseUrl) + assertEquals(mapOf("Auth" to "token"), config.exportConfig.baseHeaders) + assertEquals(Compression.NONE, config.exportConfig.compression) + } + + @Test + fun testGrpcExportDslCreatesConfig() { + config.grpcExport { + endpoint = "https://grpc.collector.example.com:4317" + headers = mapOf("Auth" to "token") + compression = Compression.GZIP + } + + assertNotNull(config.grpcExportConfig) + assertEquals("https://grpc.collector.example.com:4317", config.grpcExportConfig!!.endpoint) + assertEquals(mapOf("Auth" to "token"), config.grpcExportConfig!!.headers) + assertEquals(Compression.GZIP, config.grpcExportConfig!!.compression) + } + + @Test + fun testUnifiedExportDslCreatesConfig() { + config.export { + protocol = ExportProtocol.GRPC + endpoint = "https://collector.example.com:4317" + headers = mapOf("Auth" to "token") + compression = Compression.NONE + } + + assertNotNull(config.unifiedExportConfig) + assertEquals(ExportProtocol.GRPC, config.unifiedExportConfig!!.protocol) + assertEquals("https://collector.example.com:4317", config.unifiedExportConfig!!.endpoint) + assertEquals(mapOf("Auth" to "token"), config.unifiedExportConfig!!.headers) + assertEquals(Compression.NONE, config.unifiedExportConfig!!.compression) + } + + @Test + fun testUnifiedExportDefaultsToHttp() { + config.export { } + + assertNotNull(config.unifiedExportConfig) + assertEquals(ExportProtocol.HTTP, config.unifiedExportConfig!!.protocol) + } + + @Test + fun testMultipleExportConfigsCanCoexist() { + config.httpExport { + baseUrl = "https://http.collector.example.com:4318" + } + config.grpcExport { + endpoint = "https://grpc.collector.example.com:4317" + } + config.export { + protocol = ExportProtocol.GRPC + endpoint = "https://unified.collector.example.com:4317" + } + + assertNotNull(config.exportConfig) + assertNotNull(config.grpcExportConfig) + assertNotNull(config.unifiedExportConfig) + assertEquals("https://http.collector.example.com:4318", config.exportConfig.baseUrl) + assertEquals("https://grpc.collector.example.com:4317", config.grpcExportConfig!!.endpoint) + assertEquals("https://unified.collector.example.com:4317", config.unifiedExportConfig!!.endpoint) + } + + @Test + fun testGrpcExportWithSignalOverrides() { + config.grpcExport { + endpoint = "https://collector.example.com:4317" + spans { + url = "https://spans.collector.example.com:4317" + } + } + + val spansEndpoint = config.grpcExportConfig!!.spansEndpoint() + assertEquals("https://spans.collector.example.com:4317", spansEndpoint.getUrl()) + } + + @Test + fun testUnifiedExportWithSignalOverrides() { + config.export { + protocol = ExportProtocol.GRPC + endpoint = "https://collector.example.com:4317" + logs { + url = "https://logs.collector.example.com:4317" + } + } + + val logsEndpoint = config.unifiedExportConfig!!.logsEndpoint() + assertEquals("https://logs.collector.example.com:4317", logsEndpoint.getUrl()) + } +}