diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b724a35ba..e9def9bc5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ opentelemetry = "1.35.0" opentelemetry-alpha = "1.32.1-alpha" opentelemetry-semconv = "1.21.0-alpha" -opentelemetry-contrib = "1.31.0-alpha" +opentelemetry-contrib = "1.33.0-alpha" mockito = "5.11.0" junit = "5.10.2" byteBuddy = "1.14.12" diff --git a/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java b/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java index 4a3bc1601..338e30279 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java @@ -13,6 +13,8 @@ import android.util.Log; import io.opentelemetry.android.config.OtelRumConfig; import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration; +import io.opentelemetry.android.features.diskbuffering.SignalFromDiskExporter; +import io.opentelemetry.android.features.diskbuffering.scheduler.ExportScheduleHandler; import io.opentelemetry.android.instrumentation.InstrumentedApplication; import io.opentelemetry.android.instrumentation.activity.VisibleScreenTracker; import io.opentelemetry.android.instrumentation.anr.AnrDetector; @@ -31,8 +33,9 @@ import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; -import io.opentelemetry.contrib.disk.buffering.SpanDiskExporter; -import io.opentelemetry.contrib.disk.buffering.internal.StorageConfiguration; +import io.opentelemetry.contrib.disk.buffering.SpanFromDiskExporter; +import io.opentelemetry.contrib.disk.buffering.SpanToDiskExporter; +import io.opentelemetry.contrib.disk.buffering.StorageConfiguration; import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.logs.SdkLoggerProvider; @@ -289,24 +292,80 @@ public OpenTelemetryRum build() { applyConfiguration(); + DiskBufferingConfiguration diskBufferingConfiguration = + config.getDiskBufferingConfiguration(); + SpanExporter spanExporter = buildSpanExporter(); + SignalFromDiskExporter signalFromDiskExporter = null; + if (diskBufferingConfiguration.isEnabled()) { + try { + StorageConfiguration storageConfiguration = createStorageConfiguration(); + final SpanExporter originalSpanExporter = spanExporter; + spanExporter = + SpanToDiskExporter.create(originalSpanExporter, storageConfiguration); + + signalFromDiskExporter = + new SignalFromDiskExporter( + SpanFromDiskExporter.create( + originalSpanExporter, storageConfiguration), + null, + null); + } catch (IOException e) { + Log.e(RumConstants.OTEL_RUM_LOG_TAG, "Could not initialize disk exporters.", e); + } + } + OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() - .setTracerProvider(buildTracerProvider(sessionId, application)) + .setTracerProvider( + buildTracerProvider(sessionId, application, spanExporter)) .setMeterProvider(buildMeterProvider(application)) .setLoggerProvider(buildLoggerProvider(application)) .setPropagators(buildFinalPropagators()) .build(); + scheduleDiskTelemetryReader(signalFromDiskExporter, diskBufferingConfiguration); + SdkPreconfiguredRumBuilder delegate = new SdkPreconfiguredRumBuilder(application, sdk, sessionId); instrumentationInstallers.forEach(delegate::addInstrumentation); return delegate.build(); } + private StorageConfiguration createStorageConfiguration() throws IOException { + DiskManager diskManager = DiskManager.create(config.getDiskBufferingConfiguration()); + return StorageConfiguration.builder() + .setMaxFileSize(diskManager.getMaxCacheFileSize()) + .setMaxFolderSize(diskManager.getMaxFolderSize()) + .setRootDir(diskManager.getSignalsBufferDir()) + .setTemporaryFileProvider( + new SimpleTemporaryFileProvider(diskManager.getTemporaryDir())) + .build(); + } + + private void scheduleDiskTelemetryReader( + @Nullable SignalFromDiskExporter signalExporter, + DiskBufferingConfiguration diskBufferingConfiguration) { + ExportScheduleHandler exportScheduleHandler = + diskBufferingConfiguration.getExportScheduleHandler(); + if (signalExporter == null) { + // Disabling here allows to cancel previously scheduled exports using tools that + // can run even after the app has been terminated (such as WorkManager). + // But for in-memory only schedulers, nothing should need to be disabled. + exportScheduleHandler.disable(); + } else { + // Not null means that disk buffering is enabled and disk exporters are successfully + // initialized. + SignalFromDiskExporter.set(signalExporter); + exportScheduleHandler.enable(); + } + } + /** Leverage the configuration to wire up various instrumentation components. */ private void applyConfiguration() { if (config.shouldGenerateSdkInitializationEvents()) { - initializationEvents = new SdkInitializationEvents(); + if (initializationEvents == InitializationEvents.NO_OP) { + initializationEvents = new SdkInitializationEvents(); + } initializationEvents.recordConfiguration(config); } initializationEvents.sdkInitializationStarted(); @@ -406,13 +465,14 @@ private CurrentNetworkProvider getOrCreateCurrentNetworkProvider() { return currentNetworkProvider; } - private SdkTracerProvider buildTracerProvider(SessionId sessionId, Application application) { + private SdkTracerProvider buildTracerProvider( + SessionId sessionId, Application application, SpanExporter spanExporter) { SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder() .setResource(resource) .addSpanProcessor(new SessionIdSpanAppender(sessionId)); - SpanExporter spanExporter = buildSpanExporter(); + initializationEvents.spanExporterInitialized(spanExporter); BatchSpanProcessor batchSpanProcessor = BatchSpanProcessor.builder(spanExporter).build(); tracerProviderBuilder.addSpanProcessor(batchSpanProcessor); @@ -426,32 +486,7 @@ private SdkTracerProvider buildTracerProvider(SessionId sessionId, Application a private SpanExporter buildSpanExporter() { // TODO: Default to otlp...but how can we make endpoint and auth mandatory? SpanExporter defaultExporter = LoggingSpanExporter.create(); - SpanExporter spanExporter = defaultExporter; - DiskBufferingConfiguration diskBufferingConfiguration = - config.getDiskBufferingConfiguration(); - if (diskBufferingConfiguration.isEnabled()) { - try { - spanExporter = createDiskExporter(defaultExporter, diskBufferingConfiguration); - } catch (IOException e) { - Log.w(RumConstants.OTEL_RUM_LOG_TAG, "Could not create span disk exporter.", e); - } - } - return spanExporterCustomizer.apply(spanExporter); - } - - private static SpanExporter createDiskExporter( - SpanExporter defaultExporter, DiskBufferingConfiguration diskBufferingConfiguration) - throws IOException { - DiskManager diskManager = DiskManager.create(diskBufferingConfiguration); - StorageConfiguration storageConfiguration = - StorageConfiguration.builder() - .setMaxFileSize(diskManager.getMaxCacheFileSize()) - .setMaxFolderSize(diskManager.getMaxFolderSize()) - .setTemporaryFileProvider( - new SimpleTemporaryFileProvider(diskManager.getTemporaryDir())) - .build(); - return SpanDiskExporter.create( - defaultExporter, diskManager.getSignalsBufferDir(), storageConfiguration); + return spanExporterCustomizer.apply(defaultExporter); } private SdkMeterProvider buildMeterProvider(Application application) { @@ -478,4 +513,9 @@ private ContextPropagators buildFinalPropagators() { TextMapPropagator defaultPropagator = buildDefaultPropagator(); return ContextPropagators.create(propagatorCustomizer.apply(defaultPropagator)); } + + OpenTelemetryRumBuilder setInitializationEvents(InitializationEvents initializationEvents) { + this.initializationEvents = initializationEvents; + return this; + } } diff --git a/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/DiskBufferingConfiguration.java b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/DiskBufferingConfiguration.java index a4629e96c..ec73e8a9c 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/DiskBufferingConfiguration.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/DiskBufferingConfiguration.java @@ -5,16 +5,22 @@ package io.opentelemetry.android.features.diskbuffering; +import io.opentelemetry.android.features.diskbuffering.scheduler.DefaultExportScheduleHandler; +import io.opentelemetry.android.features.diskbuffering.scheduler.DefaultExportScheduler; +import io.opentelemetry.android.features.diskbuffering.scheduler.ExportScheduleHandler; + /** Configuration for disk buffering. */ public final class DiskBufferingConfiguration { private final boolean enabled; private final int maxCacheSize; + private final ExportScheduleHandler exportScheduleHandler; private static final int DEFAULT_MAX_CACHE_SIZE = 60 * 1024 * 1024; private static final int MAX_FILE_SIZE = 1024 * 1024; private DiskBufferingConfiguration(Builder builder) { this.enabled = builder.enabled; this.maxCacheSize = builder.maxCacheSize; + this.exportScheduleHandler = builder.exportScheduleHandler; } public static Builder builder() { @@ -33,9 +39,15 @@ public int getMaxCacheFileSize() { return MAX_FILE_SIZE; } + public ExportScheduleHandler getExportScheduleHandler() { + return exportScheduleHandler; + } + public static final class Builder { private boolean enabled; private int maxCacheSize; + private ExportScheduleHandler exportScheduleHandler = + new DefaultExportScheduleHandler(new DefaultExportScheduler()); private Builder(boolean enabled, int maxCacheSize) { this.enabled = enabled; @@ -58,6 +70,15 @@ public Builder setMaxCacheSize(int maxCacheSize) { return this; } + /** + * Sets a scheduler that will take care of periodically read data stored in disk and export + * it. + */ + public Builder setExportScheduleHandler(ExportScheduleHandler exportScheduleHandler) { + this.exportScheduleHandler = exportScheduleHandler; + return this; + } + public DiskBufferingConfiguration build() { return new DiskBufferingConfiguration(this); } diff --git a/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/SignalFromDiskExporter.kt b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/SignalFromDiskExporter.kt new file mode 100644 index 000000000..e8614f8ec --- /dev/null +++ b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/SignalFromDiskExporter.kt @@ -0,0 +1,115 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.features.diskbuffering + +import androidx.annotation.WorkerThread +import io.opentelemetry.contrib.disk.buffering.LogRecordFromDiskExporter +import io.opentelemetry.contrib.disk.buffering.MetricFromDiskExporter +import io.opentelemetry.contrib.disk.buffering.SpanFromDiskExporter +import java.io.IOException +import java.util.concurrent.TimeUnit + +/** + * Entrypoint to read and export previously cached signals. + */ +class SignalFromDiskExporter + @JvmOverloads + internal constructor( + private val spanFromDiskExporter: SpanFromDiskExporter?, + private val metricFromDiskExporter: MetricFromDiskExporter?, + private val logRecordFromDiskExporter: LogRecordFromDiskExporter?, + private val exportTimeoutInMillis: Long = TimeUnit.SECONDS.toMillis(5), + ) { + /** + * A batch contains all the signals that arrived in one call to [SpanDiskExporter.export]. So if + * that function is called 5 times, then there will be 5 batches in disk. This function reads + * and exports ONE batch every time is called. + * + * @return TRUE if it found data in disk and the exporter succeeded. FALSE if any of those conditions were + * not met. + */ + @WorkerThread + @Throws(IOException::class) + fun exportBatchOfSpans(): Boolean { + return spanFromDiskExporter?.exportStoredBatch( + exportTimeoutInMillis, + TimeUnit.MILLISECONDS, + ) ?: false + } + + /** + * A batch contains all the signals that arrived in one call to [MetricDiskExporter.export]. So if + * that function is called 5 times, then there will be 5 batches in disk. This function reads + * and exports ONE batch every time is called. + * + * @return TRUE if it found data in disk and the exporter succeeded. FALSE if any of those conditions were + * not met. + */ + @WorkerThread + @Throws(IOException::class) + fun exportBatchOfMetrics(): Boolean { + return metricFromDiskExporter?.exportStoredBatch( + exportTimeoutInMillis, + TimeUnit.MILLISECONDS, + ) ?: false + } + + /** + * A batch contains all the signals that arrived in one call to [LogRecordDiskExporter.export]. So if + * that function is called 5 times, then there will be 5 batches in disk. This function reads + * and exports ONE batch every time is called. + * + * @return TRUE if it found data in disk and the exporter succeeded. FALSE if any of those conditions were + * not met. + */ + @WorkerThread + @Throws(IOException::class) + fun exportBatchOfLogs(): Boolean { + return logRecordFromDiskExporter?.exportStoredBatch( + exportTimeoutInMillis, + TimeUnit.MILLISECONDS, + ) ?: false + } + + /** + * Convenience method that attempts to export all kinds of signals from disk. + * + * @return TRUE if at least one of the signals were successfully exported, FALSE if no signal + * of any kind was exported. + */ + @WorkerThread + @Throws(IOException::class) + fun exportBatchOfEach(): Boolean { + var atLeastOneWorked = exportBatchOfSpans() + if (exportBatchOfMetrics()) { + atLeastOneWorked = true + } + if (exportBatchOfLogs()) { + atLeastOneWorked = true + } + return atLeastOneWorked + } + + companion object { + private var instance: SignalFromDiskExporter? = null + + @JvmStatic + fun get(): SignalFromDiskExporter? { + return instance + } + + @JvmStatic + fun set(signalFromDiskExporter: SignalFromDiskExporter) { + check(instance == null) { "An instance is already set. You can only set it once." } + instance = signalFromDiskExporter + } + + @JvmStatic + fun resetForTesting() { + instance = null + } + } + } diff --git a/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler.kt b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler.kt index 029a97f16..9b2cb1eba 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler.kt +++ b/instrumentation/src/main/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportScheduler.kt @@ -5,7 +5,11 @@ package io.opentelemetry.android.features.diskbuffering.scheduler +import android.util.Log +import io.opentelemetry.android.RumConstants +import io.opentelemetry.android.features.diskbuffering.SignalFromDiskExporter import io.opentelemetry.android.internal.services.periodicwork.PeriodicRunnable +import java.io.IOException import java.util.concurrent.TimeUnit class DefaultExportScheduler : PeriodicRunnable() { @@ -14,11 +18,19 @@ class DefaultExportScheduler : PeriodicRunnable() { } override fun onRun() { - // TODO for next PR. + val exporter = SignalFromDiskExporter.get() ?: return + + try { + do { + val didExport = exporter.exportBatchOfEach() + } while (didExport) + } catch (e: IOException) { + Log.e(RumConstants.OTEL_RUM_LOG_TAG, "Error while exporting signals from disk.", e) + } } override fun shouldStopRunning(): Boolean { - return false + return SignalFromDiskExporter.get() == null } override fun minimumDelayUntilNextRunInMillis(): Long { diff --git a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java index acda6b4fa..be0a5202c 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/InitializationEvents.java @@ -6,6 +6,7 @@ package io.opentelemetry.android.instrumentation.startup; import io.opentelemetry.android.config.OtelRumConfig; +import io.opentelemetry.sdk.trace.export.SpanExporter; public interface InitializationEvents { @@ -23,6 +24,8 @@ public interface InitializationEvents { void crashReportingInitialized(); + void spanExporterInitialized(SpanExporter spanExporter); + InitializationEvents NO_OP = new InitializationEvents() { @Override @@ -45,5 +48,8 @@ public void slowRenderingDetectorInitialized() {} @Override public void crashReportingInitialized() {} + + @Override + public void spanExporterInitialized(SpanExporter spanExporter) {} }; } diff --git a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java index 21dd6c669..4816c13cc 100644 --- a/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java +++ b/instrumentation/src/main/java/io/opentelemetry/android/instrumentation/startup/SdkInitializationEvents.java @@ -6,6 +6,7 @@ package io.opentelemetry.android.instrumentation.startup; import io.opentelemetry.android.config.OtelRumConfig; +import io.opentelemetry.sdk.trace.export.SpanExporter; public class SdkInitializationEvents implements InitializationEvents { @@ -43,4 +44,9 @@ public void slowRenderingDetectorInitialized() { public void crashReportingInitialized() { // TODO: Build me "crashReportingInitialized" } + + @Override + public void spanExporterInitialized(SpanExporter spanExporter) { + // TODO: Build me "spanExporterInitialized" + } } diff --git a/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index f82172fba..db587a3a3 100644 --- a/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java +++ b/instrumentation/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java @@ -16,6 +16,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -25,7 +26,10 @@ import androidx.annotation.NonNull; import io.opentelemetry.android.config.OtelRumConfig; import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration; +import io.opentelemetry.android.features.diskbuffering.SignalFromDiskExporter; +import io.opentelemetry.android.features.diskbuffering.scheduler.ExportScheduleHandler; import io.opentelemetry.android.instrumentation.ApplicationStateListener; +import io.opentelemetry.android.instrumentation.startup.InitializationEvents; import io.opentelemetry.android.internal.services.CacheStorageService; import io.opentelemetry.android.internal.services.PreferencesService; import io.opentelemetry.android.internal.services.Service; @@ -35,7 +39,7 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.TextMapGetter; import io.opentelemetry.context.propagation.TextMapPropagator; -import io.opentelemetry.contrib.disk.buffering.SpanDiskExporter; +import io.opentelemetry.contrib.disk.buffering.SpanToDiskExporter; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.data.SpanData; @@ -44,8 +48,8 @@ import java.io.IOException; import java.time.Duration; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -67,6 +71,7 @@ class OpenTelemetryRumBuilderTest { @Mock Activity activity; @Mock ApplicationStateListener listener; + @Mock InitializationEvents initializationEvents; @Captor ArgumentCaptor activityCallbacksCaptor; @BeforeEach @@ -75,6 +80,11 @@ void setup() { when(application.getMainLooper()).thenReturn(looper); } + @AfterEach + void tearDown() { + SignalFromDiskExporter.resetForTesting(); + } + @Test void shouldRegisterApplicationStateWatcher() { makeBuilder().build(); @@ -186,25 +196,30 @@ void diskBufferingEnabled() { doReturn(60 * 1024 * 1024L).when(cacheStorage).ensureCacheSpaceAvailable(anyLong()); setUpServiceManager(preferences, cacheStorage); OtelRumConfig config = buildConfig(); + ExportScheduleHandler scheduleHandler = mock(); config.setDiskBufferingConfiguration( - DiskBufferingConfiguration.builder().setEnabled(true).build()); - AtomicReference capturedExporter = new AtomicReference<>(); + DiskBufferingConfiguration.builder() + .setEnabled(true) + .setExportScheduleHandler(scheduleHandler) + .build()); + ArgumentCaptor exporterCaptor = ArgumentCaptor.forClass(SpanExporter.class); OpenTelemetryRum.builder(application, config) - .addSpanExporterCustomizer( - spanExporter -> { - capturedExporter.set(spanExporter); - return spanExporter; - }) + .setInitializationEvents(initializationEvents) .build(); - assertThat(capturedExporter.get()).isInstanceOf(SpanDiskExporter.class); + assertThat(SignalFromDiskExporter.get()).isNotNull(); + verify(scheduleHandler).enable(); + verify(scheduleHandler, never()).disable(); + verify(initializationEvents).spanExporterInitialized(exporterCaptor.capture()); + assertThat(exporterCaptor.getValue()).isInstanceOf(SpanToDiskExporter.class); } @Test void diskBufferingEnabled_when_exception_thrown() { PreferencesService preferences = mock(); CacheStorageService cacheStorage = mock(); + ExportScheduleHandler scheduleHandler = mock(); doReturn(60 * 1024 * 1024L).when(cacheStorage).ensureCacheSpaceAvailable(anyLong()); doAnswer( invocation -> { @@ -213,35 +228,46 @@ void diskBufferingEnabled_when_exception_thrown() { .when(cacheStorage) .getCacheDir(); setUpServiceManager(preferences, cacheStorage); + ArgumentCaptor exporterCaptor = ArgumentCaptor.forClass(SpanExporter.class); OtelRumConfig config = buildConfig(); config.setDiskBufferingConfiguration( - DiskBufferingConfiguration.builder().setEnabled(true).build()); - AtomicReference capturedExporter = new AtomicReference<>(); + DiskBufferingConfiguration.builder() + .setEnabled(true) + .setExportScheduleHandler(scheduleHandler) + .build()); OpenTelemetryRum.builder(application, config) - .addSpanExporterCustomizer( - spanExporter -> { - capturedExporter.set(spanExporter); - return spanExporter; - }) + .setInitializationEvents(initializationEvents) .build(); - assertThat(capturedExporter.get()).isNotInstanceOf(SpanDiskExporter.class); + verify(initializationEvents).spanExporterInitialized(exporterCaptor.capture()); + verify(scheduleHandler, never()).enable(); + verify(scheduleHandler).disable(); + assertThat(exporterCaptor.getValue()).isNotInstanceOf(SpanToDiskExporter.class); + assertThat(SignalFromDiskExporter.get()).isNull(); } @Test void diskBufferingDisabled() { - AtomicReference capturedExporter = new AtomicReference<>(); + ArgumentCaptor exporterCaptor = ArgumentCaptor.forClass(SpanExporter.class); + ExportScheduleHandler scheduleHandler = mock(); - makeBuilder() - .addSpanExporterCustomizer( - spanExporter -> { - capturedExporter.set(spanExporter); - return spanExporter; - }) + OtelRumConfig config = buildConfig(); + config.setDiskBufferingConfiguration( + DiskBufferingConfiguration.builder() + .setEnabled(false) + .setExportScheduleHandler(scheduleHandler) + .build()); + + OpenTelemetryRum.builder(application, config) + .setInitializationEvents(initializationEvents) .build(); - assertThat(capturedExporter.get()).isNotInstanceOf(SpanDiskExporter.class); + verify(initializationEvents).spanExporterInitialized(exporterCaptor.capture()); + verify(scheduleHandler, never()).enable(); + verify(scheduleHandler).disable(); + assertThat(exporterCaptor.getValue()).isNotInstanceOf(SpanToDiskExporter.class); + assertThat(SignalFromDiskExporter.get()).isNull(); } private static void setUpServiceManager(Service... services) { diff --git a/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportSchedulerTest.kt b/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportSchedulerTest.kt index d9fe23169..9c345dc7f 100644 --- a/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportSchedulerTest.kt +++ b/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/DefaultExportSchedulerTest.kt @@ -5,9 +5,16 @@ package io.opentelemetry.android.features.diskbuffering.scheduler +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import io.opentelemetry.android.features.diskbuffering.SignalFromDiskExporter import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail +import java.io.IOException import java.util.concurrent.TimeUnit class DefaultExportSchedulerTest { @@ -18,6 +25,48 @@ class DefaultExportSchedulerTest { scheduler = DefaultExportScheduler() } + @AfterEach + fun tearDown() { + SignalFromDiskExporter.resetForTesting() + } + + @Test + fun `Try to export all available signals when running`() { + val signalFromDiskExporter = mockk() + every { signalFromDiskExporter.exportBatchOfEach() }.returns(true).andThen(true).andThen(false) + SignalFromDiskExporter.set(signalFromDiskExporter) + + scheduler.onRun() + + verify(exactly = 3) { + signalFromDiskExporter.exportBatchOfEach() + } + } + + @Test + fun `Avoid crashing when an exception happens during execution`() { + val signalFromDiskExporter = mockk() + every { signalFromDiskExporter.exportBatchOfEach() }.throws(IOException()) + SignalFromDiskExporter.set(signalFromDiskExporter) + + try { + scheduler.onRun() + } catch (e: IOException) { + fail(e) + } + } + + @Test + fun `Stop running if it can't export from disk`() { + assertThat(scheduler.shouldStopRunning()).isTrue() + } + + @Test + fun `Continue to run if it can export from disk`() { + SignalFromDiskExporter.set(mockk()) + assertThat(scheduler.shouldStopRunning()).isFalse() + } + @Test fun `Verify minimum delay`() { assertThat(scheduler.minimumDelayUntilNextRunInMillis()).isEqualTo( diff --git a/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/SignalFromDiskExporterTest.kt b/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/SignalFromDiskExporterTest.kt new file mode 100644 index 000000000..a867a7937 --- /dev/null +++ b/instrumentation/src/test/java/io/opentelemetry/android/features/diskbuffering/scheduler/SignalFromDiskExporterTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.features.diskbuffering.scheduler + +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import io.opentelemetry.android.features.diskbuffering.SignalFromDiskExporter +import io.opentelemetry.contrib.disk.buffering.LogRecordFromDiskExporter +import io.opentelemetry.contrib.disk.buffering.MetricFromDiskExporter +import io.opentelemetry.contrib.disk.buffering.SpanFromDiskExporter +import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporter +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit + +class SignalFromDiskExporterTest { + companion object { + private val DEFAULT_EXPORT_TIMEOUT_IN_MILLIS: Long = TimeUnit.SECONDS.toMillis(5) + } + + @MockK + private lateinit var spanFromDiskExporter: SpanFromDiskExporter + + @MockK + private lateinit var metricFromDiskExporter: MetricFromDiskExporter + + @MockK + private lateinit var logRecordFromDiskExporter: LogRecordFromDiskExporter + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + } + + @Test + fun `Exporting with custom timeout time`() { + val timeoutInMillis = TimeUnit.SECONDS.toMillis(10) + val instance = + createInstance( + spanFromDiskExporter, + metricFromDiskExporter, + logRecordFromDiskExporter, + timeoutInMillis, + ) + every { spanFromDiskExporter.exportStoredBatch(any(), any()) }.returns(true) + assertThat(instance.exportBatchOfSpans()).isTrue() + verifyExportStoredBatchCall(spanFromDiskExporter, timeoutInMillis) + } + + @Test + fun `Verify exporting spans`() { + val instance = + createInstance(spanFromDiskExporter, metricFromDiskExporter, logRecordFromDiskExporter) + every { spanFromDiskExporter.exportStoredBatch(any(), any()) }.returns(true) + assertThat(instance.exportBatchOfSpans()).isTrue() + verifyExportStoredBatchCall(spanFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + } + + @Test + fun `Verify exporting metrics`() { + val instance = + createInstance(spanFromDiskExporter, metricFromDiskExporter, logRecordFromDiskExporter) + every { metricFromDiskExporter.exportStoredBatch(any(), any()) }.returns(true) + assertThat(instance.exportBatchOfMetrics()).isTrue() + verifyExportStoredBatchCall(metricFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + } + + @Test + fun `Verify exporting logs`() { + val instance = + createInstance(spanFromDiskExporter, metricFromDiskExporter, logRecordFromDiskExporter) + every { logRecordFromDiskExporter.exportStoredBatch(any(), any()) }.returns(true) + assertThat(instance.exportBatchOfLogs()).isTrue() + verifyExportStoredBatchCall(logRecordFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + } + + @Test + fun `Return false when all exports fail`() { + val instance = + createInstance(spanFromDiskExporter, metricFromDiskExporter, logRecordFromDiskExporter) + every { spanFromDiskExporter.exportStoredBatch(any(), any()) }.returns(false) + every { metricFromDiskExporter.exportStoredBatch(any(), any()) }.returns(false) + every { logRecordFromDiskExporter.exportStoredBatch(any(), any()) }.returns(false) + assertThat(instance.exportBatchOfEach()).isFalse() + verifyExportStoredBatchCall(spanFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + verifyExportStoredBatchCall(metricFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + verifyExportStoredBatchCall(logRecordFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + } + + @Test + fun `Return true when spans export succeeds`() { + val instance = + createInstance(spanFromDiskExporter, metricFromDiskExporter, logRecordFromDiskExporter) + every { spanFromDiskExporter.exportStoredBatch(any(), any()) }.returns(true) + every { metricFromDiskExporter.exportStoredBatch(any(), any()) }.returns(false) + every { logRecordFromDiskExporter.exportStoredBatch(any(), any()) }.returns(false) + assertThat(instance.exportBatchOfEach()).isTrue() + verifyExportStoredBatchCall(spanFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + verifyExportStoredBatchCall(metricFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + verifyExportStoredBatchCall(logRecordFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + } + + @Test + fun `Return true when metrics export succeeds`() { + val instance = + createInstance(spanFromDiskExporter, metricFromDiskExporter, logRecordFromDiskExporter) + every { spanFromDiskExporter.exportStoredBatch(any(), any()) }.returns(false) + every { metricFromDiskExporter.exportStoredBatch(any(), any()) }.returns(true) + every { logRecordFromDiskExporter.exportStoredBatch(any(), any()) }.returns(false) + assertThat(instance.exportBatchOfEach()).isTrue() + verifyExportStoredBatchCall(spanFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + verifyExportStoredBatchCall(metricFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + verifyExportStoredBatchCall(logRecordFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + } + + @Test + fun `Return true when logs export succeeds`() { + val instance = + createInstance(spanFromDiskExporter, metricFromDiskExporter, logRecordFromDiskExporter) + every { spanFromDiskExporter.exportStoredBatch(any(), any()) }.returns(false) + every { metricFromDiskExporter.exportStoredBatch(any(), any()) }.returns(false) + every { logRecordFromDiskExporter.exportStoredBatch(any(), any()) }.returns(true) + assertThat(instance.exportBatchOfEach()).isTrue() + verifyExportStoredBatchCall(spanFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + verifyExportStoredBatchCall(metricFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + verifyExportStoredBatchCall(logRecordFromDiskExporter, DEFAULT_EXPORT_TIMEOUT_IN_MILLIS) + } + + @Test + fun `Return false when spans exporter is not available`() { + val instance = createInstance(null, metricFromDiskExporter, logRecordFromDiskExporter) + assertThat(instance.exportBatchOfSpans()).isFalse() + } + + @Test + fun `Return false when metrics exporter is not available`() { + val instance = createInstance(spanFromDiskExporter, null, logRecordFromDiskExporter) + assertThat(instance.exportBatchOfMetrics()).isFalse() + } + + @Test + fun `Return false when logs exporter is not available`() { + val instance = createInstance(spanFromDiskExporter, metricFromDiskExporter, null) + assertThat(instance.exportBatchOfLogs()).isFalse() + } + + private fun verifyExportStoredBatchCall( + exporter: FromDiskExporter, + timeoutInMillis: Long, + ) { + verify { + exporter.exportStoredBatch(timeoutInMillis, TimeUnit.MILLISECONDS) + } + } + + private fun createInstance( + spanFromDiskExporter: SpanFromDiskExporter?, + metricFromDiskExporter: MetricFromDiskExporter?, + logRecordFromDiskExporter: LogRecordFromDiskExporter?, + exportTimeoutInMillis: Long? = null, + ): SignalFromDiskExporter { + return if (exportTimeoutInMillis == null) { + SignalFromDiskExporter( + spanFromDiskExporter, + metricFromDiskExporter, + logRecordFromDiskExporter, + ) + } else { + SignalFromDiskExporter( + spanFromDiskExporter, + metricFromDiskExporter, + logRecordFromDiskExporter, + exportTimeoutInMillis, + ) + } + } +}