diff --git a/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java b/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java index a01e497ec..58bb35aaf 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,8 +96,13 @@ public final class OpenTelemetryRumBuilder { private Resource resource; - @Nullable private ServiceManager serviceManager; - @Nullable private ExportScheduleHandler exportScheduleHandler; + private final Object lock = new Object(); + + // Writes guarded by "lock" + @Nullable private volatile ServiceManager serviceManager; + + // Writes guarded by "lock" + @Nullable private volatile ExportScheduleHandler exportScheduleHandler; private static TextMapPropagator buildDefaultPropagator() { return TextMapPropagator.composite( @@ -279,6 +286,56 @@ public OpenTelemetryRum build() { 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,45 +363,31 @@ 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 private ServiceManager getServiceManager() { if (serviceManager == null) { - serviceManager = ServiceManagerImpl.Companion.create(application); + synchronized (lock) { + 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) { - this.serviceManager = serviceManager; + public OpenTelemetryRumBuilder setServiceManager(@NonNull ServiceManager serviceManager) { + requireNonNull(serviceManager, "serviceManager cannot be null"); + synchronized (lock) { + this.serviceManager = serviceManager; + } return this; } @@ -353,8 +396,11 @@ public OpenTelemetryRumBuilder setServiceManager(ServiceManager serviceManager) * If not specified, the default schedule exporter will be used. */ public OpenTelemetryRumBuilder setExportScheduleHandler( - ExportScheduleHandler exportScheduleHandler) { - this.exportScheduleHandler = exportScheduleHandler; + @NonNull ExportScheduleHandler exportScheduleHandler) { + requireNonNull(exportScheduleHandler, "exportScheduleHandler cannot be null"); + synchronized (lock) { + this.exportScheduleHandler = exportScheduleHandler; + } return this; } @@ -376,17 +422,24 @@ 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 - // avoid all this lazy supplier stuff.... - Function0 getWorkService = serviceManager::getPeriodicWorkService; - exportScheduleHandler = - new DefaultExportScheduleHandler( - new DefaultExportScheduler(getWorkService), getWorkService); + synchronized (lock) { + if (exportScheduleHandler == null) { + ServiceManager serviceManager = getServiceManager(); + // TODO: Is it safe to get the work service yet here? If so, we can + // avoid all this lazy supplier stuff.... + Function0 getWorkService = + serviceManager::getPeriodicWorkService; + exportScheduleHandler = + new DefaultExportScheduleHandler( + 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). 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..d75a22b24 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingLogExporter.kt @@ -0,0 +1,35 @@ +/* + * 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, +) : BufferedDelegatingExporter(bufferedSignals = maxBufferedLogs), + LogRecordExporter { + override fun exportToDelegate( + delegate: LogRecordExporter, + data: Collection, + ): CompletableResultCode = delegate.export(data) + + override fun shutdownDelegate(delegate: LogRecordExporter): CompletableResultCode = delegate.shutdown() + + override fun export(logs: Collection): CompletableResultCode = bufferOrDelegate(logs) + + override fun flush(): CompletableResultCode = + withDelegateOrNull { delegate -> + delegate?.flush() ?: CompletableResultCode.ofSuccess() + } +} 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..f6683a375 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingSpanExporter.kt @@ -0,0 +1,35 @@ +/* + * 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, +) : BufferedDelegatingExporter(bufferedSignals = maxBufferedSpans), + SpanExporter { + override fun exportToDelegate( + delegate: SpanExporter, + data: Collection, + ): CompletableResultCode = delegate.export(data) + + override fun shutdownDelegate(delegate: SpanExporter): CompletableResultCode = delegate.shutdown() + + override fun export(spans: Collection): CompletableResultCode = bufferOrDelegate(spans) + + override fun flush(): CompletableResultCode = + withDelegateOrNull { delegate -> + delegate?.flush() ?: CompletableResultCode.ofSuccess() + } +} diff --git a/core/src/main/java/io/opentelemetry/android/export/BufferedDelegatingExporter.kt b/core/src/main/java/io/opentelemetry/android/export/BufferedDelegatingExporter.kt new file mode 100644 index 000000000..da8dc4337 --- /dev/null +++ b/core/src/main/java/io/opentelemetry/android/export/BufferedDelegatingExporter.kt @@ -0,0 +1,100 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.android.export + +import io.opentelemetry.sdk.common.CompletableResultCode +import java.util.concurrent.atomic.AtomicBoolean + +/** + * An in-memory buffer delegating signal exporter that buffers signal in memory until a delegate is set. + * Once a delegate is set, the buffered signals are exported to the delegate. + * + * The buffer size is set to 5,000 by default. If the buffer is full, the exporter will drop new signals. + */ +internal abstract class BufferedDelegatingExporter(private val bufferedSignals: Int = 5_000) { + @Volatile + private var delegate: D? = null + private val buffer = arrayListOf() + private val lock = Any() + private var isShutDown = AtomicBoolean(false) + + /** + * Sets the delegate for this exporter and flushes the buffer to the delegate. + * + * If the delegate has already been set, an [IllegalStateException] will be thrown. + * If this exporter has been shut down, the delegate will be shut down immediately. + * + * @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) { "Exporter delegate has already been set." } + + flushToDelegate(delegate) + + this.delegate = delegate + + if (isShutDown.get()) { + shutdownDelegate(delegate) + } + } + } + + /** + * Buffers the given data if the delegate has not been set, otherwise exports the data to the delegate. + * + * @param data the data to buffer or export + */ + protected fun bufferOrDelegate(data: Collection): CompletableResultCode = + withDelegateOrNull { + if (it != null) { + exportToDelegate(it, data) + } else { + val amountToTake = bufferedSignals - buffer.size + buffer.addAll(data.take(amountToTake)) + CompletableResultCode.ofSuccess() + } + } + + /** + * Executes the given block with the delegate if it has been set, otherwise executes the block with a null delegate. + * + * @param block the block to execute + */ + protected fun withDelegateOrNull(block: (D?) -> R): R { + delegate?.let { return block(it) } + return synchronized(lock) { block(delegate) } + } + + open fun shutdown(): CompletableResultCode = bufferedShutDown() + + protected abstract fun exportToDelegate( + delegate: D, + data: Collection, + ): CompletableResultCode + + protected abstract fun shutdownDelegate(delegate: D): CompletableResultCode + + private fun flushToDelegate(delegate: D) { + exportToDelegate(delegate, buffer) + buffer.clear() + buffer.trimToSize() + } + + private fun bufferedShutDown(): CompletableResultCode { + isShutDown.set(true) + + return withDelegateOrNull { + if (it != null) { + shutdownDelegate(it) + } else { + CompletableResultCode.ofSuccess() + } + } + } +} diff --git a/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java b/core/src/test/java/io/opentelemetry/android/OpenTelemetryRumBuilderTest.java index 67d425115..951e899b1 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..d6f0b5460 --- /dev/null +++ b/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingSpanExporterTest.kt @@ -0,0 +1,118 @@ +/* + * 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 + +class BufferDelegatingSpanExporterTest { + @Test + fun `test setDelegate`() { + val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter() + val spanExporter = InMemorySpanExporter.create() + + val spanData = mockk() + bufferDelegatingSpanExporter.export(listOf(spanData)) + bufferDelegatingSpanExporter.setDelegate(spanExporter) + + assertThat(spanExporter.finishedSpanItems) + .containsExactly(spanData) + } + + @Test + fun `test buffer limit handling`() { + val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter(10) + val spanExporter = InMemorySpanExporter.create() + + repeat(11) { + val spanData = mockk() + bufferDelegatingSpanExporter.export(listOf(spanData)) + } + + bufferDelegatingSpanExporter.setDelegate(spanExporter) + + assertThat(spanExporter.finishedSpanItems) + .hasSize(10) + } + + @Test + fun `test flush with delegate`() { + val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter() + val delegate = spyk() + + val spanData = mockk() + bufferDelegatingSpanExporter.export(listOf(spanData)) + + bufferDelegatingSpanExporter.setDelegate(delegate) + + verify(exactly = 0) { delegate.flush() } + + bufferDelegatingSpanExporter.flush() + + verify { delegate.flush() } + } + + @Test + fun `test export with delegate`() { + val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter() + val delegate = spyk() + + val spanData = mockk() + bufferDelegatingSpanExporter.export(listOf(spanData)) + + verify(exactly = 0) { delegate.export(any()) } + + bufferDelegatingSpanExporter.setDelegate(delegate) + + verify(exactly = 1) { delegate.export(any()) } + + val spanData2 = mockk() + bufferDelegatingSpanExporter.export(listOf(spanData2)) + + verify(exactly = 2) { delegate.export(any()) } + } + + @Test + fun `test shutdown with delegate`() { + val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter() + val delegate = spyk() + + bufferDelegatingSpanExporter.setDelegate(delegate) + + bufferDelegatingSpanExporter.shutdown() + + verify { delegate.shutdown() } + } + + @Test + fun `test flush without delegate`() { + val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter() + + val spanData = mockk() + bufferDelegatingSpanExporter.export(listOf(spanData)) + + val flushResult = bufferDelegatingSpanExporter.flush() + + assertThat(flushResult.isSuccess).isTrue() + } + + @Test + fun `test shutdown without delegate`() { + val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter() + + val spanData = mockk() + bufferDelegatingSpanExporter.export(listOf(spanData)) + + val shutdownResult = bufferDelegatingSpanExporter.shutdown() + + assertThat(shutdownResult.isSuccess).isTrue() + } +}