From 1ff4bd14959d5c9f6a3307883ec7bef182e6e0d0 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Mon, 3 Nov 2025 14:11:56 -0500 Subject: [PATCH 1/8] feat: base parts to send otel crash reports --- .../sdktest/application/MainApplicationKT.kt | 4 + OneSignalSDK/onesignal/core/build.gradle | 10 ++ .../java/com/onesignal/core/CoreModule.kt | 12 ++ .../internal/crash/IOneSignalCrashHandler.kt | 3 + .../internal/crash/IOneSignalCrashReporter.kt | 5 + .../internal/crash/OneSignalCrashHandler.kt | 52 ++++++++ .../logging/otel/IOneSignalOpenTelemetry.kt | 13 ++ .../otel/OneSignalCrashReporterOtel.kt | 54 ++++++++ .../logging/otel/OneSignalOpenTelemetry.kt | 126 ++++++++++++++++++ 9 files changed, 279 insertions(+) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashHandler.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashReporter.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt index 123e74749..450b726fe 100644 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt +++ b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.launch class MainApplicationKT : MultiDexApplication() { @@ -81,6 +82,9 @@ class MainApplicationKT : MultiDexApplication() { OneSignal.Notifications.requestPermission(true) Log.d(Tag.LOG_TAG, Text.ONESIGNAL_SDK_INIT) + + delay(5000) + throw RuntimeException("test crash 2025-10-31") } } diff --git a/OneSignalSDK/onesignal/core/build.gradle b/OneSignalSDK/onesignal/core/build.gradle index b0f258071..0c5862515 100644 --- a/OneSignalSDK/onesignal/core/build.gradle +++ b/OneSignalSDK/onesignal/core/build.gradle @@ -88,6 +88,16 @@ dependencies { } } + implementation('io.opentelemetry:opentelemetry-api:1.55.0') + implementation('io.opentelemetry:opentelemetry-sdk:1.55.0') + // Docs sounds like okhttp is already included... + implementation('io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.55.0') + implementation('io.opentelemetry:opentelemetry-exporter-otlp:1.55.0') + implementation('io.opentelemetry.semconv:opentelemetry-semconv:1.37.0') + // TODO: Make sure by adding Android OTel that it doesn't auto send stuff to us + + + testImplementation(project(':OneSignal:testhelpers')) testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt index 9083cddad..102d3e29b 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt @@ -33,6 +33,12 @@ import com.onesignal.core.internal.purchases.impl.TrackGooglePurchase import com.onesignal.core.internal.startup.IStartableService import com.onesignal.core.internal.time.ITime import com.onesignal.core.internal.time.impl.Time +import com.onesignal.debug.internal.crash.IOneSignalCrashHandler +import com.onesignal.debug.internal.crash.IOneSignalCrashReporter +import com.onesignal.debug.internal.crash.OneSignalCrashHandler +import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetry +import com.onesignal.debug.internal.logging.otel.OneSignalCrashReporterOtel +import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetry import com.onesignal.inAppMessages.IInAppMessagesManager import com.onesignal.inAppMessages.internal.MisconfiguredIAMManager import com.onesignal.location.ILocationManager @@ -81,6 +87,12 @@ internal class CoreModule : IModule { // Purchase Tracking builder.register().provides() + // TODO: Should be a startable service instead (but we need to wait for the app id...) + builder.register().provides() + builder.register().provides() + builder.register().provides() + + // Register dummy services in the event they are not configured. These dummy services // will throw an error message if the associated functionality is attempted to be used. builder.register().provides() diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashHandler.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashHandler.kt new file mode 100644 index 000000000..59503b49c --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashHandler.kt @@ -0,0 +1,3 @@ +package com.onesignal.debug.internal.crash + +interface IOneSignalCrashHandler diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashReporter.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashReporter.kt new file mode 100644 index 000000000..368972dd3 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashReporter.kt @@ -0,0 +1,5 @@ +package com.onesignal.debug.internal.crash + +internal interface IOneSignalCrashReporter { + suspend fun sendCrash(thread: Thread, throwable: Throwable) +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt new file mode 100644 index 000000000..16961e2a9 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt @@ -0,0 +1,52 @@ +package com.onesignal.debug.internal.crash + +import android.util.Log +import kotlinx.coroutines.runBlocking + +// NOTE: For future refactors, code is written assuming this is a singleton +internal class OneSignalCrashHandler( + private val _crashReporter: IOneSignalCrashReporter, +) : IOneSignalCrashHandler, + Thread.UncaughtExceptionHandler { + private val existingHandler: Thread.UncaughtExceptionHandler? = + Thread.getDefaultUncaughtExceptionHandler() + + // TODO: Write the code to call this after we get the appId + // Recommend we only create an instance after getting a appId, otherwise there + // is no point setting up the handler. + init { + Thread.setDefaultUncaughtExceptionHandler(this) + } + + override fun uncaughtException(thread: Thread, throwable: Throwable) { + // TODO: Catch anything we may throw and silence it (print only to logcat) + // TODO: Add stackoverflow loop prevention + Log.e("OSCrashHandling", "uncaughtException TOP") + if (!isOneSignalAtFault(throwable)) { + existingHandler?.uncaughtException(thread, throwable) + return + } + + /** + * NOTE: The order and running sequentially is important as: + * The existingHandler.uncaughtException can immediately terminate the + * process, either directly (if this is Android's + * KillApplicationHandler) OR the app's handler / 3rd party SDK (either + * directly or more likely, by it calling Android's + * KillApplicationHandler). + * Given this, we can't parallelize the existingHandler work with ours. + * The safest thing is to try to finish our work as fast as possible + * (including ensuring our logging write buffers are flushed) then call + * the existingHandler so any crash handlers the app also has gets the + * crash even too. + * + * NOTE: addShutdownHook() isn't a workaround as it doesn't fire for + * Process.killProcess, which KillApplicationHandler calls. + */ + runBlocking { _crashReporter.sendCrash(thread, throwable) } + existingHandler?.uncaughtException(thread, throwable) + } +} + +internal fun isOneSignalAtFault(throwable: Throwable): Boolean = + throwable.stackTrace.any { it.className.startsWith("com.onesignal") } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt new file mode 100644 index 000000000..51392f4f7 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt @@ -0,0 +1,13 @@ +package com.onesignal.debug.internal.logging.otel + +import android.os.Build +import androidx.annotation.RequiresApi +import io.opentelemetry.api.logs.Logger +import io.opentelemetry.sdk.common.CompletableResultCode + +@RequiresApi(Build.VERSION_CODES.O) +internal interface IOneSignalOpenTelemetry { + val logger: Logger + + suspend fun forceFlush(): CompletableResultCode +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt new file mode 100644 index 000000000..75e1413ab --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt @@ -0,0 +1,54 @@ +package com.onesignal.debug.internal.logging.otel + +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.onesignal.debug.internal.crash.IOneSignalCrashReporter +import io.opentelemetry.api.common.Attributes +import java.io.PrintWriter +import java.io.StringWriter + +@RequiresApi(Build.VERSION_CODES.O) +internal class OneSignalCrashReporterOtel( + val _openTelemetry: IOneSignalOpenTelemetry +) : IOneSignalCrashReporter { + companion object { + private const val EXCEPTION_TYPE = "exception.type" + private const val EXCEPTION_MESSAGE = "exception.message" + private const val EXCEPTION_STACKTRACE = "exception.stacktrace" + } + + override suspend fun sendCrash(therad: Thread, throwable: Throwable) { + Log.e("OSCrashHandling", "sendCrash TOP") + val attributesBuilder = + Attributes + .builder() + .put(EXCEPTION_STACKTRACE, throwable.stackTraceToString()) + .put(EXCEPTION_TYPE, throwable.javaClass.name) + .build() + // TODO:1: Remaining attributes + // TODO:1.1: process name: +// final String processName = ActivityThread.currentProcessName(); +// if (processName != null) { +// message.append("Process: ").append(processName).append(", "); +// } + + _openTelemetry.logger + .logRecordBuilder() + .setAllAttributes(attributesBuilder) + .emit() + + _openTelemetry.forceFlush() + Log.e("OSCrashHandling", "sendCrash BOTTOM") + } + + private fun stackTraceToString(throwable: Throwable): String { + val stringWriter = StringWriter(256) + val printWriter = PrintWriter(stringWriter) + + throwable.printStackTrace(printWriter) + printWriter.flush() + + return stringWriter.toString() + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt new file mode 100644 index 000000000..5b9ab55ca --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt @@ -0,0 +1,126 @@ +package com.onesignal.debug.internal.logging.otel + +import android.os.Build +import androidx.annotation.RequiresApi +import com.onesignal.core.internal.config.ConfigModel +import com.onesignal.core.internal.config.ConfigModelStore +import io.opentelemetry.api.logs.Logger +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.logs.LogLimits +import io.opentelemetry.sdk.logs.LogRecordProcessor +import io.opentelemetry.sdk.logs.SdkLoggerProvider +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor +import io.opentelemetry.sdk.logs.export.LogRecordExporter +import io.opentelemetry.sdk.resources.Resource +import io.opentelemetry.semconv.ServiceAttributes +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +internal object LogLimitsConfig { + fun logLimits(): LogLimits = + LogLimits + .builder() + .setMaxNumberOfAttributes(128) + // We want a high value max length as the exception.stacktrace + // value can be lengthly. + .setMaxAttributeValueLength(32000) + .build() +} + +internal object LogRecordProcessorConfig { + @RequiresApi(Build.VERSION_CODES.O) + fun batchLogRecordProcessor(logRecordExporter: LogRecordExporter): LogRecordProcessor = + BatchLogRecordProcessor + .builder(logRecordExporter) + .setMaxQueueSize(100) + .setMaxExportBatchSize(100) + .setExporterTimeout(Duration.ofSeconds(30)) + .setScheduleDelay(Duration.ofSeconds(1)) + .build() +} + +internal object LogRecordExporterConfig { + @RequiresApi(Build.VERSION_CODES.O) + fun otlpHttpLogRecordExporter( + headers: Map, + endpoint: String, + ): LogRecordExporter { + val builder = OtlpHttpLogRecordExporter.builder() + headers.forEach { builder.addHeader(it.key, it.value) } + builder + .setEndpoint(endpoint) + .setTimeout(Duration.ofSeconds(10)) + return builder.build() + } +} + +internal object SdkLoggerProviderConfig { + // TODO: Switch to sdklogs.onesignal.com + const val BASE_URL = "https://api.honeycomb.io:443" + + @RequiresApi(Build.VERSION_CODES.O) + fun create( + resource: Resource, + extraHttpHeaders: Map, + ): SdkLoggerProvider = + SdkLoggerProvider + .builder() + .setResource(resource) + .addLogRecordProcessor( + LogRecordProcessorConfig.batchLogRecordProcessor( + LogRecordExporterConfig.otlpHttpLogRecordExporter( + extraHttpHeaders, + "${BASE_URL}/v1/logs" + ) + ) + ).setLogLimits(LogLimitsConfig::logLimits) + .build() +} + +internal object ResourceConfig { + fun create(configModel: ConfigModel): Resource = + Resource + .getDefault() + .toBuilder() +// .put(ServiceAttributes.SERVICE_NAME, "OneSignalDeviceSDK") + .put(ServiceAttributes.SERVICE_NAME, "OS-Android-SDK-Test") + .put("ossdk.app_id", configModel.appId) + // TODO: other fields + // TODO: Why not set all top level fields here? Use a top level provider + .build() +} + +@RequiresApi(Build.VERSION_CODES.O) +internal class OneSignalOpenTelemetry( + private val _configModelStore: ConfigModelStore, +) : IOneSignalOpenTelemetry { + private val sdk: OpenTelemetrySdk by lazy { + val extraHttpHeaders = + mapOf( + "OS-App-Id" to "value", + "x-honeycomb-team" to "", // TODO: REMOVE + ) + OpenTelemetrySdk + .builder() + .setLoggerProvider( + SdkLoggerProviderConfig.create( + ResourceConfig.create(_configModelStore.model), + extraHttpHeaders + ) + ).build() + } + + override val logger: Logger + get() = sdk.sdkLoggerProvider.loggerBuilder("loggerBuilder").build() + + override suspend fun forceFlush(): CompletableResultCode = + suspendCoroutine { + it.resume( + sdk.sdkLoggerProvider.forceFlush().join(10, TimeUnit.SECONDS) + ) + } +} From 8a71c35c9c55139f85fd6625b994b7b2a82e7d75 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Tue, 4 Nov 2025 21:14:05 -0500 Subject: [PATCH 2/8] feat: disk buffering for Otel Adding disk-buffering to otel logging required bumping to Kotlin 2.2. Adding the code to implement this in a follow up commit. --- OneSignalSDK/build.gradle | 2 +- OneSignalSDK/onesignal/core/build.gradle | 12 +++++++----- .../impl/NotificationRestoreWorkManager.kt | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/OneSignalSDK/build.gradle b/OneSignalSDK/build.gradle index bee581d40..d6991ba3b 100644 --- a/OneSignalSDK/build.gradle +++ b/OneSignalSDK/build.gradle @@ -14,7 +14,7 @@ buildscript { huaweiAgconnectVersion = '1.9.1.304' huaweiHMSPushVersion = '6.3.0.304' huaweiHMSLocationVersion = '4.0.0.300' - kotlinVersion = '1.9.25' + kotlinVersion = '2.2.21' coroutinesVersion = '1.7.3' kotestVersion = '5.8.0' ioMockVersion = '1.13.2' diff --git a/OneSignalSDK/onesignal/core/build.gradle b/OneSignalSDK/onesignal/core/build.gradle index 0c5862515..a581fa085 100644 --- a/OneSignalSDK/onesignal/core/build.gradle +++ b/OneSignalSDK/onesignal/core/build.gradle @@ -88,12 +88,14 @@ dependencies { } } - implementation('io.opentelemetry:opentelemetry-api:1.55.0') - implementation('io.opentelemetry:opentelemetry-sdk:1.55.0') - // Docs sounds like okhttp is already included... - implementation('io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.55.0') - implementation('io.opentelemetry:opentelemetry-exporter-otlp:1.55.0') + implementation platform("io.opentelemetry:opentelemetry-bom:1.55.0") + + implementation('io.opentelemetry:opentelemetry-api') + implementation('io.opentelemetry:opentelemetry-sdk') + + implementation('io.opentelemetry:opentelemetry-exporter-otlp') // includes okhttp implementation('io.opentelemetry.semconv:opentelemetry-semconv:1.37.0') + implementation('io.opentelemetry.contrib:opentelemetry-disk-buffering:1.51.0-alpha') // TODO: Make sure by adding Android OTel that it doesn't auto send stuff to us diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreWorkManager.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreWorkManager.kt index 1f91f2b4c..78d9c4718 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreWorkManager.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/restoration/impl/NotificationRestoreWorkManager.kt @@ -17,13 +17,14 @@ internal class NotificationRestoreWorkManager : INotificationRestoreWorkManager // Notifications will never be force removed when the app's process is running, // so we only need to restore at most once per cold start of the app. private var restored = false + private val lock = Any() override fun beginEnqueueingWork( context: Context, shouldDelay: Boolean, ) { // Only allow one piece of work to be enqueued. - synchronized(restored) { + synchronized(lock) { if (restored) { return } From 08dbcac734bea707b4fcf4b8766307968f3dcec1 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Tue, 4 Nov 2025 21:19:28 -0500 Subject: [PATCH 3/8] feat: Otel crash reports log to disk When the app is started again we send any pending crash reports. --- .../sdktest/application/MainApplicationKT.kt | 4 +- .../java/com/onesignal/core/CoreModule.kt | 18 ++- .../internal/crash/IOneSignalCrashHandler.kt | 3 - .../internal/crash/OneSignalCrashHandler.kt | 35 +++-- .../otel/IOneSignalCrashConfigProvider.kt | 7 + .../logging/otel/IOneSignalOpenTelemetry.kt | 10 +- .../otel/OneSignalCrashConfigProvider.kt | 17 +++ .../otel/OneSignalCrashReporterOtel.kt | 2 +- .../logging/otel/OneSignalCrashUploader.kt | 60 ++++++++ .../logging/otel/OneSignalOpenTelemetry.kt | 128 ++++++------------ .../otel/config/OtelConfigCrashFile.kt | 53 ++++++++ .../otel/config/OtelConfigRemoteOneSignal.kt | 57 ++++++++ .../logging/otel/config/OtelConfigShared.kt | 50 +++++++ 13 files changed, 334 insertions(+), 110 deletions(-) delete mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashHandler.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalCrashConfigProvider.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashConfigProvider.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashUploader.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigRemoteOneSignal.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt index 450b726fe..f5f78029b 100644 --- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt +++ b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplicationKT.kt @@ -83,8 +83,8 @@ class MainApplicationKT : MultiDexApplication() { Log.d(Tag.LOG_TAG, Text.ONESIGNAL_SDK_INIT) - delay(5000) - throw RuntimeException("test crash 2025-10-31") + delay(3000) + //throw RuntimeException("test crash 2025-11-04 18") } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt index 102d3e29b..5175d3c70 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt @@ -33,12 +33,17 @@ import com.onesignal.core.internal.purchases.impl.TrackGooglePurchase import com.onesignal.core.internal.startup.IStartableService import com.onesignal.core.internal.time.ITime import com.onesignal.core.internal.time.impl.Time -import com.onesignal.debug.internal.crash.IOneSignalCrashHandler import com.onesignal.debug.internal.crash.IOneSignalCrashReporter import com.onesignal.debug.internal.crash.OneSignalCrashHandler +import com.onesignal.debug.internal.logging.otel.IOneSignalCrashConfigProvider import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetry +import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryCrash +import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryRemote +import com.onesignal.debug.internal.logging.otel.OneSignalCrashConfigProvider import com.onesignal.debug.internal.logging.otel.OneSignalCrashReporterOtel -import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetry +import com.onesignal.debug.internal.logging.otel.OneSignalCrashUploader +import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetryCrashLocal +import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetryRemote import com.onesignal.inAppMessages.IInAppMessagesManager import com.onesignal.inAppMessages.internal.MisconfiguredIAMManager import com.onesignal.location.ILocationManager @@ -88,10 +93,15 @@ internal class CoreModule : IModule { builder.register().provides() // TODO: Should be a startable service instead (but we need to wait for the app id...) + builder.register().provides() builder.register().provides() - builder.register().provides() - builder.register().provides() + builder.register().provides() + builder.register().provides() + builder.register().provides() + + builder.register().provides() + builder.register().provides() // Register dummy services in the event they are not configured. These dummy services // will throw an error message if the associated functionality is attempted to be used. diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashHandler.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashHandler.kt deleted file mode 100644 index 59503b49c..000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashHandler.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.onesignal.debug.internal.crash - -interface IOneSignalCrashHandler diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt index 16961e2a9..7ced04094 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt @@ -1,26 +1,41 @@ package com.onesignal.debug.internal.crash import android.util.Log +import com.onesignal.core.internal.startup.IStartableService import kotlinx.coroutines.runBlocking -// NOTE: For future refactors, code is written assuming this is a singleton +/** + * Purpose: Writes any crashes involving OneSignal to a file where they can + * later be send to OneSignal to help improve reliability. + * NOTE: For future refactors, code is written assuming this is a singleton + */ internal class OneSignalCrashHandler( private val _crashReporter: IOneSignalCrashReporter, -) : IOneSignalCrashHandler, +) : IStartableService, Thread.UncaughtExceptionHandler { - private val existingHandler: Thread.UncaughtExceptionHandler? = - Thread.getDefaultUncaughtExceptionHandler() + private var existingHandler: Thread.UncaughtExceptionHandler? = null + private val seenThrowables: MutableList = mutableListOf() - // TODO: Write the code to call this after we get the appId - // Recommend we only create an instance after getting a appId, otherwise there - // is no point setting up the handler. - init { + override fun start() { + existingHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler(this) } override fun uncaughtException(thread: Thread, throwable: Throwable) { - // TODO: Catch anything we may throw and silence it (print only to logcat) - // TODO: Add stackoverflow loop prevention + // Ensure we never attempt to process the same throwable instance + // more than once. This would only happen if there was another crash + // handler faulty in a specific way. + synchronized(seenThrowables) { + if (seenThrowables.contains(throwable)) + return + seenThrowables.add(throwable) + } + + // TODO: Catch anything we may throw and print only to logcat + // TODO: Also send a stop command to OneSignalCrashUploader, + // give a bit of time to finish and then call existingHandler. + // * This way the app doesn't have to open a 2nd time to get the + // crash report and should help prevent duplicated reports. Log.e("OSCrashHandling", "uncaughtException TOP") if (!isOneSignalAtFault(throwable)) { existingHandler?.uncaughtException(thread, throwable) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalCrashConfigProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalCrashConfigProvider.kt new file mode 100644 index 000000000..b15f7160a --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalCrashConfigProvider.kt @@ -0,0 +1,7 @@ +package com.onesignal.debug.internal.logging.otel + +interface IOneSignalCrashConfigProvider { + val path: String + + val minFileAgeForReadMillis: Long +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt index 51392f4f7..a6de9c75b 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt @@ -1,13 +1,17 @@ package com.onesignal.debug.internal.logging.otel -import android.os.Build -import androidx.annotation.RequiresApi import io.opentelemetry.api.logs.Logger import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.logs.export.LogRecordExporter -@RequiresApi(Build.VERSION_CODES.O) internal interface IOneSignalOpenTelemetry { val logger: Logger suspend fun forceFlush(): CompletableResultCode } + +internal interface IOneSignalOpenTelemetryCrash : IOneSignalOpenTelemetry + +internal interface IOneSignalOpenTelemetryRemote : IOneSignalOpenTelemetry { + val logExporter: LogRecordExporter +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashConfigProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashConfigProvider.kt new file mode 100644 index 000000000..21b16470c --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashConfigProvider.kt @@ -0,0 +1,17 @@ +package com.onesignal.debug.internal.logging.otel + +import com.onesignal.core.internal.application.IApplicationService +import java.io.File + +class OneSignalCrashConfigProvider( + private val _applicationService: IApplicationService +) : IOneSignalCrashConfigProvider { + override val path: String by lazy { + _applicationService.appContext.cacheDir.path + File.separator + + "onesignal" + File.separator + + "otel" + File.separator + + "crashes" + } + + override val minFileAgeForReadMillis: Long = 5_000 +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt index 75e1413ab..94d49829c 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt @@ -10,7 +10,7 @@ import java.io.StringWriter @RequiresApi(Build.VERSION_CODES.O) internal class OneSignalCrashReporterOtel( - val _openTelemetry: IOneSignalOpenTelemetry + val _openTelemetry: IOneSignalOpenTelemetryCrash ) : IOneSignalCrashReporter { companion object { private const val EXCEPTION_TYPE = "exception.type" diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashUploader.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashUploader.kt new file mode 100644 index 000000000..5a2b6b105 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashUploader.kt @@ -0,0 +1,60 @@ +package com.onesignal.debug.internal.logging.otel + +import com.onesignal.core.internal.startup.IStartableService +import com.onesignal.debug.internal.logging.otel.config.OtelConfigCrashFile +import io.opentelemetry.sdk.logs.data.LogRecordData +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import java.util.concurrent.TimeUnit + + +/** + * Purpose: This reads a local crash report files created by OneSignal's + * crash handler and sends them to OneSignal on the app's next start. + */ +internal class OneSignalCrashUploader( + private val _openTelemetryRemote: IOneSignalOpenTelemetryRemote, + private val _crashPathProvider: IOneSignalCrashConfigProvider, +) : IStartableService { + companion object { + const val SEND_TIMEOUT_SECONDS = 30L + } + + private fun getReports() = + OtelConfigCrashFile.SdkLoggerProviderConfig + .getFileLogRecordStorage( + _crashPathProvider.path, + _crashPathProvider.minFileAgeForReadMillis + ).iterator() + + override fun start() { + runBlocking { internalStart() } + } + + /** + * NOTE: sendCrashReports is called twice for the these reasons: + * 1. We want to send crash reports as soon as possible. + * - App may crash quickly after starting a 2nd time. + * 2. Reports could be delayed until the 2nd start after a crash + * - Otel doesn't let you read a file it could be writing so we must + * wait a minium amount of time after a crash to ensure we get the + * report from the last crash. + */ + suspend fun internalStart() { + sendCrashReports(getReports()) + delay(_crashPathProvider.minFileAgeForReadMillis) + sendCrashReports(getReports()) + } + + private fun sendCrashReports(reports: Iterator>) { + val networkExporter = _openTelemetryRemote.logExporter + var failed = false + // NOTE: next() will delete the previous report, so we only want to send + // another one if there isn't an issue making network calls. + while (reports.hasNext() && !failed) { + val future = networkExporter.export(reports.next()) + val result = future.join(SEND_TIMEOUT_SECONDS, TimeUnit.SECONDS) + failed = !result.isSuccess + } + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt index 5b9ab55ca..c06b5952f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt @@ -2,116 +2,70 @@ package com.onesignal.debug.internal.logging.otel import android.os.Build import androidx.annotation.RequiresApi -import com.onesignal.core.internal.config.ConfigModel import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.debug.internal.logging.otel.config.OtelConfigCrashFile +import com.onesignal.debug.internal.logging.otel.config.OtelConfigRemoteOneSignal +import com.onesignal.debug.internal.logging.otel.config.OtelConfigShared import io.opentelemetry.api.logs.Logger -import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.common.CompletableResultCode -import io.opentelemetry.sdk.logs.LogLimits -import io.opentelemetry.sdk.logs.LogRecordProcessor -import io.opentelemetry.sdk.logs.SdkLoggerProvider -import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor -import io.opentelemetry.sdk.logs.export.LogRecordExporter -import io.opentelemetry.sdk.resources.Resource -import io.opentelemetry.semconv.ServiceAttributes -import java.time.Duration import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -internal object LogLimitsConfig { - fun logLimits(): LogLimits = - LogLimits - .builder() - .setMaxNumberOfAttributes(128) - // We want a high value max length as the exception.stacktrace - // value can be lengthly. - .setMaxAttributeValueLength(32000) - .build() -} - -internal object LogRecordProcessorConfig { - @RequiresApi(Build.VERSION_CODES.O) - fun batchLogRecordProcessor(logRecordExporter: LogRecordExporter): LogRecordProcessor = - BatchLogRecordProcessor - .builder(logRecordExporter) - .setMaxQueueSize(100) - .setMaxExportBatchSize(100) - .setExporterTimeout(Duration.ofSeconds(30)) - .setScheduleDelay(Duration.ofSeconds(1)) - .build() -} - -internal object LogRecordExporterConfig { - @RequiresApi(Build.VERSION_CODES.O) - fun otlpHttpLogRecordExporter( - headers: Map, - endpoint: String, - ): LogRecordExporter { - val builder = OtlpHttpLogRecordExporter.builder() - headers.forEach { builder.addHeader(it.key, it.value) } - builder - .setEndpoint(endpoint) - .setTimeout(Duration.ofSeconds(10)) - return builder.build() +@RequiresApi(Build.VERSION_CODES.O) +internal class OneSignalOpenTelemetryRemote( + private val _configModelStore: ConfigModelStore, +) : IOneSignalOpenTelemetryRemote { + val extraHttpHeaders by lazy { + mapOf( + "OS-App-Id" to _configModelStore.model.appId, + "x-honeycomb-team" to "", // TODO: REMOVE + ) } -} -internal object SdkLoggerProviderConfig { - // TODO: Switch to sdklogs.onesignal.com - const val BASE_URL = "https://api.honeycomb.io:443" + override val logExporter by lazy { + OtelConfigRemoteOneSignal.HttpRecordBatchExporter.create(extraHttpHeaders) + } - @RequiresApi(Build.VERSION_CODES.O) - fun create( - resource: Resource, - extraHttpHeaders: Map, - ): SdkLoggerProvider = - SdkLoggerProvider + private val sdk: OpenTelemetrySdk by lazy { + OpenTelemetrySdk .builder() - .setResource(resource) - .addLogRecordProcessor( - LogRecordProcessorConfig.batchLogRecordProcessor( - LogRecordExporterConfig.otlpHttpLogRecordExporter( - extraHttpHeaders, - "${BASE_URL}/v1/logs" - ) + .setLoggerProvider( + OtelConfigRemoteOneSignal.SdkLoggerProviderConfig.create( + OtelConfigShared.ResourceConfig.create(_configModelStore.model), + extraHttpHeaders ) - ).setLogLimits(LogLimitsConfig::logLimits) - .build() -} + ).build() + } -internal object ResourceConfig { - fun create(configModel: ConfigModel): Resource = - Resource - .getDefault() - .toBuilder() -// .put(ServiceAttributes.SERVICE_NAME, "OneSignalDeviceSDK") - .put(ServiceAttributes.SERVICE_NAME, "OS-Android-SDK-Test") - .put("ossdk.app_id", configModel.appId) - // TODO: other fields - // TODO: Why not set all top level fields here? Use a top level provider - .build() + override val logger: Logger + get() = sdk.sdkLoggerProvider.loggerBuilder("loggerBuilder").build() + + override suspend fun forceFlush(): CompletableResultCode = + suspendCoroutine { + it.resume( + sdk.sdkLoggerProvider.forceFlush().join(10, TimeUnit.SECONDS) + ) + } } @RequiresApi(Build.VERSION_CODES.O) -internal class OneSignalOpenTelemetry( +internal class OneSignalOpenTelemetryCrashLocal( private val _configModelStore: ConfigModelStore, -) : IOneSignalOpenTelemetry { + private val _crashPathProvider: IOneSignalCrashConfigProvider, +) : IOneSignalOpenTelemetryCrash { private val sdk: OpenTelemetrySdk by lazy { - val extraHttpHeaders = - mapOf( - "OS-App-Id" to "value", - "x-honeycomb-team" to "", // TODO: REMOVE - ) OpenTelemetrySdk .builder() .setLoggerProvider( - SdkLoggerProviderConfig.create( - ResourceConfig.create(_configModelStore.model), - extraHttpHeaders + OtelConfigCrashFile.SdkLoggerProviderConfig.create( + OtelConfigShared.ResourceConfig.create(_configModelStore.model), + _crashPathProvider.path, + _crashPathProvider.minFileAgeForReadMillis, ) - ).build() + ) + .build() } override val logger: Logger diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt new file mode 100644 index 000000000..213eabb70 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt @@ -0,0 +1,53 @@ +package com.onesignal.debug.internal.logging.otel.config + +import android.os.Build +import androidx.annotation.RequiresApi +import com.onesignal.debug.internal.logging.otel.config.OtelConfigShared.LogLimitsConfig +import com.onesignal.debug.internal.logging.otel.config.OtelConfigShared.LogLimitsConfig.logLimits +import io.opentelemetry.contrib.disk.buffering.exporters.LogRecordToDiskExporter +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileLogRecordStorage +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileStorageConfiguration +import io.opentelemetry.sdk.logs.SdkLoggerProvider +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor +import io.opentelemetry.sdk.resources.Resource +import java.io.File +import kotlin.time.Duration.Companion.hours + +class OtelConfigCrashFile { + internal object SdkLoggerProviderConfig { + fun getFileLogRecordStorage( + rootDir: String, + minFileAgeForReadMillis: Long + ): FileLogRecordStorage = + FileLogRecordStorage.create( + File(rootDir), + FileStorageConfiguration + .builder() + // NOTE: Only use such as small maxFileAgeForWrite for + // crashes, as we want to send them as soon as possible + // without have to wait too long for buffers. + .setMaxFileAgeForWriteMillis(2_000) + .setMinFileAgeForReadMillis(minFileAgeForReadMillis) + .setMaxFileAgeForReadMillis(72.hours.inWholeMilliseconds) + .build() + ) + + fun create( + resource: Resource, + rootDir: String, + minFileAgeForReadMillis: Long, + ): SdkLoggerProvider { + val logToDiskExporter = + LogRecordToDiskExporter + .builder(getFileLogRecordStorage(rootDir, minFileAgeForReadMillis)) + .build() + return SdkLoggerProvider + .builder() + .setResource(resource) + .addLogRecordProcessor( + BatchLogRecordProcessor.builder(logToDiskExporter).build() + ).setLogLimits(LogLimitsConfig::logLimits) + .build() + } + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigRemoteOneSignal.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigRemoteOneSignal.kt new file mode 100644 index 000000000..6f7023c83 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigRemoteOneSignal.kt @@ -0,0 +1,57 @@ +package com.onesignal.debug.internal.logging.otel.config + +import android.os.Build +import androidx.annotation.RequiresApi +import com.onesignal.debug.internal.logging.otel.config.OtelConfigRemoteOneSignal.SdkLoggerProviderConfig.BASE_URL +import com.onesignal.debug.internal.logging.otel.config.OtelConfigShared.LogLimitsConfig +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter +import io.opentelemetry.sdk.logs.SdkLoggerProvider +import io.opentelemetry.sdk.logs.export.LogRecordExporter +import io.opentelemetry.sdk.resources.Resource +import java.time.Duration + +internal class OtelConfigRemoteOneSignal { + object LogRecordExporterConfig { + @RequiresApi(Build.VERSION_CODES.O) + fun otlpHttpLogRecordExporter( + headers: Map, + endpoint: String, + ): LogRecordExporter { + val builder = OtlpHttpLogRecordExporter.builder() + headers.forEach { builder.addHeader(it.key, it.value) } + builder + .setEndpoint(endpoint) + .setTimeout(Duration.ofSeconds(10)) + return builder.build() + } + } + + object SdkLoggerProviderConfig { + // TODO: Switch to sdklogs.onesignal.com + const val BASE_URL = "https://api.honeycomb.io:443" + + @RequiresApi(Build.VERSION_CODES.O) + fun create( + resource: Resource, + extraHttpHeaders: Map, + ): SdkLoggerProvider = + SdkLoggerProvider + .builder() + .setResource(resource) + .addLogRecordProcessor( + OtelConfigShared.LogRecordProcessorConfig.batchLogRecordProcessor( + HttpRecordBatchExporter.create(extraHttpHeaders) + ) + ).setLogLimits(LogLimitsConfig::logLimits) + .build() + } + + object HttpRecordBatchExporter { + @RequiresApi(Build.VERSION_CODES.O) + fun create(extraHttpHeaders: Map) = + LogRecordExporterConfig.otlpHttpLogRecordExporter( + extraHttpHeaders, + "${BASE_URL}/v1/logs" + ) + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt new file mode 100644 index 000000000..61d65362a --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt @@ -0,0 +1,50 @@ +package com.onesignal.debug.internal.logging.otel.config + +import android.os.Build +import androidx.annotation.RequiresApi +import com.onesignal.core.internal.config.ConfigModel +import io.opentelemetry.sdk.logs.LogLimits +import io.opentelemetry.sdk.logs.LogRecordProcessor +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor +import io.opentelemetry.sdk.logs.export.LogRecordExporter +import io.opentelemetry.sdk.resources.Resource +import io.opentelemetry.semconv.ServiceAttributes +import java.time.Duration + +internal class OtelConfigShared { + object ResourceConfig { + fun create(configModel: ConfigModel): Resource = + Resource + .getDefault() + .toBuilder() + // .put(ServiceAttributes.SERVICE_NAME, "OneSignalDeviceSDK") + .put(ServiceAttributes.SERVICE_NAME, "OS-Android-SDK-Test") + .put("ossdk.app_id", configModel.appId) + // TODO: other fields + // TODO: Why not set all top level fields here? Use a top level provider + .build() + } + + object LogRecordProcessorConfig { + @RequiresApi(Build.VERSION_CODES.O) + fun batchLogRecordProcessor(logRecordExporter: LogRecordExporter): LogRecordProcessor = + BatchLogRecordProcessor + .builder(logRecordExporter) + .setMaxQueueSize(100) + .setMaxExportBatchSize(100) + .setExporterTimeout(Duration.ofSeconds(30)) + .setScheduleDelay(Duration.ofSeconds(1)) + .build() + } + + object LogLimitsConfig { + fun logLimits(): LogLimits = + LogLimits + .builder() + .setMaxNumberOfAttributes(128) + // We want a high value max length as the exception.stacktrace + // value can be lengthly. + .setMaxAttributeValueLength(32000) + .build() + } +} From 9b0478baedd01faca255f98592b826df60a7f5b5 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Wed, 5 Nov 2025 13:46:28 -0500 Subject: [PATCH 4/8] feat: add top level fields Also refactored crash handle classes into their own namespace. Refactored IOneSignalOpenTelemetry properties into suspend functions, as some values we want to add to all Otel requests were suspend. --- .../java/com/onesignal/core/CoreModule.kt | 13 ++-- .../logging/otel/IOneSignalOpenTelemetry.kt | 2 +- .../logging/otel/OneSignalOpenTelemetry.kt | 78 ++++++++++++------- .../attributes/OneSignalOtelTopLevelFields.kt | 58 ++++++++++++++ .../otel/config/OtelConfigCrashFile.kt | 5 +- .../logging/otel/config/OtelConfigShared.kt | 15 ++-- .../IOneSignalCrashConfigProvider.kt | 2 +- .../OneSignalCrashConfigProvider.kt | 2 +- .../{ => crash}/OneSignalCrashReporterOtel.kt | 23 ++---- .../{ => crash}/OneSignalCrashUploader.kt | 5 +- 10 files changed, 135 insertions(+), 68 deletions(-) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelTopLevelFields.kt rename OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/{ => crash}/IOneSignalCrashConfigProvider.kt (65%) rename OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/{ => crash}/OneSignalCrashConfigProvider.kt (89%) rename OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/{ => crash}/OneSignalCrashReporterOtel.kt (68%) rename OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/{ => crash}/OneSignalCrashUploader.kt (91%) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt index 5175d3c70..59a3a1dcc 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt @@ -35,15 +35,16 @@ import com.onesignal.core.internal.time.ITime import com.onesignal.core.internal.time.impl.Time import com.onesignal.debug.internal.crash.IOneSignalCrashReporter import com.onesignal.debug.internal.crash.OneSignalCrashHandler -import com.onesignal.debug.internal.logging.otel.IOneSignalCrashConfigProvider +import com.onesignal.debug.internal.logging.otel.crash.IOneSignalCrashConfigProvider import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetry import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryCrash import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryRemote -import com.onesignal.debug.internal.logging.otel.OneSignalCrashConfigProvider -import com.onesignal.debug.internal.logging.otel.OneSignalCrashReporterOtel -import com.onesignal.debug.internal.logging.otel.OneSignalCrashUploader +import com.onesignal.debug.internal.logging.otel.crash.OneSignalCrashConfigProvider +import com.onesignal.debug.internal.logging.otel.crash.OneSignalCrashReporterOtel +import com.onesignal.debug.internal.logging.otel.crash.OneSignalCrashUploader import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetryCrashLocal import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetryRemote +import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelTopLevelFields import com.onesignal.inAppMessages.IInAppMessagesManager import com.onesignal.inAppMessages.internal.MisconfiguredIAMManager import com.onesignal.location.ILocationManager @@ -92,7 +93,7 @@ internal class CoreModule : IModule { // Purchase Tracking builder.register().provides() - // TODO: Should be a startable service instead (but we need to wait for the app id...) + // Remote Crash and error logging builder.register().provides() builder.register().provides() builder.register().provides() @@ -103,6 +104,8 @@ internal class CoreModule : IModule { builder.register().provides() builder.register().provides() + builder.register().provides() + // Register dummy services in the event they are not configured. These dummy services // will throw an error message if the associated functionality is attempted to be used. builder.register().provides() diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt index a6de9c75b..4d5c606bb 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt @@ -5,7 +5,7 @@ import io.opentelemetry.sdk.common.CompletableResultCode import io.opentelemetry.sdk.logs.export.LogRecordExporter internal interface IOneSignalOpenTelemetry { - val logger: Logger + suspend fun getLogger(): Logger suspend fun forceFlush(): CompletableResultCode } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt index c06b5952f..831ffd498 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt @@ -3,9 +3,11 @@ package com.onesignal.debug.internal.logging.otel import android.os.Build import androidx.annotation.RequiresApi import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelTopLevelFields import com.onesignal.debug.internal.logging.otel.config.OtelConfigCrashFile import com.onesignal.debug.internal.logging.otel.config.OtelConfigRemoteOneSignal import com.onesignal.debug.internal.logging.otel.config.OtelConfigShared +import com.onesignal.debug.internal.logging.otel.crash.IOneSignalCrashConfigProvider import io.opentelemetry.api.logs.Logger import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.common.CompletableResultCode @@ -13,10 +15,46 @@ import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +internal abstract class OneSignalOpenTelemetryBase( + private val _osFields: OneSignalOtelTopLevelFields +) : IOneSignalOpenTelemetry { + private val lock = Any() + private var sdk: OpenTelemetrySdk? = null + protected suspend fun getSdk(): OpenTelemetrySdk { + val attributes = _osFields.getAttributes() + synchronized(lock) { + var localSdk = sdk + if (localSdk != null) { + return localSdk + } + + localSdk = getSdkInstance(attributes) + sdk = localSdk + return localSdk + } + } + + protected abstract fun getSdkInstance(attributes: Map): OpenTelemetrySdk + + override suspend fun forceFlush(): CompletableResultCode { + val sdkLoggerProvider = getSdk().sdkLoggerProvider + return suspendCoroutine { + it.resume( + sdkLoggerProvider.forceFlush().join(10, TimeUnit.SECONDS) + ) + } + } + + override suspend fun getLogger(): Logger = + getSdk().sdkLoggerProvider.loggerBuilder("loggerBuilder").build() +} + @RequiresApi(Build.VERSION_CODES.O) internal class OneSignalOpenTelemetryRemote( private val _configModelStore: ConfigModelStore, -) : IOneSignalOpenTelemetryRemote { + _osFields: OneSignalOtelTopLevelFields, +) : OneSignalOpenTelemetryBase(_osFields), + IOneSignalOpenTelemetryRemote { val extraHttpHeaders by lazy { mapOf( "OS-App-Id" to _configModelStore.model.appId, @@ -28,53 +66,33 @@ internal class OneSignalOpenTelemetryRemote( OtelConfigRemoteOneSignal.HttpRecordBatchExporter.create(extraHttpHeaders) } - private val sdk: OpenTelemetrySdk by lazy { + override fun getSdkInstance(attributes: Map): OpenTelemetrySdk = OpenTelemetrySdk .builder() .setLoggerProvider( OtelConfigRemoteOneSignal.SdkLoggerProviderConfig.create( - OtelConfigShared.ResourceConfig.create(_configModelStore.model), + OtelConfigShared.ResourceConfig.create(attributes), extraHttpHeaders ) ).build() - } - - override val logger: Logger - get() = sdk.sdkLoggerProvider.loggerBuilder("loggerBuilder").build() - - override suspend fun forceFlush(): CompletableResultCode = - suspendCoroutine { - it.resume( - sdk.sdkLoggerProvider.forceFlush().join(10, TimeUnit.SECONDS) - ) - } } -@RequiresApi(Build.VERSION_CODES.O) internal class OneSignalOpenTelemetryCrashLocal( - private val _configModelStore: ConfigModelStore, private val _crashPathProvider: IOneSignalCrashConfigProvider, -) : IOneSignalOpenTelemetryCrash { - private val sdk: OpenTelemetrySdk by lazy { + _osFields: OneSignalOtelTopLevelFields, +) : OneSignalOpenTelemetryBase(_osFields), + IOneSignalOpenTelemetryCrash { + override fun getSdkInstance(attributes: Map): OpenTelemetrySdk = OpenTelemetrySdk .builder() .setLoggerProvider( OtelConfigCrashFile.SdkLoggerProviderConfig.create( - OtelConfigShared.ResourceConfig.create(_configModelStore.model), + OtelConfigShared.ResourceConfig.create( + attributes + ), _crashPathProvider.path, _crashPathProvider.minFileAgeForReadMillis, ) ) .build() - } - - override val logger: Logger - get() = sdk.sdkLoggerProvider.loggerBuilder("loggerBuilder").build() - - override suspend fun forceFlush(): CompletableResultCode = - suspendCoroutine { - it.resume( - sdk.sdkLoggerProvider.forceFlush().join(10, TimeUnit.SECONDS) - ) - } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelTopLevelFields.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelTopLevelFields.kt new file mode 100644 index 000000000..c8f0da58a --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelTopLevelFields.kt @@ -0,0 +1,58 @@ +package com.onesignal.debug.internal.logging.otel.attributes + +import android.os.Build +import com.onesignal.common.AndroidUtils +import com.onesignal.common.OneSignalUtils +import com.onesignal.common.OneSignalWrapper +import com.onesignal.core.internal.application.IApplicationService +import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.device.IInstallIdService +import com.squareup.wire.internal.toUnmodifiableMap + +/** + * Purpose: Fields to be included in every Otel request that goes out. + * Requirements: Only include fields that can NOT change during runtime, + * as these are only fetched once. (Calculated fields are ok) + */ +class OneSignalOtelTopLevelFields( + private val _applicationService: IApplicationService, + private val _configModelStore: ConfigModelStore, + private val _installIdService: IInstallIdService, +) { + suspend fun getAttributes(): Map { + val attributes: MutableMap = + mutableMapOf( + "ossdk.app_id" to _configModelStore.model.appId, + "ossdk.install_id" to _installIdService.getId().toString(), + "ossdk.sdk_base" to "android", + "ossdk.sdk_base_version" to OneSignalUtils.sdkVersion, + "ossdk.app_package_id" to + _applicationService.appContext.packageName, + "ossdk.app_version" to + (AndroidUtils.getAppVersion(_applicationService.appContext) ?: "unknown"), + "device.manufacturer" to Build.MANUFACTURER, + "device.model.identifier" to Build.MODEL, + "os.name" to "Android", + "os.version" to Build.VERSION.RELEASE, + "os.build_id" to Build.ID, + ) + + attributes + .putIfValueNotNull( + "ossdk.sdk_wrapper", + OneSignalWrapper.sdkType + ) + .putIfValueNotNull( + "ossdk.sdk_wrapper_version", + OneSignalWrapper.sdkVersion + ) + + return attributes.toUnmodifiableMap() + } +} + +internal fun MutableMap.putIfValueNotNull(key: K, value: V?): MutableMap { + if (value != null) + this[key] = value + return this +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt index 213eabb70..612f1f8c7 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt @@ -1,9 +1,6 @@ package com.onesignal.debug.internal.logging.otel.config -import android.os.Build -import androidx.annotation.RequiresApi import com.onesignal.debug.internal.logging.otel.config.OtelConfigShared.LogLimitsConfig -import com.onesignal.debug.internal.logging.otel.config.OtelConfigShared.LogLimitsConfig.logLimits import io.opentelemetry.contrib.disk.buffering.exporters.LogRecordToDiskExporter import io.opentelemetry.contrib.disk.buffering.storage.impl.FileLogRecordStorage import io.opentelemetry.contrib.disk.buffering.storage.impl.FileStorageConfiguration @@ -25,7 +22,7 @@ class OtelConfigCrashFile { .builder() // NOTE: Only use such as small maxFileAgeForWrite for // crashes, as we want to send them as soon as possible - // without have to wait too long for buffers. + // without having to wait too long for buffers. .setMaxFileAgeForWriteMillis(2_000) .setMinFileAgeForReadMillis(minFileAgeForReadMillis) .setMaxFileAgeForReadMillis(72.hours.inWholeMilliseconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt index 61d65362a..8c0d7242a 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt @@ -2,26 +2,29 @@ package com.onesignal.debug.internal.logging.otel.config import android.os.Build import androidx.annotation.RequiresApi -import com.onesignal.core.internal.config.ConfigModel import io.opentelemetry.sdk.logs.LogLimits import io.opentelemetry.sdk.logs.LogRecordProcessor import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor import io.opentelemetry.sdk.logs.export.LogRecordExporter import io.opentelemetry.sdk.resources.Resource +import io.opentelemetry.sdk.resources.ResourceBuilder import io.opentelemetry.semconv.ServiceAttributes import java.time.Duration +internal fun ResourceBuilder.putAll(attributes: Map): ResourceBuilder { + attributes.forEach { this.put(it.key, it.value) } + return this +} + internal class OtelConfigShared { object ResourceConfig { - fun create(configModel: ConfigModel): Resource = + fun create(attributes: Map): Resource = Resource .getDefault() .toBuilder() - // .put(ServiceAttributes.SERVICE_NAME, "OneSignalDeviceSDK") + // .put(ServiceAttributes.SERVICE_NAME, "OneSignalDeviceSDK") .put(ServiceAttributes.SERVICE_NAME, "OS-Android-SDK-Test") - .put("ossdk.app_id", configModel.appId) - // TODO: other fields - // TODO: Why not set all top level fields here? Use a top level provider + .putAll(attributes) .build() } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalCrashConfigProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/IOneSignalCrashConfigProvider.kt similarity index 65% rename from OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalCrashConfigProvider.kt rename to OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/IOneSignalCrashConfigProvider.kt index b15f7160a..e6ba9fd0f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalCrashConfigProvider.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/IOneSignalCrashConfigProvider.kt @@ -1,4 +1,4 @@ -package com.onesignal.debug.internal.logging.otel +package com.onesignal.debug.internal.logging.otel.crash interface IOneSignalCrashConfigProvider { val path: String diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashConfigProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashConfigProvider.kt similarity index 89% rename from OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashConfigProvider.kt rename to OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashConfigProvider.kt index 21b16470c..4648d56d5 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashConfigProvider.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashConfigProvider.kt @@ -1,4 +1,4 @@ -package com.onesignal.debug.internal.logging.otel +package com.onesignal.debug.internal.logging.otel.crash import com.onesignal.core.internal.application.IApplicationService import java.io.File diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt similarity index 68% rename from OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt rename to OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt index 94d49829c..84ad67a26 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashReporterOtel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt @@ -1,14 +1,10 @@ -package com.onesignal.debug.internal.logging.otel +package com.onesignal.debug.internal.logging.otel.crash -import android.os.Build import android.util.Log -import androidx.annotation.RequiresApi import com.onesignal.debug.internal.crash.IOneSignalCrashReporter +import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryCrash import io.opentelemetry.api.common.Attributes -import java.io.PrintWriter -import java.io.StringWriter -@RequiresApi(Build.VERSION_CODES.O) internal class OneSignalCrashReporterOtel( val _openTelemetry: IOneSignalOpenTelemetryCrash ) : IOneSignalCrashReporter { @@ -18,11 +14,12 @@ internal class OneSignalCrashReporterOtel( private const val EXCEPTION_STACKTRACE = "exception.stacktrace" } - override suspend fun sendCrash(therad: Thread, throwable: Throwable) { + override suspend fun sendCrash(thread: Thread, throwable: Throwable) { Log.e("OSCrashHandling", "sendCrash TOP") val attributesBuilder = Attributes .builder() + .put(EXCEPTION_MESSAGE, throwable.message) .put(EXCEPTION_STACKTRACE, throwable.stackTraceToString()) .put(EXCEPTION_TYPE, throwable.javaClass.name) .build() @@ -33,7 +30,7 @@ internal class OneSignalCrashReporterOtel( // message.append("Process: ").append(processName).append(", "); // } - _openTelemetry.logger + _openTelemetry.getLogger() .logRecordBuilder() .setAllAttributes(attributesBuilder) .emit() @@ -41,14 +38,4 @@ internal class OneSignalCrashReporterOtel( _openTelemetry.forceFlush() Log.e("OSCrashHandling", "sendCrash BOTTOM") } - - private fun stackTraceToString(throwable: Throwable): String { - val stringWriter = StringWriter(256) - val printWriter = PrintWriter(stringWriter) - - throwable.printStackTrace(printWriter) - printWriter.flush() - - return stringWriter.toString() - } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashUploader.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashUploader.kt similarity index 91% rename from OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashUploader.kt rename to OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashUploader.kt index 5a2b6b105..6089966dc 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalCrashUploader.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashUploader.kt @@ -1,13 +1,14 @@ -package com.onesignal.debug.internal.logging.otel +package com.onesignal.debug.internal.logging.otel.crash import com.onesignal.core.internal.startup.IStartableService +import com.onesignal.debug.internal.logging.otel.crash.IOneSignalCrashConfigProvider +import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryRemote import com.onesignal.debug.internal.logging.otel.config.OtelConfigCrashFile import io.opentelemetry.sdk.logs.data.LogRecordData import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import java.util.concurrent.TimeUnit - /** * Purpose: This reads a local crash report files created by OneSignal's * crash handler and sends them to OneSignal on the app's next start. From 3ed87a6fc8646ef51dedc73125f6f9fe6cdabdd5 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Wed, 5 Nov 2025 18:32:38 -0500 Subject: [PATCH 5/8] feat: add Otel shared per event fields Also made all otel types internal and a few other misc cleanup. --- .../java/com/onesignal/core/CoreModule.kt | 6 +- .../com/onesignal/core/internal/time/ITime.kt | 5 ++ .../onesignal/core/internal/time/impl/Time.kt | 6 ++ .../internal/crash/OneSignalCrashHandler.kt | 4 +- .../logging/otel/IOneSignalOpenTelemetry.kt | 4 +- .../logging/otel/OneSignalOpenTelemetry.kt | 33 +++++++--- .../attributes/OneSignalOtelFieldsPerEvent.kt | 64 +++++++++++++++++++ ...elds.kt => OneSignalOtelFieldsTopLevel.kt} | 49 ++++++++------ .../otel/config/OtelConfigCrashFile.kt | 2 +- .../logging/otel/config/OtelConfigShared.kt | 3 +- .../crash/IOneSignalCrashConfigProvider.kt | 2 +- .../crash/OneSignalCrashConfigProvider.kt | 2 +- .../otel/crash/OneSignalCrashReporterOtel.kt | 29 ++++----- .../otel/crash/OneSignalCrashUploader.kt | 1 - 14 files changed, 153 insertions(+), 57 deletions(-) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt rename OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/{OneSignalOtelTopLevelFields.kt => OneSignalOtelFieldsTopLevel.kt} (51%) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt index 59a3a1dcc..ff16b2ae1 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt @@ -44,7 +44,8 @@ import com.onesignal.debug.internal.logging.otel.crash.OneSignalCrashReporterOte import com.onesignal.debug.internal.logging.otel.crash.OneSignalCrashUploader import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetryCrashLocal import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetryRemote -import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelTopLevelFields +import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsPerEvent +import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsTopLevel import com.onesignal.inAppMessages.IInAppMessagesManager import com.onesignal.inAppMessages.internal.MisconfiguredIAMManager import com.onesignal.location.ILocationManager @@ -104,7 +105,8 @@ internal class CoreModule : IModule { builder.register().provides() builder.register().provides() - builder.register().provides() + builder.register().provides() + builder.register().provides() // Register dummy services in the event they are not configured. These dummy services // will throw an error message if the associated functionality is attempted to be used. diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/ITime.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/ITime.kt index ff35096ef..8f1824d48 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/ITime.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/ITime.kt @@ -10,4 +10,9 @@ interface ITime { * current time and midnight, January 1, 1970 UTC). */ val currentTimeMillis: Long + + /** + * Returns how long the app has been running. + */ + val processUptimeMillis: Long } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/impl/Time.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/impl/Time.kt index 231f37edf..753ef124d 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/impl/Time.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/time/impl/Time.kt @@ -1,8 +1,14 @@ package com.onesignal.core.internal.time.impl +import android.os.Build +import android.os.SystemClock +import androidx.annotation.RequiresApi import com.onesignal.core.internal.time.ITime internal class Time : ITime { override val currentTimeMillis: Long get() = System.currentTimeMillis() + override val processUptimeMillis: Long + @RequiresApi(Build.VERSION_CODES.N) + get() = SystemClock.uptimeMillis() - android.os.Process.getStartUptimeMillis() } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt index 7ced04094..4440c37d6 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt @@ -1,6 +1,5 @@ package com.onesignal.debug.internal.crash -import android.util.Log import com.onesignal.core.internal.startup.IStartableService import kotlinx.coroutines.runBlocking @@ -24,7 +23,7 @@ internal class OneSignalCrashHandler( override fun uncaughtException(thread: Thread, throwable: Throwable) { // Ensure we never attempt to process the same throwable instance // more than once. This would only happen if there was another crash - // handler faulty in a specific way. + // handler and was faulty in a specific way. synchronized(seenThrowables) { if (seenThrowables.contains(throwable)) return @@ -36,7 +35,6 @@ internal class OneSignalCrashHandler( // give a bit of time to finish and then call existingHandler. // * This way the app doesn't have to open a 2nd time to get the // crash report and should help prevent duplicated reports. - Log.e("OSCrashHandling", "uncaughtException TOP") if (!isOneSignalAtFault(throwable)) { existingHandler?.uncaughtException(thread, throwable) return diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt index 4d5c606bb..2dd559030 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/IOneSignalOpenTelemetry.kt @@ -1,11 +1,11 @@ package com.onesignal.debug.internal.logging.otel -import io.opentelemetry.api.logs.Logger +import io.opentelemetry.api.logs.LogRecordBuilder import io.opentelemetry.sdk.common.CompletableResultCode import io.opentelemetry.sdk.logs.export.LogRecordExporter internal interface IOneSignalOpenTelemetry { - suspend fun getLogger(): Logger + suspend fun getLogger(): LogRecordBuilder suspend fun forceFlush(): CompletableResultCode } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt index 831ffd498..88a089b13 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt @@ -3,25 +3,32 @@ package com.onesignal.debug.internal.logging.otel import android.os.Build import androidx.annotation.RequiresApi import com.onesignal.core.internal.config.ConfigModelStore -import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelTopLevelFields +import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsPerEvent +import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsTopLevel import com.onesignal.debug.internal.logging.otel.config.OtelConfigCrashFile import com.onesignal.debug.internal.logging.otel.config.OtelConfigRemoteOneSignal import com.onesignal.debug.internal.logging.otel.config.OtelConfigShared import com.onesignal.debug.internal.logging.otel.crash.IOneSignalCrashConfigProvider -import io.opentelemetry.api.logs.Logger +import io.opentelemetry.api.logs.LogRecordBuilder import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.common.CompletableResultCode import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +internal fun LogRecordBuilder.setAllAttributes(attributes: Map): LogRecordBuilder { + attributes.forEach { this.setAttribute(it.key, it.value) } + return this +} + internal abstract class OneSignalOpenTelemetryBase( - private val _osFields: OneSignalOtelTopLevelFields + private val _osTopLevelFields: OneSignalOtelFieldsTopLevel, + private val _osPerEventFields: OneSignalOtelFieldsPerEvent, ) : IOneSignalOpenTelemetry { private val lock = Any() private var sdk: OpenTelemetrySdk? = null protected suspend fun getSdk(): OpenTelemetrySdk { - val attributes = _osFields.getAttributes() + val attributes = _osTopLevelFields.getAttributes() synchronized(lock) { var localSdk = sdk if (localSdk != null) { @@ -45,15 +52,20 @@ internal abstract class OneSignalOpenTelemetryBase( } } - override suspend fun getLogger(): Logger = - getSdk().sdkLoggerProvider.loggerBuilder("loggerBuilder").build() + override suspend fun getLogger(): LogRecordBuilder = + getSdk().sdkLoggerProvider + .loggerBuilder("loggerBuilder") + .build() + .logRecordBuilder() + .setAllAttributes(_osPerEventFields.getAttributes()) } @RequiresApi(Build.VERSION_CODES.O) internal class OneSignalOpenTelemetryRemote( private val _configModelStore: ConfigModelStore, - _osFields: OneSignalOtelTopLevelFields, -) : OneSignalOpenTelemetryBase(_osFields), + _osTopLevelFields: OneSignalOtelFieldsTopLevel, + _osPerEventFields: OneSignalOtelFieldsPerEvent, +) : OneSignalOpenTelemetryBase(_osTopLevelFields, _osPerEventFields), IOneSignalOpenTelemetryRemote { val extraHttpHeaders by lazy { mapOf( @@ -79,8 +91,9 @@ internal class OneSignalOpenTelemetryRemote( internal class OneSignalOpenTelemetryCrashLocal( private val _crashPathProvider: IOneSignalCrashConfigProvider, - _osFields: OneSignalOtelTopLevelFields, -) : OneSignalOpenTelemetryBase(_osFields), + _osTopLevelFields: OneSignalOtelFieldsTopLevel, + _osPerEventFields: OneSignalOtelFieldsPerEvent, +) : OneSignalOpenTelemetryBase(_osTopLevelFields, _osPerEventFields), IOneSignalOpenTelemetryCrash { override fun getSdkInstance(attributes: Map): OpenTelemetrySdk = OpenTelemetrySdk diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt new file mode 100644 index 000000000..937755fc0 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt @@ -0,0 +1,64 @@ +package com.onesignal.debug.internal.logging.otel.attributes + +import com.onesignal.common.IDManager +import com.onesignal.core.internal.application.IApplicationService +import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.time.ITime +import com.onesignal.user.internal.identity.IdentityModelStore +import com.squareup.wire.internal.toUnmodifiableMap + +internal class OneSignalOtelFieldsPerEvent( + private val _applicationService: IApplicationService, + private val _configModelStore: ConfigModelStore, + private val _identityModelStore: IdentityModelStore, + private val _time: ITime, +) { + fun getAttributes(): Map { + val attributes: MutableMap = mutableMapOf() + + attributes + .putIfValueNotNull( + "$OS_OTEL_NAMESPACE.onesignal_id", + onesignalId + ) + .putIfValueNotNull( + "$OS_OTEL_NAMESPACE.push_subscription_id", + subscriptionId + ) + + attributes.put("android.app.state", appState) + attributes.put("process.uptime", processUptime.toString()) + attributes.put("thread.name", currentThreadName) + + return attributes.toUnmodifiableMap() + } + + private val onesignalId: String? get() { + val onesignalId = _identityModelStore.model.onesignalId + if (IDManager.isLocalId(onesignalId)) { + return null + } + return onesignalId + } + + private val subscriptionId: String? get() { + val pushSubscriptionId = _configModelStore.model.pushSubscriptionId + if (pushSubscriptionId == null || + IDManager.isLocalId(pushSubscriptionId)) { + return null + } + return pushSubscriptionId + } + + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/android/ + private val appState: String get() = + if (_applicationService.isInForeground) "foreground" else "background" + + // https://opentelemetry.io/docs/specs/semconv/system/process-metrics/#metric-processuptime + private val processUptime: Double get() = + _time.processUptimeMillis / 1_000.toDouble() + + // https://opentelemetry.io/docs/specs/semconv/general/attributes/#general-thread-attributes + private val currentThreadName: String get() = + Thread.currentThread().name +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelTopLevelFields.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsTopLevel.kt similarity index 51% rename from OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelTopLevelFields.kt rename to OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsTopLevel.kt index c8f0da58a..50d929774 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelTopLevelFields.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsTopLevel.kt @@ -9,41 +9,53 @@ import com.onesignal.core.internal.config.ConfigModelStore import com.onesignal.core.internal.device.IInstallIdService import com.squareup.wire.internal.toUnmodifiableMap +// Used on all attributes / fields we add to Otel events that is NOT part of +// their spec. We do this to make it clear where the source of this field is. +internal const val OS_OTEL_NAMESPACE: String = "ossdk" + /** * Purpose: Fields to be included in every Otel request that goes out. * Requirements: Only include fields that can NOT change during runtime, * as these are only fetched once. (Calculated fields are ok) */ -class OneSignalOtelTopLevelFields( +internal class OneSignalOtelFieldsTopLevel( private val _applicationService: IApplicationService, private val _configModelStore: ConfigModelStore, private val _installIdService: IInstallIdService, ) { suspend fun getAttributes(): Map { - val attributes: MutableMap = + val attributes: MutableMap = mutableMapOf( - "ossdk.app_id" to _configModelStore.model.appId, - "ossdk.install_id" to _installIdService.getId().toString(), - "ossdk.sdk_base" to "android", - "ossdk.sdk_base_version" to OneSignalUtils.sdkVersion, - "ossdk.app_package_id" to + "$OS_OTEL_NAMESPACE.app_id" to + _configModelStore.model.appId, + "$OS_OTEL_NAMESPACE.install_id" to + _installIdService.getId().toString(), + "$OS_OTEL_NAMESPACE.sdk_base" + to "android", + "$OS_OTEL_NAMESPACE.sdk_base_version" to + OneSignalUtils.sdkVersion, + "$OS_OTEL_NAMESPACE.app_package_id" to _applicationService.appContext.packageName, - "ossdk.app_version" to + "$OS_OTEL_NAMESPACE.app_version" to (AndroidUtils.getAppVersion(_applicationService.appContext) ?: "unknown"), - "device.manufacturer" to Build.MANUFACTURER, - "device.model.identifier" to Build.MODEL, - "os.name" to "Android", - "os.version" to Build.VERSION.RELEASE, - "os.build_id" to Build.ID, + "device.manufacturer" + to Build.MANUFACTURER, + "device.model.identifier" + to Build.MODEL, + "os.name" + to "Android", + "os.version" + to Build.VERSION.RELEASE, + "os.build_id" + to Build.ID, ) attributes .putIfValueNotNull( - "ossdk.sdk_wrapper", + "$OS_OTEL_NAMESPACE.sdk_wrapper", OneSignalWrapper.sdkType - ) - .putIfValueNotNull( - "ossdk.sdk_wrapper_version", + ).putIfValueNotNull( + "$OS_OTEL_NAMESPACE.sdk_wrapper_version", OneSignalWrapper.sdkVersion ) @@ -52,7 +64,8 @@ class OneSignalOtelTopLevelFields( } internal fun MutableMap.putIfValueNotNull(key: K, value: V?): MutableMap { - if (value != null) + if (value != null) { this[key] = value + } return this } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt index 612f1f8c7..0556d75a5 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigCrashFile.kt @@ -10,7 +10,7 @@ import io.opentelemetry.sdk.resources.Resource import java.io.File import kotlin.time.Duration.Companion.hours -class OtelConfigCrashFile { +internal class OtelConfigCrashFile { internal object SdkLoggerProviderConfig { fun getFileLogRecordStorage( rootDir: String, diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt index 8c0d7242a..a5b09ef14 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigShared.kt @@ -22,8 +22,7 @@ internal class OtelConfigShared { Resource .getDefault() .toBuilder() - // .put(ServiceAttributes.SERVICE_NAME, "OneSignalDeviceSDK") - .put(ServiceAttributes.SERVICE_NAME, "OS-Android-SDK-Test") + .put(ServiceAttributes.SERVICE_NAME, "OneSignalDeviceSDK") .putAll(attributes) .build() } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/IOneSignalCrashConfigProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/IOneSignalCrashConfigProvider.kt index e6ba9fd0f..37b9741a7 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/IOneSignalCrashConfigProvider.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/IOneSignalCrashConfigProvider.kt @@ -1,6 +1,6 @@ package com.onesignal.debug.internal.logging.otel.crash -interface IOneSignalCrashConfigProvider { +internal interface IOneSignalCrashConfigProvider { val path: String val minFileAgeForReadMillis: Long diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashConfigProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashConfigProvider.kt index 4648d56d5..4e40dccaf 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashConfigProvider.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashConfigProvider.kt @@ -3,7 +3,7 @@ package com.onesignal.debug.internal.logging.otel.crash import com.onesignal.core.internal.application.IApplicationService import java.io.File -class OneSignalCrashConfigProvider( +internal class OneSignalCrashConfigProvider( private val _applicationService: IApplicationService ) : IOneSignalCrashConfigProvider { override val path: String by lazy { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt index 84ad67a26..37fb3a08b 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt @@ -1,41 +1,38 @@ package com.onesignal.debug.internal.logging.otel.crash -import android.util.Log import com.onesignal.debug.internal.crash.IOneSignalCrashReporter import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryCrash +import com.onesignal.debug.internal.logging.otel.attributes.OS_OTEL_NAMESPACE import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.logs.Severity internal class OneSignalCrashReporterOtel( val _openTelemetry: IOneSignalOpenTelemetryCrash ) : IOneSignalCrashReporter { companion object { - private const val EXCEPTION_TYPE = "exception.type" - private const val EXCEPTION_MESSAGE = "exception.message" - private const val EXCEPTION_STACKTRACE = "exception.stacktrace" + private const val OTEL_EXCEPTION_TYPE = "exception.type" + private const val OTEL_EXCEPTION_MESSAGE = "exception.message" + private const val OTEL_EXCEPTION_STACKTRACE = "exception.stacktrace" + } override suspend fun sendCrash(thread: Thread, throwable: Throwable) { - Log.e("OSCrashHandling", "sendCrash TOP") val attributesBuilder = Attributes .builder() - .put(EXCEPTION_MESSAGE, throwable.message) - .put(EXCEPTION_STACKTRACE, throwable.stackTraceToString()) - .put(EXCEPTION_TYPE, throwable.javaClass.name) + .put(OTEL_EXCEPTION_MESSAGE, throwable.message) + .put(OTEL_EXCEPTION_STACKTRACE, throwable.stackTraceToString()) + .put(OTEL_EXCEPTION_TYPE, throwable.javaClass.name) + // This matches the top level thread.name today, but it may not + // always if things are refactored to use a different thread. + .put("$OS_OTEL_NAMESPACE.exception.thread.name", thread.name) .build() - // TODO:1: Remaining attributes - // TODO:1.1: process name: -// final String processName = ActivityThread.currentProcessName(); -// if (processName != null) { -// message.append("Process: ").append(processName).append(", "); -// } _openTelemetry.getLogger() - .logRecordBuilder() .setAllAttributes(attributesBuilder) + .setSeverity(Severity.FATAL) .emit() _openTelemetry.forceFlush() - Log.e("OSCrashHandling", "sendCrash BOTTOM") } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashUploader.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashUploader.kt index 6089966dc..6346b260c 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashUploader.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashUploader.kt @@ -1,7 +1,6 @@ package com.onesignal.debug.internal.logging.otel.crash import com.onesignal.core.internal.startup.IStartableService -import com.onesignal.debug.internal.logging.otel.crash.IOneSignalCrashConfigProvider import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryRemote import com.onesignal.debug.internal.logging.otel.config.OtelConfigCrashFile import io.opentelemetry.sdk.logs.data.LogRecordData From 66708d1115635e41a3fc12d852b97d6bc9da4abe Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Wed, 5 Nov 2025 19:16:36 -0500 Subject: [PATCH 6/8] chore: improved names and lint fixes --- .../internal/crash/IOneSignalCrashReporter.kt | 2 +- .../debug/internal/crash/OneSignalCrashHandler.kt | 7 ++++--- .../logging/otel/OneSignalOpenTelemetry.kt | 15 ++++++++------- .../attributes/OneSignalOtelFieldsPerEvent.kt | 3 +-- .../otel/config/OtelConfigRemoteOneSignal.kt | 4 ++-- .../otel/crash/OneSignalCrashReporterOtel.kt | 6 +++--- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashReporter.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashReporter.kt index 368972dd3..c51391c3d 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashReporter.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/IOneSignalCrashReporter.kt @@ -1,5 +1,5 @@ package com.onesignal.debug.internal.crash internal interface IOneSignalCrashReporter { - suspend fun sendCrash(thread: Thread, throwable: Throwable) + suspend fun saveCrash(thread: Thread, throwable: Throwable) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt index 4440c37d6..65d580a42 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandler.kt @@ -25,8 +25,9 @@ internal class OneSignalCrashHandler( // more than once. This would only happen if there was another crash // handler and was faulty in a specific way. synchronized(seenThrowables) { - if (seenThrowables.contains(throwable)) + if (seenThrowables.contains(throwable)) { return + } seenThrowables.add(throwable) } @@ -55,8 +56,8 @@ internal class OneSignalCrashHandler( * * NOTE: addShutdownHook() isn't a workaround as it doesn't fire for * Process.killProcess, which KillApplicationHandler calls. - */ - runBlocking { _crashReporter.sendCrash(thread, throwable) } + */ + runBlocking { _crashReporter.saveCrash(thread, throwable) } existingHandler?.uncaughtException(thread, throwable) } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt index 88a089b13..0a5f691eb 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt @@ -26,22 +26,23 @@ internal abstract class OneSignalOpenTelemetryBase( private val _osPerEventFields: OneSignalOtelFieldsPerEvent, ) : IOneSignalOpenTelemetry { private val lock = Any() - private var sdk: OpenTelemetrySdk? = null + private var sdkCachedValue: OpenTelemetrySdk? = null + protected suspend fun getSdk(): OpenTelemetrySdk { val attributes = _osTopLevelFields.getAttributes() synchronized(lock) { - var localSdk = sdk + var localSdk = sdkCachedValue if (localSdk != null) { return localSdk } localSdk = getSdkInstance(attributes) - sdk = localSdk + sdkCachedValue = localSdk return localSdk } } - protected abstract fun getSdkInstance(attributes: Map): OpenTelemetrySdk + protected abstract fun getSdkInstance(attributes: Map): OpenTelemetrySdk override suspend fun forceFlush(): CompletableResultCode { val sdkLoggerProvider = getSdk().sdkLoggerProvider @@ -53,7 +54,8 @@ internal abstract class OneSignalOpenTelemetryBase( } override suspend fun getLogger(): LogRecordBuilder = - getSdk().sdkLoggerProvider + getSdk() + .sdkLoggerProvider .loggerBuilder("loggerBuilder") .build() .logRecordBuilder() @@ -106,6 +108,5 @@ internal class OneSignalOpenTelemetryCrashLocal( _crashPathProvider.path, _crashPathProvider.minFileAgeForReadMillis, ) - ) - .build() + ).build() } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt index 937755fc0..1f1c28165 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt @@ -20,8 +20,7 @@ internal class OneSignalOtelFieldsPerEvent( .putIfValueNotNull( "$OS_OTEL_NAMESPACE.onesignal_id", onesignalId - ) - .putIfValueNotNull( + ).putIfValueNotNull( "$OS_OTEL_NAMESPACE.push_subscription_id", subscriptionId ) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigRemoteOneSignal.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigRemoteOneSignal.kt index 6f7023c83..b6ffb30c5 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigRemoteOneSignal.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/config/OtelConfigRemoteOneSignal.kt @@ -27,7 +27,7 @@ internal class OtelConfigRemoteOneSignal { } object SdkLoggerProviderConfig { - // TODO: Switch to sdklogs.onesignal.com + // TODO: Switch to https://sdklogs.onesignal.com:443/sdk/otel const val BASE_URL = "https://api.honeycomb.io:443" @RequiresApi(Build.VERSION_CODES.O) @@ -40,7 +40,7 @@ internal class OtelConfigRemoteOneSignal { .setResource(resource) .addLogRecordProcessor( OtelConfigShared.LogRecordProcessorConfig.batchLogRecordProcessor( - HttpRecordBatchExporter.create(extraHttpHeaders) + HttpRecordBatchExporter.create(extraHttpHeaders) ) ).setLogLimits(LogLimitsConfig::logLimits) .build() diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt index 37fb3a08b..e711978ca 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/crash/OneSignalCrashReporterOtel.kt @@ -13,10 +13,9 @@ internal class OneSignalCrashReporterOtel( private const val OTEL_EXCEPTION_TYPE = "exception.type" private const val OTEL_EXCEPTION_MESSAGE = "exception.message" private const val OTEL_EXCEPTION_STACKTRACE = "exception.stacktrace" - } - override suspend fun sendCrash(thread: Thread, throwable: Throwable) { + override suspend fun saveCrash(thread: Thread, throwable: Throwable) { val attributesBuilder = Attributes .builder() @@ -28,7 +27,8 @@ internal class OneSignalCrashReporterOtel( .put("$OS_OTEL_NAMESPACE.exception.thread.name", thread.name) .build() - _openTelemetry.getLogger() + _openTelemetry + .getLogger() .setAllAttributes(attributesBuilder) .setSeverity(Severity.FATAL) .emit() From 432f84218240954bd9d69976bf6f140db2de9b74 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Wed, 5 Nov 2025 19:16:59 -0500 Subject: [PATCH 7/8] feat: Add idempotency key to Otel events --- .../otel/attributes/OneSignalOtelFieldsPerEvent.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt index 1f1c28165..eeff4da6d 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/attributes/OneSignalOtelFieldsPerEvent.kt @@ -6,6 +6,7 @@ import com.onesignal.core.internal.config.ConfigModelStore import com.onesignal.core.internal.time.ITime import com.onesignal.user.internal.identity.IdentityModelStore import com.squareup.wire.internal.toUnmodifiableMap +import java.util.UUID internal class OneSignalOtelFieldsPerEvent( private val _applicationService: IApplicationService, @@ -16,6 +17,8 @@ internal class OneSignalOtelFieldsPerEvent( fun getAttributes(): Map { val attributes: MutableMap = mutableMapOf() + attributes.put("log.record.uid", recordId.toString()) + attributes .putIfValueNotNull( "$OS_OTEL_NAMESPACE.onesignal_id", @@ -60,4 +63,9 @@ internal class OneSignalOtelFieldsPerEvent( // https://opentelemetry.io/docs/specs/semconv/general/attributes/#general-thread-attributes private val currentThreadName: String get() = Thread.currentThread().name + + // idempotency so the backend can filter on duplicate events + // https://opentelemetry.io/docs/specs/semconv/general/logs/#general-log-identification-attributes + private val recordId: UUID get() = + UUID.randomUUID() } From ce8f9b96e1e3f1e26687ddcf7382f66532e9e537 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Thu, 6 Nov 2025 16:09:49 -0500 Subject: [PATCH 8/8] fix: update HTTP headers sent to otel endpoints --- .../java/com/onesignal/core/internal/http/impl/HttpClient.kt | 5 ++++- .../debug/internal/logging/otel/OneSignalOpenTelemetry.kt | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt index 825637f31..29b5578ae 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt @@ -26,6 +26,9 @@ import java.net.UnknownHostException import java.util.Scanner import javax.net.ssl.HttpsURLConnection +internal const val HTTP_SDK_VERSION_HEADER_KEY = "SDK-Version" +internal val HTTP_SDK_VERSION_HEADER_VALUE = "onesignal/android/${OneSignalUtils.sdkVersion}" + internal class HttpClient( private val _connectionFactory: IHttpConnectionFactory, private val _prefs: IPreferencesService, @@ -131,7 +134,7 @@ internal class HttpClient( con.useCaches = false con.connectTimeout = timeout con.readTimeout = timeout - con.setRequestProperty("SDK-Version", "onesignal/android/" + OneSignalUtils.sdkVersion) + con.setRequestProperty(HTTP_SDK_VERSION_HEADER_KEY, HTTP_SDK_VERSION_HEADER_VALUE) if (OneSignalWrapper.sdkType != null && OneSignalWrapper.sdkVersion != null) { con.setRequestProperty("SDK-Wrapper", "onesignal/${OneSignalWrapper.sdkType}/${OneSignalWrapper.sdkVersion}") diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt index 0a5f691eb..f0a82c505 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/OneSignalOpenTelemetry.kt @@ -3,6 +3,8 @@ package com.onesignal.debug.internal.logging.otel import android.os.Build import androidx.annotation.RequiresApi import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.http.impl.HTTP_SDK_VERSION_HEADER_KEY +import com.onesignal.core.internal.http.impl.HTTP_SDK_VERSION_HEADER_VALUE import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsPerEvent import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsTopLevel import com.onesignal.debug.internal.logging.otel.config.OtelConfigCrashFile @@ -71,7 +73,8 @@ internal class OneSignalOpenTelemetryRemote( IOneSignalOpenTelemetryRemote { val extraHttpHeaders by lazy { mapOf( - "OS-App-Id" to _configModelStore.model.appId, + "X-OneSignal-App-Id" to _configModelStore.model.appId, + HTTP_SDK_VERSION_HEADER_KEY to HTTP_SDK_VERSION_HEADER_VALUE, "x-honeycomb-team" to "", // TODO: REMOVE ) }