diff --git a/core/opentelemetry/signals/logs/1733967890157 b/core/opentelemetry/signals/logs/1733967890157 new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/core/opentelemetry/signals/logs/1733967890157 differ diff --git a/core/opentelemetry/signals/logs/1733968675537 b/core/opentelemetry/signals/logs/1733968675537 new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/core/opentelemetry/signals/logs/1733968675537 differ diff --git a/core/opentelemetry/signals/spans/1733967890160 b/core/opentelemetry/signals/spans/1733967890160 new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/core/opentelemetry/signals/spans/1733967890160 differ diff --git a/core/opentelemetry/signals/spans/1733968675539 b/core/opentelemetry/signals/spans/1733968675539 new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/core/opentelemetry/signals/spans/1733968675539 differ diff --git a/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java b/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java index a01e497ec..66b3c89be 100644 --- a/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java +++ b/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java @@ -10,8 +10,11 @@ import android.app.Application; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.opentelemetry.android.common.RumConstants; import io.opentelemetry.android.config.OtelRumConfig; +import io.opentelemetry.android.export.BufferDelegatingLogExporter; +import io.opentelemetry.android.export.BufferDelegatingSpanExporter; import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfiguration; import io.opentelemetry.android.features.diskbuffering.SignalFromDiskExporter; import io.opentelemetry.android.features.diskbuffering.scheduler.DefaultExportScheduleHandler; @@ -63,7 +66,6 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; -import javax.annotation.Nullable; import kotlin.jvm.functions.Function0; /** @@ -94,7 +96,10 @@ public final class OpenTelemetryRumBuilder { private Resource resource; + private boolean isBuilt = false; + @Nullable private ServiceManager serviceManager; + @Nullable private ExportScheduleHandler exportScheduleHandler; private static TextMapPropagator buildDefaultPropagator() { @@ -122,6 +127,7 @@ public static OpenTelemetryRumBuilder create(Application application, OtelRumCon * @return {@code this} */ public OpenTelemetryRumBuilder setResource(Resource resource) { + checkNotBuilt(); this.resource = resource; return this; } @@ -134,6 +140,7 @@ public OpenTelemetryRumBuilder setResource(Resource resource) { * @return {@code this} */ public OpenTelemetryRumBuilder mergeResource(Resource resource) { + checkNotBuilt(); this.resource = this.resource.merge(resource); return this; } @@ -173,6 +180,7 @@ public OpenTelemetryRumBuilder addTracerProviderCustomizer( */ public OpenTelemetryRumBuilder addMeterProviderCustomizer( BiFunction customizer) { + checkNotBuilt(); meterProviderCustomizers.add(customizer); return this; } @@ -193,6 +201,7 @@ public OpenTelemetryRumBuilder addMeterProviderCustomizer( public OpenTelemetryRumBuilder addLoggerProviderCustomizer( BiFunction customizer) { + checkNotBuilt(); loggerProviderCustomizers.add(customizer); return this; } @@ -204,6 +213,7 @@ public OpenTelemetryRumBuilder addLoggerProviderCustomizer( */ public OpenTelemetryRumBuilder addInstrumentation(AndroidInstrumentation instrumentation) { instrumentations.add(instrumentation); + checkNotBuilt(); return this; } @@ -218,6 +228,7 @@ public OpenTelemetryRumBuilder addInstrumentation(AndroidInstrumentation instrum public OpenTelemetryRumBuilder addPropagatorCustomizer( Function propagatorCustomizer) { requireNonNull(propagatorCustomizer, "propagatorCustomizer"); + checkNotBuilt(); Function existing = this.propagatorCustomizer; this.propagatorCustomizer = @@ -237,6 +248,7 @@ public OpenTelemetryRumBuilder addPropagatorCustomizer( public OpenTelemetryRumBuilder addSpanExporterCustomizer( Function spanExporterCustomizer) { requireNonNull(spanExporterCustomizer, "spanExporterCustomizer"); + checkNotBuilt(); Function existing = this.spanExporterCustomizer; this.spanExporterCustomizer = @@ -256,6 +268,7 @@ public OpenTelemetryRumBuilder addSpanExporterCustomizer( public OpenTelemetryRumBuilder addLogRecordExporterCustomizer( Function logRecordExporterCustomizer) { + checkNotBuilt(); Function existing = this.logRecordExporterCustomizer; this.logRecordExporterCustomizer = @@ -276,9 +289,63 @@ public OpenTelemetryRumBuilder addLogRecordExporterCustomizer( * @return A new {@link OpenTelemetryRum} instance. */ public OpenTelemetryRum build() { + if (isBuilt) { + throw new IllegalStateException("You cannot call build multiple times"); + } + isBuilt = true; InitializationEvents initializationEvents = InitializationEvents.get(); applyConfiguration(initializationEvents); + BufferDelegatingLogExporter bufferDelegatingLogExporter = new BufferDelegatingLogExporter(); + + BufferDelegatingSpanExporter bufferDelegatingSpanExporter = + new BufferDelegatingSpanExporter(); + + SessionManager sessionManager = + SessionManager.create(timeoutHandler, config.getSessionTimeout().toNanos()); + + OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + buildTracerProvider( + sessionManager, application, bufferDelegatingSpanExporter)) + .setLoggerProvider( + buildLoggerProvider( + sessionManager, application, bufferDelegatingLogExporter)) + .setMeterProvider(buildMeterProvider(application)) + .setPropagators(buildFinalPropagators()) + .build(); + + otelSdkReadyListeners.forEach(listener -> listener.accept(sdk)); + + SdkPreconfiguredRumBuilder delegate = + new SdkPreconfiguredRumBuilder( + application, + sdk, + timeoutHandler, + sessionManager, + config.shouldDiscoverInstrumentations(), + getServiceManager()); + + // AsyncTask is deprecated but the thread pool is still used all over the Android SDK + // and it provides a way to get a background thread without having to create a new one. + android.os.AsyncTask.THREAD_POOL_EXECUTOR.execute( + () -> + initializeExporters( + initializationEvents, + bufferDelegatingSpanExporter, + bufferDelegatingLogExporter)); + + instrumentations.forEach(delegate::addInstrumentation); + + return delegate.build(); + } + + private void initializeExporters( + InitializationEvents initializationEvents, + BufferDelegatingSpanExporter bufferDelegatingSpanExporter, + BufferDelegatingLogExporter bufferedDelegatingLogExporter) { + DiskBufferingConfiguration diskBufferingConfiguration = config.getDiskBufferingConfiguration(); SpanExporter spanExporter = buildSpanExporter(); @@ -306,33 +373,11 @@ public OpenTelemetryRum build() { } initializationEvents.spanExporterInitialized(spanExporter); - SessionManager sessionManager = - SessionManager.create(timeoutHandler, config.getSessionTimeout().toNanos()); + bufferedDelegatingLogExporter.setDelegate(logsExporter); - OpenTelemetrySdk sdk = - OpenTelemetrySdk.builder() - .setTracerProvider( - buildTracerProvider(sessionManager, application, spanExporter)) - .setLoggerProvider( - buildLoggerProvider(sessionManager, application, logsExporter)) - .setMeterProvider(buildMeterProvider(application)) - .setPropagators(buildFinalPropagators()) - .build(); - - otelSdkReadyListeners.forEach(listener -> listener.accept(sdk)); + bufferDelegatingSpanExporter.setDelegate(spanExporter); scheduleDiskTelemetryReader(signalFromDiskExporter); - - SdkPreconfiguredRumBuilder delegate = - new SdkPreconfiguredRumBuilder( - application, - sdk, - timeoutHandler, - sessionManager, - config.shouldDiscoverInstrumentations(), - getServiceManager()); - instrumentations.forEach(delegate::addInstrumentation); - return delegate.build(); } @NonNull @@ -340,10 +385,13 @@ private ServiceManager getServiceManager() { if (serviceManager == null) { serviceManager = ServiceManagerImpl.Companion.create(application); } - return serviceManager; + // This can never be null since we never write `null` to it + return requireNonNull(serviceManager); } - public OpenTelemetryRumBuilder setServiceManager(ServiceManager serviceManager) { + public OpenTelemetryRumBuilder setServiceManager(@NonNull ServiceManager serviceManager) { + requireNonNull(serviceManager, "serviceManager cannot be null"); + checkNotBuilt(); this.serviceManager = serviceManager; return this; } @@ -353,7 +401,9 @@ public OpenTelemetryRumBuilder setServiceManager(ServiceManager serviceManager) * If not specified, the default schedule exporter will be used. */ public OpenTelemetryRumBuilder setExportScheduleHandler( - ExportScheduleHandler exportScheduleHandler) { + @NonNull ExportScheduleHandler exportScheduleHandler) { + requireNonNull(exportScheduleHandler, "exportScheduleHandler cannot be null"); + checkNotBuilt(); this.exportScheduleHandler = exportScheduleHandler; return this; } @@ -376,7 +426,6 @@ private StorageConfiguration createStorageConfiguration() throws IOException { } private void scheduleDiskTelemetryReader(@Nullable SignalFromDiskExporter signalExporter) { - if (exportScheduleHandler == null) { ServiceManager serviceManager = getServiceManager(); // TODO: Is it safe to get the work service yet here? If so, we can @@ -387,6 +436,9 @@ private void scheduleDiskTelemetryReader(@Nullable SignalFromDiskExporter signal new DefaultExportScheduler(getWorkService), getWorkService); } + final ExportScheduleHandler exportScheduleHandler = + requireNonNull(this.exportScheduleHandler); + 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). @@ -408,6 +460,7 @@ private void scheduleDiskTelemetryReader(@Nullable SignalFromDiskExporter signal * @return this */ public OpenTelemetryRumBuilder addOtelSdkReadyListener(Consumer callback) { + checkNotBuilt(); otelSdkReadyListeners.add(callback); return this; } @@ -521,4 +574,10 @@ private ContextPropagators buildFinalPropagators() { TextMapPropagator defaultPropagator = buildDefaultPropagator(); return ContextPropagators.create(propagatorCustomizer.apply(defaultPropagator)); } + + private void checkNotBuilt() { + if (isBuilt) { + throw new IllegalStateException("This method cannot be called after calling build"); + } + } } diff --git a/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingLogExporter.kt b/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingLogExporter.kt new file mode 100644 index 000000000..563c0fc11 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingLogExporter.kt @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.export + +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.logs.data.LogRecordData +import io.opentelemetry.sdk.logs.export.LogRecordExporter + +/** + * An in-memory buffer delegating log exporter that buffers log records in memory until a delegate is set. + * Once a delegate is set, the buffered log records are exported to the delegate. + * + * The buffer size is set to 5,000 log entries by default. If the buffer is full, the exporter will drop new log records. + */ +internal class BufferDelegatingLogExporter( + maxBufferedLogs: Int = 5_000, +) : LogRecordExporter { + private val delegatingExporter = + DelegatingExporter( + doExport = LogRecordExporter::export, + doFlush = LogRecordExporter::flush, + doShutdown = LogRecordExporter::shutdown, + maxBufferedData = maxBufferedLogs, + ) + + fun setDelegate(delegate: LogRecordExporter) { + delegatingExporter.setDelegate(delegate) + } + + override fun export(logs: Collection): CompletableResultCode = delegatingExporter.export(logs) + + override fun flush(): CompletableResultCode = delegatingExporter.flush() + + override fun shutdown(): CompletableResultCode = delegatingExporter.shutdown() +} diff --git a/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingSpanExporter.kt b/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingSpanExporter.kt new file mode 100644 index 000000000..88724a483 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingSpanExporter.kt @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.export + +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.sdk.trace.export.SpanExporter + +/** + * An in-memory buffer delegating span exporter that buffers span data in memory until a delegate is set. + * Once a delegate is set, the buffered span data is exported to the delegate. + * + * The buffer size is set to 5,000 spans by default. If the buffer is full, the exporter will drop new span data. + */ +internal class BufferDelegatingSpanExporter( + maxBufferedSpans: Int = 5_000, +) : SpanExporter { + private val delegatingExporter = + DelegatingExporter( + doExport = SpanExporter::export, + doFlush = SpanExporter::flush, + doShutdown = SpanExporter::shutdown, + maxBufferedData = maxBufferedSpans, + ) + + fun setDelegate(delegate: SpanExporter) { + delegatingExporter.setDelegate(delegate) + } + + override fun export(spans: Collection): CompletableResultCode = delegatingExporter.export(spans) + + override fun flush(): CompletableResultCode = delegatingExporter.flush() + + override fun shutdown(): CompletableResultCode = delegatingExporter.shutdown() +} diff --git a/core/src/main/java/io/opentelemetry/android/export/DelegatingExporter.kt b/core/src/main/java/io/opentelemetry/android/export/DelegatingExporter.kt new file mode 100644 index 000000000..cf7bece81 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/export/DelegatingExporter.kt @@ -0,0 +1,161 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.export + +import io.opentelemetry.api.internal.GuardedBy +import io.opentelemetry.sdk.common.CompletableResultCode +import java.nio.BufferOverflowException + +/** + * An exporter that delegates calls to a delegate exporter. Any data exported before the delegate + * is set will be buffered in memory, up to the [maxBufferedData] number of entries. + * + * If the buffer is full, the exporter will drop any new signals. + * + * @param D the type of the delegate. + * @param T the type of the data. + * @param doExport a lambda that handles exporting to the delegate. + * @param doFlush a lambda that handles flushing the delegate. + * @param doShutdown a lambda that handles shutting down the delegate. + * @param maxBufferedData the maximum number of data to buffer in memory before dropping new data. + */ +internal class DelegatingExporter( + private val doExport: D.(data: Collection) -> CompletableResultCode, + private val doFlush: D.() -> CompletableResultCode, + private val doShutdown: D.() -> CompletableResultCode, + private val maxBufferedData: Int, +) { + private val lock = Any() + + @GuardedBy("lock") + private var delegate: D? = null + + @GuardedBy("lock") + private val buffer = arrayListOf() + + @GuardedBy("lock") + private var pendingExport: CompletableResultCode? = null + + @GuardedBy("lock") + private var pendingFlush: CompletableResultCode? = null + + @GuardedBy("lock") + private var pendingShutdown: CompletableResultCode? = null + + /** + * Sets the delegate for this exporter. + * + * Any buffered data will be written to the delegate followed by a flush and shut down if + * [flush] and/or [shutdown] has been called prior to this call. + * + * @param delegate the delegate to set + * @throws IllegalStateException if a delegate has already been set + */ + fun setDelegate(delegate: D) { + synchronized(lock) { + check(this.delegate == null) { "A delegate has already been set." } + this.delegate = delegate + } + // Exporting outside of the synchronized block could lead to an out of order export + // but export order shouldn't matter so this is fine. It's better to avoid calling external + // code from within the synchronized block. + pendingExport?.setTo(delegate.doExport(buffer)) + pendingFlush?.setTo(delegate.doFlush()) + pendingShutdown?.setTo(delegate.doShutdown()) + synchronized(lock) { + pendingExport = null + pendingFlush = null + pendingShutdown = null + } + clearBuffer() + } + + /** + * Exports the given data using the [doExport] lambda. If the delegate is not yet set an export + * will be scheduled and executed when the delegate is set. + * + * @param data the data to export. + * @return the result. If the delegate is set then the result from it will be returned, + * otherwise a result is returned which will complete when the delegate is set and the data + * has been exported. If all of the data was dropped then a failure is returned. + */ + fun export(data: Collection): CompletableResultCode = + withDelegate( + ifSet = { doExport(this, data) }, + ifNotSet = { + val amountToTake = maxBufferedData - buffer.size + buffer.addAll(data.take(amountToTake)) + // If all the data was dropped we return an exception + if (amountToTake == 0 && data.isNotEmpty()) { + CompletableResultCode.ofExceptionalFailure(BufferOverflowException()) + } else { + pendingExport + ?: CompletableResultCode().also { pendingExport = it } + } + }, + ) + + /** + * Flushes the exporter using the [doFlush] lambda. If the delegate is not yet set a flush will + * be scheduled and executed when the delegate is set. + * + * @return the result. If the delegate is set then the result from it will be returned, + * otherwise a result is returned which will complete when the delegate is set and has been + * flushed. + */ + fun flush(): CompletableResultCode = + withDelegate( + ifSet = doFlush, + ifNotSet = { pendingFlush ?: CompletableResultCode().also { pendingFlush = it } }, + ) + + /** + * Shuts down the exporter using the [doShutdown]. If the delegate is not yet set a shut down + * will be scheduled and executed when the delegate is set. + * + * @return the result. If the delegate is set then the result from it will be returned, + * otherwise a result is returned which will complete when the delegate is set and has been + * shut down. + */ + fun shutdown(): CompletableResultCode = + withDelegate( + ifSet = doShutdown, + ifNotSet = { pendingShutdown ?: CompletableResultCode().also { pendingShutdown = it } }, + ) + + private fun clearBuffer() { + buffer.clear() + buffer.trimToSize() + } + + private inline fun withDelegate( + ifSet: D.() -> R, + ifNotSet: () -> R, + ): R { + val delegate = + synchronized(lock) { + delegate ?: return ifNotSet() + } + // We interact with the delegate outside of the synchronized block to avoid any potential + // deadlocks due to reentrant calls + return delegate.ifSet() + } + + private fun CompletableResultCode.setTo(other: CompletableResultCode) { + other.whenComplete { + if (other.isSuccess) { + succeed() + } else { + val throwable = other.failureThrowable + if (throwable == null) { + fail() + } else { + failExceptionally(throwable) + } + } + } + } +} diff --git a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index 28654643c..1fd9663d7 100644 --- a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java +++ b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java @@ -135,21 +135,26 @@ public void shouldBuildTracerProvider() { SimpleSpanProcessor.create(spanExporter))) .build(); - String sessionId = openTelemetryRum.getRumSessionId(); - openTelemetryRum - .getOpenTelemetry() - .getTracer("test") - .spanBuilder("test span") - .startSpan() - .end(); - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).hasSize(1); - assertThat(spans.get(0)) - .hasName("test span") - .hasResource(resource) - .hasAttributesSatisfyingExactly( - equalTo(SESSION_ID, sessionId), equalTo(SCREEN_NAME_KEY, "unknown")); + await().atMost(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + String sessionId = openTelemetryRum.getRumSessionId(); + openTelemetryRum + .getOpenTelemetry() + .getTracer("test") + .spanBuilder("test span") + .startSpan() + .end(); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).hasSize(1); + assertThat(spans.get(0)) + .hasName("test span") + .hasResource(resource) + .hasAttributesSatisfyingExactly( + equalTo(SESSION_ID, sessionId), + equalTo(SCREEN_NAME_KEY, "unknown")); + }); } @Test @@ -344,11 +349,17 @@ public void diskBufferingEnabled() { .setServiceManager(serviceManager) .build(); - assertThat(SignalFromDiskExporter.get()).isNotNull(); - verify(scheduleHandler).enable(); - verify(scheduleHandler, never()).disable(); - verify(initializationEvents).spanExporterInitialized(exporterCaptor.capture()); - assertThat(exporterCaptor.getValue()).isInstanceOf(SpanToDiskExporter.class); + await().atMost(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + assertThat(SignalFromDiskExporter.get()).isNotNull(); + verify(scheduleHandler).enable(); + verify(scheduleHandler, never()).disable(); + verify(initializationEvents) + .spanExporterInitialized(exporterCaptor.capture()); + assertThat(exporterCaptor.getValue()) + .isInstanceOf(SpanToDiskExporter.class); + }); } @Test @@ -373,11 +384,17 @@ public void diskBufferingEnabled_when_exception_thrown() { .setExportScheduleHandler(scheduleHandler) .build(); - verify(initializationEvents).spanExporterInitialized(exporterCaptor.capture()); - verify(scheduleHandler, never()).enable(); - verify(scheduleHandler).disable(); - assertThat(exporterCaptor.getValue()).isNotInstanceOf(SpanToDiskExporter.class); - assertThat(SignalFromDiskExporter.get()).isNull(); + await().atMost(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + verify(initializationEvents) + .spanExporterInitialized(exporterCaptor.capture()); + verify(scheduleHandler, never()).enable(); + verify(scheduleHandler).disable(); + assertThat(exporterCaptor.getValue()) + .isNotInstanceOf(SpanToDiskExporter.class); + assertThat(SignalFromDiskExporter.get()).isNull(); + }); } @Test @@ -404,11 +421,17 @@ public void diskBufferingDisabled() { .setExportScheduleHandler(scheduleHandler) .build(); - verify(initializationEvents).spanExporterInitialized(exporterCaptor.capture()); - verify(scheduleHandler, never()).enable(); - verify(scheduleHandler).disable(); - assertThat(exporterCaptor.getValue()).isNotInstanceOf(SpanToDiskExporter.class); - assertThat(SignalFromDiskExporter.get()).isNull(); + await().atMost(Duration.ofSeconds(30)) + .untilAsserted( + () -> { + verify(initializationEvents) + .spanExporterInitialized(exporterCaptor.capture()); + verify(scheduleHandler, never()).enable(); + verify(scheduleHandler).disable(); + assertThat(exporterCaptor.getValue()) + .isNotInstanceOf(SpanToDiskExporter.class); + assertThat(SignalFromDiskExporter.get()).isNull(); + }); } @Test diff --git a/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingLogExporterTest.kt b/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingLogExporterTest.kt new file mode 100644 index 000000000..44017314e --- /dev/null +++ b/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingLogExporterTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.export + +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import io.opentelemetry.sdk.logs.data.LogRecordData +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat +import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter +import org.junit.Test + +class BufferDelegatingLogExporterTest { + @Test + fun `test setDelegate`() { + val inMemoryBufferDelegatingLogExporter = BufferDelegatingLogExporter() + val logRecordExporter = InMemoryLogRecordExporter.create() + + val logRecordData = mockk() + inMemoryBufferDelegatingLogExporter.export(listOf(logRecordData)) + inMemoryBufferDelegatingLogExporter.setDelegate(logRecordExporter) + + assertThat(logRecordExporter.finishedLogRecordItems) + .containsExactly(logRecordData) + } + + @Test + fun `test buffer limit handling`() { + val inMemoryBufferDelegatingLogExporter = BufferDelegatingLogExporter(10) + val logRecordExporter = InMemoryLogRecordExporter.create() + + repeat(11) { + val logRecordData = mockk() + inMemoryBufferDelegatingLogExporter.export(listOf(logRecordData)) + } + + inMemoryBufferDelegatingLogExporter.setDelegate(logRecordExporter) + + assertThat(logRecordExporter.finishedLogRecordItems) + .hasSize(10) + } + + @Test + fun `test flush with delegate`() { + val inMemoryBufferDelegatingLogExporter = BufferDelegatingLogExporter() + val delegate = spyk() + + val logRecordData = mockk() + inMemoryBufferDelegatingLogExporter.export(listOf(logRecordData)) + + inMemoryBufferDelegatingLogExporter.setDelegate(delegate) + + inMemoryBufferDelegatingLogExporter.flush() + + verify { delegate.flush() } + } + + @Test + fun `test export with delegate`() { + val inMemoryBufferDelegatingLogExporter = BufferDelegatingLogExporter() + val delegate = spyk() + + val logRecordData = mockk() + inMemoryBufferDelegatingLogExporter.export(listOf(logRecordData)) + + verify(exactly = 0) { delegate.export(any()) } + + inMemoryBufferDelegatingLogExporter.setDelegate(delegate) + + verify(exactly = 1) { delegate.export(any()) } + + val logRecordData2 = mockk() + inMemoryBufferDelegatingLogExporter.export(listOf(logRecordData2)) + + verify(exactly = 2) { delegate.export(any()) } + } + + @Test + fun `test shutdown with delegate`() { + val inMemoryBufferDelegatingLogExporter = BufferDelegatingLogExporter() + val delegate = spyk() + + inMemoryBufferDelegatingLogExporter.setDelegate(delegate) + + inMemoryBufferDelegatingLogExporter.shutdown() + + verify { delegate.shutdown() } + } +} diff --git a/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingSpanExporterTest.kt b/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingSpanExporterTest.kt new file mode 100644 index 000000000..f415f61a6 --- /dev/null +++ b/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingSpanExporterTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.export + +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter +import io.opentelemetry.sdk.trace.data.SpanData +import org.junit.Test +import java.nio.BufferOverflowException + +class BufferDelegatingSpanExporterTest { + private val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter() + private val delegate = spyk() + private val spanData = mockk() + + @Test + fun `test no data`() { + bufferDelegatingSpanExporter.setDelegate(delegate) + + verify(exactly = 0) { delegate.export(any()) } + verify(exactly = 0) { delegate.flush() } + verify(exactly = 0) { delegate.shutdown() } + } + + @Test + fun `test setDelegate`() { + bufferDelegatingSpanExporter.export(listOf(spanData)) + bufferDelegatingSpanExporter.setDelegate(delegate) + + assertThat(delegate.finishedSpanItems) + .containsExactly(spanData) + verify(exactly = 0) { delegate.flush() } + verify(exactly = 0) { delegate.shutdown() } + } + + @Test + fun `the export result should complete when the delegate is set`() { + val result = bufferDelegatingSpanExporter.export(listOf(spanData)) + assertThat(result.isDone).isFalse() + bufferDelegatingSpanExporter.setDelegate(delegate) + assertThat(result.isSuccess).isTrue() + } + + @Test + fun `test buffer limit handling`() { + val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter(10) + val spanExporter = InMemorySpanExporter.create() + val initialResult = bufferDelegatingSpanExporter.export(List(10) { mockk() }) + assertThat(initialResult.isDone).isFalse() + + val overflowResult = bufferDelegatingSpanExporter.export(listOf(mockk())) + assertThat(overflowResult.isDone).isTrue() + assertThat(overflowResult.isSuccess).isFalse() + assertThat(overflowResult.failureThrowable).isInstanceOf(BufferOverflowException::class.java) + + bufferDelegatingSpanExporter.setDelegate(spanExporter) + + assertThat(spanExporter.finishedSpanItems) + .hasSize(10) + } + + @Test + fun `test flush with delegate`() { + bufferDelegatingSpanExporter.setDelegate(delegate) + verify(exactly = 0) { delegate.flush() } + val result = bufferDelegatingSpanExporter.flush() + verify(exactly = 1) { delegate.flush() } + assertThat(result.isSuccess).isTrue() + } + + @Test + fun `test flush without delegate`() { + val result = bufferDelegatingSpanExporter.flush() + assertThat(result.isDone).isFalse() + + bufferDelegatingSpanExporter.setDelegate(delegate) + verify(exactly = 1) { delegate.flush() } + assertThat(result.isSuccess).isTrue() + } + + @Test + fun `test export with delegate`() { + bufferDelegatingSpanExporter.export(listOf(spanData)) + bufferDelegatingSpanExporter.setDelegate(delegate) + + assertThat(delegate.finishedSpanItems).containsExactly(spanData) + + val spanData2 = mockk() + val result = bufferDelegatingSpanExporter.export(listOf(spanData2)) + + assertThat(delegate.finishedSpanItems).containsExactly(spanData, spanData2) + assertThat(result.isSuccess).isTrue() + } + + @Test + fun `test shutdown with delegate`() { + bufferDelegatingSpanExporter.setDelegate(delegate) + val result = bufferDelegatingSpanExporter.shutdown() + verify(exactly = 1) { delegate.shutdown() } + assertThat(result.isSuccess).isTrue() + } + + @Test + fun `test shutdown without delegate`() { + val result = bufferDelegatingSpanExporter.shutdown() + assertThat(result.isDone).isFalse() + + bufferDelegatingSpanExporter.setDelegate(delegate) + assertThat(result.isSuccess).isTrue() + } +}