From 5c4f0e5f77fb4daaf2d28986f9ddb48f9409baaf Mon Sep 17 00:00:00 2001 From: Nicklas Ansman Date: Wed, 11 Dec 2024 16:54:51 -0500 Subject: [PATCH] Address the comments from the reviews --- .../android/OpenTelemetryRumBuilder.java | 62 ++++--- .../export/BufferDelegatingLogExporter.kt | 27 +-- .../export/BufferDelegatingSpanExporter.kt | 27 +-- .../export/BufferedDelegatingExporter.kt | 100 ----------- .../android/export/DelegatingExporter.kt | 161 ++++++++++++++++++ .../BufferDelegatingSpanExporterTest.kt | 109 ++++++------ 6 files changed, 279 insertions(+), 207 deletions(-) delete mode 100644 core/src/main/java/io/opentelemetry/android/export/BufferedDelegatingExporter.kt create mode 100644 core/src/main/java/io/opentelemetry/android/export/DelegatingExporter.kt diff --git a/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java b/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java index 58bb35aaf..66b3c89be 100644 --- a/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java +++ b/core/src/main/java/io/opentelemetry/android/OpenTelemetryRumBuilder.java @@ -96,13 +96,11 @@ public final class OpenTelemetryRumBuilder { private Resource resource; - private final Object lock = new Object(); + private boolean isBuilt = false; - // Writes guarded by "lock" - @Nullable private volatile ServiceManager serviceManager; + @Nullable private ServiceManager serviceManager; - // Writes guarded by "lock" - @Nullable private volatile ExportScheduleHandler exportScheduleHandler; + @Nullable private ExportScheduleHandler exportScheduleHandler; private static TextMapPropagator buildDefaultPropagator() { return TextMapPropagator.composite( @@ -129,6 +127,7 @@ public static OpenTelemetryRumBuilder create(Application application, OtelRumCon * @return {@code this} */ public OpenTelemetryRumBuilder setResource(Resource resource) { + checkNotBuilt(); this.resource = resource; return this; } @@ -141,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; } @@ -180,6 +180,7 @@ public OpenTelemetryRumBuilder addTracerProviderCustomizer( */ public OpenTelemetryRumBuilder addMeterProviderCustomizer( BiFunction customizer) { + checkNotBuilt(); meterProviderCustomizers.add(customizer); return this; } @@ -200,6 +201,7 @@ public OpenTelemetryRumBuilder addMeterProviderCustomizer( public OpenTelemetryRumBuilder addLoggerProviderCustomizer( BiFunction customizer) { + checkNotBuilt(); loggerProviderCustomizers.add(customizer); return this; } @@ -211,6 +213,7 @@ public OpenTelemetryRumBuilder addLoggerProviderCustomizer( */ public OpenTelemetryRumBuilder addInstrumentation(AndroidInstrumentation instrumentation) { instrumentations.add(instrumentation); + checkNotBuilt(); return this; } @@ -225,6 +228,7 @@ public OpenTelemetryRumBuilder addInstrumentation(AndroidInstrumentation instrum public OpenTelemetryRumBuilder addPropagatorCustomizer( Function propagatorCustomizer) { requireNonNull(propagatorCustomizer, "propagatorCustomizer"); + checkNotBuilt(); Function existing = this.propagatorCustomizer; this.propagatorCustomizer = @@ -244,6 +248,7 @@ public OpenTelemetryRumBuilder addPropagatorCustomizer( public OpenTelemetryRumBuilder addSpanExporterCustomizer( Function spanExporterCustomizer) { requireNonNull(spanExporterCustomizer, "spanExporterCustomizer"); + checkNotBuilt(); Function existing = this.spanExporterCustomizer; this.spanExporterCustomizer = @@ -263,6 +268,7 @@ public OpenTelemetryRumBuilder addSpanExporterCustomizer( public OpenTelemetryRumBuilder addLogRecordExporterCustomizer( Function logRecordExporterCustomizer) { + checkNotBuilt(); Function existing = this.logRecordExporterCustomizer; this.logRecordExporterCustomizer = @@ -283,6 +289,10 @@ 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); @@ -373,11 +383,7 @@ private void initializeExporters( @NonNull private ServiceManager getServiceManager() { if (serviceManager == null) { - synchronized (lock) { - if (serviceManager == null) { - serviceManager = ServiceManagerImpl.Companion.create(application); - } - } + serviceManager = ServiceManagerImpl.Companion.create(application); } // This can never be null since we never write `null` to it return requireNonNull(serviceManager); @@ -385,9 +391,8 @@ private ServiceManager getServiceManager() { public OpenTelemetryRumBuilder setServiceManager(@NonNull ServiceManager serviceManager) { requireNonNull(serviceManager, "serviceManager cannot be null"); - synchronized (lock) { - this.serviceManager = serviceManager; - } + checkNotBuilt(); + this.serviceManager = serviceManager; return this; } @@ -398,9 +403,8 @@ public OpenTelemetryRumBuilder setServiceManager(@NonNull ServiceManager service public OpenTelemetryRumBuilder setExportScheduleHandler( @NonNull ExportScheduleHandler exportScheduleHandler) { requireNonNull(exportScheduleHandler, "exportScheduleHandler cannot be null"); - synchronized (lock) { - this.exportScheduleHandler = exportScheduleHandler; - } + checkNotBuilt(); + this.exportScheduleHandler = exportScheduleHandler; return this; } @@ -423,18 +427,13 @@ private StorageConfiguration createStorageConfiguration() throws IOException { private void scheduleDiskTelemetryReader(@Nullable SignalFromDiskExporter signalExporter) { if (exportScheduleHandler == null) { - 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); - } - } + 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 = @@ -461,6 +460,7 @@ private void scheduleDiskTelemetryReader(@Nullable SignalFromDiskExporter signal * @return this */ public OpenTelemetryRumBuilder addOtelSdkReadyListener(Consumer callback) { + checkNotBuilt(); otelSdkReadyListeners.add(callback); return this; } @@ -574,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 index d75a22b24..563c0fc11 100644 --- a/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingLogExporter.kt +++ b/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingLogExporter.kt @@ -17,19 +17,22 @@ import io.opentelemetry.sdk.logs.export.LogRecordExporter */ internal class BufferDelegatingLogExporter( maxBufferedLogs: Int = 5_000, -) : BufferedDelegatingExporter(bufferedSignals = maxBufferedLogs), - LogRecordExporter { - override fun exportToDelegate( - delegate: LogRecordExporter, - data: Collection, - ): CompletableResultCode = delegate.export(data) +) : LogRecordExporter { + private val delegatingExporter = + DelegatingExporter( + doExport = LogRecordExporter::export, + doFlush = LogRecordExporter::flush, + doShutdown = LogRecordExporter::shutdown, + maxBufferedData = maxBufferedLogs, + ) - override fun shutdownDelegate(delegate: LogRecordExporter): CompletableResultCode = delegate.shutdown() + fun setDelegate(delegate: LogRecordExporter) { + delegatingExporter.setDelegate(delegate) + } - override fun export(logs: Collection): CompletableResultCode = bufferOrDelegate(logs) + override fun export(logs: Collection): CompletableResultCode = delegatingExporter.export(logs) - override fun flush(): CompletableResultCode = - withDelegateOrNull { delegate -> - delegate?.flush() ?: CompletableResultCode.ofSuccess() - } + 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 index f6683a375..88724a483 100644 --- a/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingSpanExporter.kt +++ b/core/src/main/java/io/opentelemetry/android/export/BufferDelegatingSpanExporter.kt @@ -17,19 +17,22 @@ import io.opentelemetry.sdk.trace.export.SpanExporter */ internal class BufferDelegatingSpanExporter( maxBufferedSpans: Int = 5_000, -) : BufferedDelegatingExporter(bufferedSignals = maxBufferedSpans), - SpanExporter { - override fun exportToDelegate( - delegate: SpanExporter, - data: Collection, - ): CompletableResultCode = delegate.export(data) +) : SpanExporter { + private val delegatingExporter = + DelegatingExporter( + doExport = SpanExporter::export, + doFlush = SpanExporter::flush, + doShutdown = SpanExporter::shutdown, + maxBufferedData = maxBufferedSpans, + ) - override fun shutdownDelegate(delegate: SpanExporter): CompletableResultCode = delegate.shutdown() + fun setDelegate(delegate: SpanExporter) { + delegatingExporter.setDelegate(delegate) + } - override fun export(spans: Collection): CompletableResultCode = bufferOrDelegate(spans) + override fun export(spans: Collection): CompletableResultCode = delegatingExporter.export(spans) - override fun flush(): CompletableResultCode = - withDelegateOrNull { delegate -> - delegate?.flush() ?: CompletableResultCode.ofSuccess() - } + override fun flush(): CompletableResultCode = delegatingExporter.flush() + + override fun shutdown(): CompletableResultCode = delegatingExporter.shutdown() } diff --git a/core/src/main/java/io/opentelemetry/android/export/BufferedDelegatingExporter.kt b/core/src/main/java/io/opentelemetry/android/export/BufferedDelegatingExporter.kt deleted file mode 100644 index da8dc4337..000000000 --- a/core/src/main/java/io/opentelemetry/android/export/BufferedDelegatingExporter.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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/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/export/BufferDelegatingSpanExporterTest.kt b/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingSpanExporterTest.kt index d6f0b5460..f415f61a6 100644 --- a/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingSpanExporterTest.kt +++ b/core/src/test/java/io/opentelemetry/android/export/BufferDelegatingSpanExporterTest.kt @@ -12,30 +12,52 @@ 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 setDelegate`() { - val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter() - val spanExporter = InMemorySpanExporter.create() + fun `test no data`() { + bufferDelegatingSpanExporter.setDelegate(delegate) + + verify(exactly = 0) { delegate.export(any()) } + verify(exactly = 0) { delegate.flush() } + verify(exactly = 0) { delegate.shutdown() } + } - val spanData = mockk() + @Test + fun `test setDelegate`() { bufferDelegatingSpanExporter.export(listOf(spanData)) - bufferDelegatingSpanExporter.setDelegate(spanExporter) + bufferDelegatingSpanExporter.setDelegate(delegate) - assertThat(spanExporter.finishedSpanItems) + 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() - repeat(11) { - val spanData = mockk() - bufferDelegatingSpanExporter.export(listOf(spanData)) - } + val overflowResult = bufferDelegatingSpanExporter.export(listOf(mockk())) + assertThat(overflowResult.isDone).isTrue() + assertThat(overflowResult.isSuccess).isFalse() + assertThat(overflowResult.failureThrowable).isInstanceOf(BufferOverflowException::class.java) bufferDelegatingSpanExporter.setDelegate(spanExporter) @@ -45,74 +67,51 @@ class BufferDelegatingSpanExporterTest { @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() } + val result = bufferDelegatingSpanExporter.flush() + verify(exactly = 1) { delegate.flush() } + assertThat(result.isSuccess).isTrue() + } - bufferDelegatingSpanExporter.flush() + @Test + fun `test flush without delegate`() { + val result = bufferDelegatingSpanExporter.flush() + assertThat(result.isDone).isFalse() - verify { delegate.flush() } + bufferDelegatingSpanExporter.setDelegate(delegate) + verify(exactly = 1) { delegate.flush() } + assertThat(result.isSuccess).isTrue() } @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()) } + assertThat(delegate.finishedSpanItems).containsExactly(spanData) val spanData2 = mockk() - bufferDelegatingSpanExporter.export(listOf(spanData2)) + val result = bufferDelegatingSpanExporter.export(listOf(spanData2)) - verify(exactly = 2) { delegate.export(any()) } + assertThat(delegate.finishedSpanItems).containsExactly(spanData, spanData2) + assertThat(result.isSuccess).isTrue() } @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() + val result = bufferDelegatingSpanExporter.shutdown() + verify(exactly = 1) { delegate.shutdown() } + assertThat(result.isSuccess).isTrue() } @Test fun `test shutdown without delegate`() { - val bufferDelegatingSpanExporter = BufferDelegatingSpanExporter() - - val spanData = mockk() - bufferDelegatingSpanExporter.export(listOf(spanData)) + val result = bufferDelegatingSpanExporter.shutdown() + assertThat(result.isDone).isFalse() - val shutdownResult = bufferDelegatingSpanExporter.shutdown() - - assertThat(shutdownResult.isSuccess).isTrue() + bufferDelegatingSpanExporter.setDelegate(delegate) + assertThat(result.isSuccess).isTrue() } }