From 2861d89c12176461ccf1b7ea51d82ebb63eaf6db Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Thu, 12 Mar 2026 12:44:44 -0400 Subject: [PATCH 1/3] adding logger to log otel status updated header name --- .../otel/android/OtelPlatformProvider.kt | 7 ++ .../onesignal/otel/IOtelPlatformProvider.kt | 7 ++ .../onesignal/otel/OneSignalOpenTelemetry.kt | 11 +++- .../otel/config/OtelConfigRemoteOneSignal.kt | 66 +++++++++++++++++-- 4 files changed, 82 insertions(+), 9 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt index eebf9469c..0dd48bf6c 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt @@ -9,6 +9,9 @@ import com.onesignal.core.internal.http.OneSignalService import com.onesignal.debug.internal.logging.Logging import com.onesignal.otel.IOtelPlatformProvider +// Use this to enable/disable the Otel exporter logging in debug builds. +internal const val OTEL_EXPORTER_LOGGING_ENABLED = false + /** * Configuration for AndroidOtelPlatformProvider. */ @@ -18,6 +21,7 @@ internal data class OtelPlatformProviderConfig( val appVersion: String, val context: Context? = null, val getIsInForeground: (() -> Boolean?)? = null, + val isOtelExporterLoggingEnabled: Boolean, ) /** @@ -135,6 +139,8 @@ internal class OtelPlatformProvider( } } + override val isOtelExporterLoggingEnabled: Boolean = OTEL_EXPORTER_LOGGING_ENABLED + override val appIdForHeaders: String get() = appId ?: "" @@ -159,6 +165,7 @@ internal fun createAndroidOtelPlatformProvider( appPackageId = context.packageName, appVersion = com.onesignal.common.AndroidUtils.getAppVersion(context) ?: "unknown", context = context, + isOtelExporterLoggingEnabled = OTEL_EXPORTER_LOGGING_ENABLED, ) ) } diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt index 98978ee19..f13549bc0 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt @@ -53,6 +53,13 @@ interface IOtelPlatformProvider { * Valid values: "NONE", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "VERBOSE" */ val remoteLogLevel: String? + + /** + * Debug-only toggle for local exporter diagnostics. + * When true, Otel exporter request/response success/failure logs are emitted to logcat. + */ + val isOtelExporterLoggingEnabled: Boolean + val appIdForHeaders: String /** diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt index 3e470f942..96432c9f3 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt @@ -103,15 +103,19 @@ internal class OneSignalOpenTelemetryRemote( val extraHttpHeaders: Map by lazy { mapOf( - "X-OneSignal-App-Id" to appId, - "X-OneSignal-SDK-Version" to platformProvider.sdkBaseVersion, + "SDK-Version" to "onesignal/${platformProvider.sdkBase}/${platformProvider.sdkBaseVersion}", ) } private val apiBaseUrl: String get() = platformProvider.apiBaseUrl override val logExporter by lazy { - OtelConfigRemoteOneSignal.HttpRecordBatchExporter.create(extraHttpHeaders, appId, apiBaseUrl) + OtelConfigRemoteOneSignal.HttpRecordBatchExporter.create( + extraHttpHeaders, + appId, + apiBaseUrl, + platformProvider.isOtelExporterLoggingEnabled, + ) } override fun getSdkInstance(attributes: Map): OpenTelemetrySdk = @@ -123,6 +127,7 @@ internal class OneSignalOpenTelemetryRemote( extraHttpHeaders, appId, apiBaseUrl, + platformProvider.isOtelExporterLoggingEnabled, ) ).build() } diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt index b6d877dda..70fec8f20 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt @@ -1,7 +1,10 @@ package com.onesignal.otel.config +import android.util.Log import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter +import io.opentelemetry.sdk.common.CompletableResultCode import io.opentelemetry.sdk.logs.SdkLoggerProvider +import io.opentelemetry.sdk.logs.data.LogRecordData import io.opentelemetry.sdk.logs.export.LogRecordExporter import java.time.Duration @@ -35,23 +38,74 @@ internal class OtelConfigRemoteOneSignal { extraHttpHeaders: Map, appId: String, apiBaseUrl: String, + enableExporterLogging: Boolean ): SdkLoggerProvider = SdkLoggerProvider .builder() .setResource(resource) .addLogRecordProcessor( OtelConfigShared.LogRecordProcessorConfig.batchLogRecordProcessor( - HttpRecordBatchExporter.create(extraHttpHeaders, appId, apiBaseUrl) + HttpRecordBatchExporter.create( + extraHttpHeaders, + appId, + apiBaseUrl, + enableExporterLogging, + ) ) ).setLogLimits(OtelConfigShared.LogLimitsConfig::logLimits) .build() } object HttpRecordBatchExporter { - fun create(extraHttpHeaders: Map, appId: String, apiBaseUrl: String) = - LogRecordExporterConfig.otlpHttpLogRecordExporter( - extraHttpHeaders, - buildEndpoint(apiBaseUrl, appId) - ) + fun create( + extraHttpHeaders: Map, + appId: String, + apiBaseUrl: String, + enableExporterLogging: Boolean, + ): LogRecordExporter { + val exporter = + LogRecordExporterConfig.otlpHttpLogRecordExporter( + extraHttpHeaders, + buildEndpoint(apiBaseUrl, appId) + ) + + return if (enableExporterLogging) { + ExporterLoggingConfig.loggingExporter(exporter) + } else { + exporter + } + } + } + + object ExporterLoggingConfig { + private const val TAG = "OneSignalOtel" + + fun loggingExporter(delegate: LogRecordExporter): LogRecordExporter = LoggingLogRecordExporter(delegate) + + private class LoggingLogRecordExporter( + private val delegate: LogRecordExporter + ) : LogRecordExporter { + override fun export(logs: Collection): CompletableResultCode { + Log.d(TAG, "OTEL export request sent to backend. count=${logs.size}") + val result = delegate.export(logs) + result.whenComplete { + if (result.isSuccess) { + Log.d(TAG, "OTEL export response received: success") + } else { + val throwable = result.failureThrowable + Log.e( + TAG, + "OTEL export response received: failed${throwable?.let { " - ${it.message}" } ?: ""}", + throwable + ) + } + } + return result + } + + override fun flush(): CompletableResultCode = delegate.flush() + + override fun shutdown(): CompletableResultCode = delegate.shutdown() + } } } From e6f0e6c6a3183c879f572a7de2c76ca1877c5d13 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Thu, 12 Mar 2026 12:57:25 -0400 Subject: [PATCH 2/3] cleanup flag --- .../debug/internal/logging/otel/android/OtelPlatformProvider.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt index 0dd48bf6c..4aa976700 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt @@ -21,7 +21,6 @@ internal data class OtelPlatformProviderConfig( val appVersion: String, val context: Context? = null, val getIsInForeground: (() -> Boolean?)? = null, - val isOtelExporterLoggingEnabled: Boolean, ) /** @@ -165,7 +164,6 @@ internal fun createAndroidOtelPlatformProvider( appPackageId = context.packageName, appVersion = com.onesignal.common.AndroidUtils.getAppVersion(context) ?: "unknown", context = context, - isOtelExporterLoggingEnabled = OTEL_EXPORTER_LOGGING_ENABLED, ) ) } From acb4aaf6b6852a74134a8a1d0307baa8807620b3 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Thu, 12 Mar 2026 13:47:40 -0400 Subject: [PATCH 3/3] some more cleanup and fixes --- .../onesignal/otel/OneSignalOpenTelemetry.kt | 18 ----------- .../otel/config/OtelConfigRemoteOneSignal.kt | 29 ++++++++++++++++-- .../otel/OneSignalOpenTelemetryTest.kt | 30 ++++++++----------- .../onesignal/otel/config/OtelConfigTest.kt | 5 ++-- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt index 96432c9f3..ea66980ab 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt @@ -17,24 +17,6 @@ internal fun LogRecordBuilder.setAllAttributes(attributes: Map): return this } -/** - * Extension function to set all attributes from an Attributes object. - * Made public so it can be used from other modules (e.g., core module for logging). - */ -fun LogRecordBuilder.setAllAttributes(attributes: io.opentelemetry.api.common.Attributes): LogRecordBuilder { - attributes.forEach { key, value -> - val keyString = key.key - when (value) { - is String -> this.setAttribute(keyString, value) - is Long -> this.setAttribute(keyString, value) - is Double -> this.setAttribute(keyString, value) - is Boolean -> this.setAttribute(keyString, value) - else -> this.setAttribute(keyString, value.toString()) - } - } - return this -} - internal abstract class OneSignalOpenTelemetryBase( private val osTopLevelFields: OtelFieldsTopLevel, private val osPerEventFields: OtelFieldsPerEvent, diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt index 70fec8f20..8ecbcbf90 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/config/OtelConfigRemoteOneSignal.kt @@ -10,10 +10,10 @@ import java.time.Duration internal class OtelConfigRemoteOneSignal { companion object { - const val OTEL_PATH = "sdk/otel" + const val OTEL_PATH = "sdk" fun buildEndpoint(apiBaseUrl: String, appId: String): String = - "$apiBaseUrl$OTEL_PATH/v1/logs?app_id=$appId" + "$apiBaseUrl$OTEL_PATH/log?app_id=$appId" } object LogRecordExporterConfig { @@ -85,6 +85,28 @@ internal class OtelConfigRemoteOneSignal { private class LoggingLogRecordExporter( private val delegate: LogRecordExporter ) : LogRecordExporter { + @Suppress("TooGenericExceptionCaught") + private fun resolveHttpFailureMessage(throwable: Throwable?): String { + if (throwable == null) return "unknown" + + return try { + if (!throwable.javaClass.name.endsWith("FailedExportException\$HttpExportException")) { + return throwable.message ?: "unknown" + } + + val response = throwable.javaClass.getMethod("getResponse").invoke(throwable) ?: return throwable.message ?: "unknown" + val statusCode = response.javaClass.getMethod("statusCode").invoke(response) + val statusMessage = response.javaClass.getMethod("statusMessage").invoke(response) + val responseBodyBytes = response.javaClass.getMethod("responseBody").invoke(response) as? ByteArray + val responseBody = responseBodyBytes?.decodeToString() + + "status=$statusCode message=$statusMessage" + + (if (responseBody.isNullOrBlank()) "" else " body=$responseBody") + } catch (_: Throwable) { + throwable.message ?: "unknown" + } + } + override fun export(logs: Collection): CompletableResultCode { Log.d(TAG, "OTEL export request sent to backend. count=${logs.size}") val result = delegate.export(logs) @@ -93,9 +115,10 @@ internal class OtelConfigRemoteOneSignal { Log.d(TAG, "OTEL export response received: success") } else { val throwable = result.failureThrowable + val failureMessage = resolveHttpFailureMessage(throwable) Log.e( TAG, - "OTEL export response received: failed${throwable?.let { " - ${it.message}" } ?: ""}", + "OTEL export response received: failed - $failureMessage", throwable ) } diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt index 5bc57abf3..775c1ad04 100644 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt +++ b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt @@ -1,13 +1,15 @@ package com.onesignal.otel import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.maps.shouldContainKey +import io.kotest.matchers.maps.shouldNotContainKey +import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.types.shouldBeInstanceOf import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.logs.LogRecordBuilder import kotlinx.coroutines.runBlocking @@ -77,6 +79,15 @@ class OneSignalOpenTelemetryTest : FunSpec({ } } + test("remote telemetry should only send SDK-Version header and not legacy OneSignal SDK header") { + val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) as OneSignalOpenTelemetryRemote + val headers = remoteTelemetry.extraHttpHeaders + + headers.shouldContainKey("SDK-Version") + headers["SDK-Version"] shouldBe "onesignal/android/5.0.0" + headers.shouldNotContainKey("X-OneSignal-SDK-Version") + } + // ===== Crash Local Telemetry Tests ===== test("createCrashLocalTelemetry should return IOtelOpenTelemetryCrash") { @@ -126,23 +137,6 @@ class OneSignalOpenTelemetryTest : FunSpec({ io.mockk.verify { mockBuilder.setAttribute("key2", "value2") } } - test("setAllAttributes with Attributes should handle different types") { - val mockBuilder = mockk(relaxed = true) - val attributes = Attributes.builder() - .put("string.key", "string-value") - .put("long.key", 123L) - .put("double.key", 45.67) - .put("boolean.key", true) - .build() - - mockBuilder.setAllAttributes(attributes) - - io.mockk.verify { mockBuilder.setAttribute("string.key", "string-value") } - io.mockk.verify { mockBuilder.setAttribute("long.key", 123L) } - io.mockk.verify { mockBuilder.setAttribute("double.key", 45.67) } - io.mockk.verify { mockBuilder.setAttribute("boolean.key", true) } - } - // ===== SDK Caching Tests ===== test("remote telemetry should cache SDK instance") { diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/config/OtelConfigTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/config/OtelConfigTest.kt index f4f8daaf1..e31fdfea9 100644 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/config/OtelConfigTest.kt +++ b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/config/OtelConfigTest.kt @@ -59,7 +59,7 @@ class OtelConfigTest : FunSpec({ test("buildEndpoint should construct correct URL from base and appId") { val endpoint = OtelConfigRemoteOneSignal.buildEndpoint("https://api.onesignal.com", "my-app") - endpoint shouldBe "https://api.onesignal.com/sdk/otel/v1/logs?app_id=my-app" + endpoint shouldBe "https://api.onesignal.com/sdk/log?app_id=my-app" } test("HttpRecordBatchExporter should create exporter with correct endpoint") { @@ -67,7 +67,7 @@ class OtelConfigTest : FunSpec({ val appId = "test-app-id" val apiBaseUrl = "https://api.onesignal.com" - val exporter = OtelConfigRemoteOneSignal.HttpRecordBatchExporter.create(headers, appId, apiBaseUrl) + val exporter = OtelConfigRemoteOneSignal.HttpRecordBatchExporter.create(headers, appId, apiBaseUrl, false) exporter shouldNotBe null } @@ -93,6 +93,7 @@ class OtelConfigTest : FunSpec({ headers, "test-app-id", "https://api.onesignal.com", + false, ) provider shouldNotBe null