From a76b5426de3f8da0951b5b49d246e71c023b5910 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:54:57 +0200 Subject: [PATCH 01/30] Making builder static --- .../contrib/disk/buffering/exporters/SpanToDiskExporter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java index 2bda19da9..670c7cab7 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java @@ -26,7 +26,7 @@ private SpanToDiskExporter( this.callback = callback; } - public Builder builder(SignalStorage.Span storage) { + public static Builder builder(SignalStorage.Span storage) { return new Builder(storage); } From 54cdbbfd03a3f8b67655389c6ceccd7219d45ecb Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Thu, 21 Aug 2025 10:24:31 +0200 Subject: [PATCH 02/30] Writing spans using FileSpanStorage --- .../internal/storage/FileSpanStorage.java | 59 +++++++++++++++++-- .../internal/storage/FolderManager.java | 45 ++++++++++++-- .../buffering/internal/storage/Storage.java | 34 ++++++----- .../buffering/storage/result/WriteResult.java | 8 ++- .../exporters/SignalStorageExporterTest.java | 6 +- .../internal/storage/FolderManagerTest.java | 17 ++++++ 6 files changed, 140 insertions(+), 29 deletions(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java index 5ba51790f..01e4ab7ce 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java @@ -5,30 +5,81 @@ package io.opentelemetry.contrib.disk.buffering.internal.storage; +import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; +import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.contrib.disk.buffering.storage.result.WriteResult; +import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.trace.data.SpanData; +import java.io.File; +import java.io.IOException; import java.util.Collection; import java.util.Iterator; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nonnull; /** Default storage implementation where items are stored in multiple protobuf files. */ public final class FileSpanStorage implements SignalStorage.Span { + private final Storage storage; + private final SignalSerializer serializer; + private final Logger logger = Logger.getLogger(FileSpanStorage.class.getName()); + private final AtomicBoolean isClosed = new AtomicBoolean(false); + + public static FileSpanStorage create(File destinationDir, StorageConfiguration configuration) { + return create(destinationDir, configuration, Clock.getDefault()); + } + + public static FileSpanStorage create( + File destinationDir, StorageConfiguration configuration, Clock clock) { + FolderManager folderManager = FolderManager.create(destinationDir, configuration, clock); + return new FileSpanStorage( + new Storage(folderManager, configuration.isDebugEnabled()), SignalSerializer.ofSpans()); + } + + FileSpanStorage(Storage storage, SignalSerializer serializer) { + this.storage = storage; + this.serializer = serializer; + } @Override public CompletableFuture write(Collection items) { - throw new UnsupportedOperationException("For next PR"); + logger.finer("Intercepting batch."); + try { + serializer.initialize(items); + if (storage.write(serializer)) { + return CompletableFuture.completedFuture(WriteResult.successful()); + } + logger.info("Could not store batch in disk."); + return CompletableFuture.completedFuture(WriteResult.error(null)); + } catch (IOException e) { + logger.log( + Level.WARNING, + "An unexpected error happened while attempting to write the data in disk.", + e); + return CompletableFuture.completedFuture(WriteResult.error(e)); + } finally { + serializer.reset(); + } } @Override public CompletableFuture clear() { - throw new UnsupportedOperationException("For next PR"); + try { + storage.clear(); + return CompletableFuture.completedFuture(WriteResult.successful()); + } catch (IOException e) { + return CompletableFuture.completedFuture(WriteResult.error(e)); + } } @Override - public void close() { - throw new UnsupportedOperationException("For next PR"); + public void close() throws IOException { + if (isClosed.compareAndSet(false, true)) { + storage.close(); + } } @Nonnull diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java index 1f76419ab..cad1d8302 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java @@ -14,6 +14,8 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import javax.annotation.Nullable; import org.jetbrains.annotations.NotNull; @@ -25,6 +27,18 @@ public final class FolderManager implements Closeable { @Nullable private ReadableFile currentReadableFile; @Nullable private WritableFile currentWritableFile; + public static FolderManager create( + File destinationDir, StorageConfiguration configuration, Clock clock) { + if (destinationDir.isFile()) { + throw new IllegalArgumentException("destinationDir must be a directory"); + } else if (!destinationDir.exists()) { + if (!destinationDir.mkdirs()) { + throw new IllegalStateException("Could not created dir " + destinationDir); + } + } + return new FolderManager(destinationDir, configuration, clock); + } + public FolderManager(File folder, StorageConfiguration configuration, Clock clock) { this.folder = folder; this.configuration = configuration; @@ -33,12 +47,7 @@ public FolderManager(File folder, StorageConfiguration configuration, Clock cloc @Override public void close() throws IOException { - if (currentReadableFile != null) { - currentReadableFile.close(); - } - if (currentWritableFile != null) { - currentWritableFile.close(); - } + closeCurrentFiles(); } @Nullable @@ -68,6 +77,21 @@ public synchronized WritableFile createWritableFile() throws IOException { return currentWritableFile; } + public synchronized void clear() throws IOException { + closeCurrentFiles(); + List undeletedFiles = new ArrayList<>(); + + for (File file : Objects.requireNonNull(folder.listFiles())) { + if (!file.delete()) { + undeletedFiles.add(file); + } + } + + if (!undeletedFiles.isEmpty()) { + throw new IOException("Could not delete files " + undeletedFiles); + } + } + @Nullable private File findReadableFile() throws IOException { long currentTime = nowMillis(clock); @@ -152,4 +176,13 @@ private boolean hasExpiredForReading(long systemCurrentTimeMillis, long createdT return systemCurrentTimeMillis > (createdTimeInMillis + configuration.getMaxFileAgeForReadMillis()); } + + private synchronized void closeCurrentFiles() throws IOException { + if (currentReadableFile != null) { + currentReadableFile.close(); + } + if (currentWritableFile != null) { + currentWritableFile.close(); + } + } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java index 86b5284ca..e1988507a 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java @@ -19,9 +19,9 @@ import java.io.Closeable; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.logging.Logger; -import javax.annotation.Nullable; public final class Storage implements Closeable { private static final int MAX_ATTEMPTS = 3; @@ -29,8 +29,8 @@ public final class Storage implements Closeable { private final FolderManager folderManager; private final boolean debugEnabled; private final AtomicBoolean isClosed = new AtomicBoolean(false); - @Nullable private WritableFile writableFile; - @Nullable private ReadableFile readableFile; + private final AtomicReference writableFileRef = new AtomicReference<>(); + private final AtomicReference readableFileRef = new AtomicReference<>(); public Storage(FolderManager folderManager, boolean debugEnabled) { this.folderManager = folderManager; @@ -66,20 +66,23 @@ private boolean write(SignalSerializer marshaler, int attemptNumber) throws I logger.log("Max number of attempts to write buffered data exceeded.", WARNING); return false; } + WritableFile writableFile = writableFileRef.get(); if (writableFile == null) { writableFile = folderManager.createWritableFile(); + writableFileRef.set(writableFile); logger.log("Created new writableFile: " + writableFile); } WritableResult result = writableFile.append(marshaler); if (result != WritableResult.SUCCEEDED) { // Retry with new file - writableFile = null; + writableFileRef.set(null); return write(marshaler, ++attemptNumber); } return true; } public void flush() throws IOException { + WritableFile writableFile = writableFileRef.get(); if (writableFile != null) { writableFile.flush(); } else { @@ -95,10 +98,14 @@ public void flush() throws IOException { */ public ReadableResult readAndProcess(Function processing) throws IOException { - return readAndProcess(processing, 1); + return doReadAndProcess(processing, 1); } - private ReadableResult readAndProcess( + public void clear() throws IOException { + folderManager.clear(); + } + + private ReadableResult doReadAndProcess( Function processing, int attemptNumber) throws IOException { if (isClosed.get()) { logger.log("Refusing to read from storage after being closed."); @@ -108,9 +115,11 @@ private ReadableResult readAndProcess( logger.log("Maximum number of attempts to read and process buffered data exceeded.", WARNING); return ReadableResult.FAILED; } + ReadableFile readableFile = readableFileRef.get(); if (readableFile == null) { logger.log("Obtaining a new readableFile from the folderManager."); readableFile = folderManager.getReadableFile(); + readableFileRef.set(readableFile); if (readableFile == null) { logger.log("Unable to get or create readable file."); return ReadableResult.FAILED; @@ -124,8 +133,8 @@ private ReadableResult readAndProcess( return result; default: // Retry with new file - readableFile = null; - return readAndProcess(processing, ++attemptNumber); + readableFileRef.set(null); + return doReadAndProcess(processing, ++attemptNumber); } } @@ -133,12 +142,9 @@ private ReadableResult readAndProcess( public void close() throws IOException { logger.log("Closing disk buffering storage."); if (isClosed.compareAndSet(false, true)) { - if (writableFile != null) { - writableFile.close(); - } - if (readableFile != null) { - readableFile.close(); - } + folderManager.close(); + writableFileRef.set(null); + readableFileRef.set(null); } } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/result/WriteResult.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/result/WriteResult.java index 8e4534a55..85c08ed37 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/result/WriteResult.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/result/WriteResult.java @@ -25,7 +25,11 @@ public interface WriteResult { @Nullable Throwable getError(); - static WriteResult create(boolean successful, @Nullable Throwable error) { - return new DefaultWriteResult(successful, error); + static WriteResult successful() { + return new DefaultWriteResult(/* successful= */ true, null); + } + + static WriteResult error(@Nullable Throwable t) { + return new DefaultWriteResult(/* successful= */ false, t); } } diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java index 6f7db99ef..1283c5cdd 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java @@ -79,7 +79,7 @@ void verifyExportToStorage_failure() { // Without exception when(storage.write(anyCollection())) - .thenReturn(CompletableFuture.completedFuture(WriteResult.create(false, null))); + .thenReturn(CompletableFuture.completedFuture(WriteResult.error(null))); CompletableResultCode resultCode = storageExporter.exportToStorage(Collections.singletonList(item1)); @@ -93,7 +93,7 @@ void verifyExportToStorage_failure() { clearInvocations(callback); Exception exception = new Exception(); when(storage.write(anyCollection())) - .thenReturn(CompletableFuture.completedFuture(WriteResult.create(false, exception))); + .thenReturn(CompletableFuture.completedFuture(WriteResult.error(exception))); resultCode = storageExporter.exportToStorage(Collections.singletonList(item1)); @@ -129,7 +129,7 @@ public Iterator> iterator() { @Nonnull private static CompletableFuture getSuccessfulFuture() { - return CompletableFuture.completedFuture(WriteResult.create(true, null)); + return CompletableFuture.completedFuture(WriteResult.successful()); } } } diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java index ad994c38d..1d1d84c68 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java @@ -9,6 +9,7 @@ import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.MAX_FILE_SIZE; import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.MIN_FILE_AGE_FOR_READ_MILLIS; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -55,6 +56,22 @@ void createWritableFile_withTimeMillisAsName() throws IOException { assertEquals("1000", file.getFile().getName()); } + @Test + void clearFiles() throws IOException { + when(clock.now()) + .thenReturn(MILLISECONDS.toNanos(1000L)) + .thenReturn(MILLISECONDS.toNanos(2000L)); + + // Creating 2 files + folderManager.createWritableFile(); + folderManager.createWritableFile(); + assertThat(rootDir.list()).containsExactlyInAnyOrder("1000", "2000"); + + // Clear + folderManager.clear(); + assertThat(rootDir.list()).isEmpty(); + } + @Test void createWritableFile_andRemoveOldestOne_whenTheAvailableFolderSpaceIsNotEnough() throws IOException { From 9b233ee280622af31d290dd4be08c0563741a04f Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:52:39 +0200 Subject: [PATCH 03/30] Creating StorageIterator --- .../buffering/internal/storage/Storage.java | 107 +++++++++++++----- .../internal/storage/StorageIterator.java | 81 +++++++++++++ .../internal/storage/files/ReadableFile.java | 67 ++++------- .../reader/DelimitedProtoStreamReader.java | 4 +- .../storage/files/reader/ProcessResult.java | 13 --- .../storage/files/reader/ReadResult.java | 15 --- .../storage/files/reader/StreamReader.java | 2 +- .../storage/responses/ReadableResult.java | 14 ++- 8 files changed, 194 insertions(+), 109 deletions(-) create mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/ProcessResult.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/ReadResult.java diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java index e1988507a..00382895c 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java @@ -8,27 +8,29 @@ import static java.util.logging.Level.WARNING; import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporterImpl; +import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.DeserializationException; +import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.ReadableFile; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.WritableFile; -import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.ProcessResult; import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.ReadableResult; import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.WritableResult; import io.opentelemetry.contrib.disk.buffering.internal.utils.DebugLogger; import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; import java.io.Closeable; import java.io.IOException; +import java.util.Collection; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; import java.util.logging.Logger; -public final class Storage implements Closeable { +public final class Storage implements Closeable { private static final int MAX_ATTEMPTS = 3; private final DebugLogger logger; private final FolderManager folderManager; private final boolean debugEnabled; private final AtomicBoolean isClosed = new AtomicBoolean(false); + private final AtomicBoolean activeReadResultAvailable = new AtomicBoolean(false); private final AtomicReference writableFileRef = new AtomicReference<>(); private final AtomicReference readableFileRef = new AtomicReference<>(); @@ -53,11 +55,11 @@ public boolean isDebugEnabled() { * @param marshaler - The data that would be appended to the file. * @throws IOException If an unexpected error happens. */ - public boolean write(SignalSerializer marshaler) throws IOException { + public boolean write(SignalSerializer marshaler) throws IOException { return write(marshaler, 1); } - private boolean write(SignalSerializer marshaler, int attemptNumber) throws IOException { + private boolean write(SignalSerializer marshaler, int attemptNumber) throws IOException { if (isClosed.get()) { logger.log("Refusing to write to storage after being closed."); return false; @@ -93,27 +95,25 @@ public void flush() throws IOException { /** * Attempts to read an item from a ready-to-read file. * - * @param processing Is passed over to {@link ReadableFile#readAndProcess(Function)}. * @throws IOException If an unexpected error happens. */ - public ReadableResult readAndProcess(Function processing) - throws IOException { - return doReadAndProcess(processing, 1); - } - - public void clear() throws IOException { - folderManager.clear(); + public ReadableResult readNext(SignalDeserializer deserializer) throws IOException { + if (activeReadResultAvailable.get()) { + throw new IllegalStateException( + "You must close any previous ReadableResult before requesting a new one"); + } + return doReadAndProcess(deserializer, 1); } - private ReadableResult doReadAndProcess( - Function processing, int attemptNumber) throws IOException { + private ReadableResult doReadAndProcess(SignalDeserializer deserializer, int attemptNumber) + throws IOException { if (isClosed.get()) { logger.log("Refusing to read from storage after being closed."); - return ReadableResult.FAILED; + return null; } if (attemptNumber > MAX_ATTEMPTS) { logger.log("Maximum number of attempts to read and process buffered data exceeded.", WARNING); - return ReadableResult.FAILED; + return null; } ReadableFile readableFile = readableFileRef.get(); if (readableFile == null) { @@ -122,20 +122,33 @@ private ReadableResult doReadAndProcess( readableFileRef.set(readableFile); if (readableFile == null) { logger.log("Unable to get or create readable file."); - return ReadableResult.FAILED; + return null; } } + logger.log("Attempting to read data from " + readableFile); - ReadableResult result = readableFile.readAndProcess(processing); - switch (result) { - case SUCCEEDED: - case TRY_LATER: - return result; - default: - // Retry with new file - readableFileRef.set(null); - return doReadAndProcess(processing, ++attemptNumber); + byte[] result = readableFile.readNext(); + if (result != null) { + try { + Collection items = deserializer.deserialize(result); + return new FileReadResult(items, readableFile); + } catch (DeserializationException e) { + // Data corrupted, clear file. + readableFile.clear(); + } } + + // Retry with new file + readableFileRef.set(null); + return doReadAndProcess(deserializer, ++attemptNumber); + } + + public void clear() throws IOException { + folderManager.clear(); + } + + public boolean isClosed() { + return isClosed.get(); } @Override @@ -147,4 +160,44 @@ public void close() throws IOException { readableFileRef.set(null); } } + + class FileReadResult implements ReadableResult { + private final Collection content; + private final AtomicBoolean itemDeleted = new AtomicBoolean(false); + private final AtomicBoolean closed = new AtomicBoolean(false); + private final AtomicReference readableFile = new AtomicReference<>(); + + FileReadResult(Collection content, ReadableFile readableFile) { + this.content = content; + this.readableFile.set(readableFile); + } + + @Override + public Collection getContent() { + return content; + } + + @Override + public void delete() throws IOException { + if (closed.get()) { + return; + } + if (itemDeleted.compareAndSet(false, true)) { + try { + readableFile.get().removeTopItem(); + } catch (IOException e) { + itemDeleted.set(false); + throw e; + } + } + } + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false, true)) { + activeReadResultAvailable.set(false); + readableFile.set(null); + } + } + } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java new file mode 100644 index 000000000..73ea80ec6 --- /dev/null +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java @@ -0,0 +1,81 @@ +package io.opentelemetry.contrib.disk.buffering.internal.storage; + +import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; +import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.ReadableResult; +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; + +public final class StorageIterator implements Iterator> { + private final Storage storage; + private final SignalDeserializer deserializer; + private final Logger logger = Logger.getLogger(StorageIterator.class.getName()); + + @GuardedBy("this") + @Nullable + private ReadableResult currentResult; + + @GuardedBy("this") + private boolean currentResultConsumed = false; + + public StorageIterator(Storage storage, SignalDeserializer deserializer) { + this.storage = storage; + this.deserializer = deserializer; + } + + @Override + public synchronized boolean hasNext() { + if (storage.isClosed()) { + return false; + } + return findNext(); + } + + @Override + public synchronized Collection next() { + if (findNext()) { + currentResultConsumed = true; + return Objects.requireNonNull(currentResult).getContent(); + } + return null; + } + + @Override + public synchronized void remove() { + if (currentResult != null) { + try { + currentResult.delete(); + } catch (IOException e) { + logger.log(Level.SEVERE, "Error deleting stored item", e); + } + } + } + + private boolean findNext() { + try { + if (currentResult != null) { + if (!currentResultConsumed) { + return true; + } + currentResult.delete(); + currentResult.close(); + currentResult = null; + } + + currentResultConsumed = false; + ReadableResult result = storage.readNext(deserializer); + if (result != null) { + currentResult = result; + return true; + } + } catch (IOException e) { + logger.log(Level.SEVERE, "Error reading from storage", e); + } + return false; + } +} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java index 710e192bb..2a10ac73c 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java @@ -9,18 +9,14 @@ import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.DelimitedProtoStreamReader; -import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.ProcessResult; -import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.ReadResult; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.StreamReader; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.utils.FileStream; -import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.ReadableResult; import io.opentelemetry.sdk.common.Clock; import java.io.File; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -import javax.annotation.Nullable; -import org.jetbrains.annotations.NotNull; +import javax.annotation.Nonnull; /** * Reads from a file and updates it in parallel in order to avoid re-reading the same items later. @@ -32,13 +28,12 @@ *

More information on the overall storage process in the CONTRIBUTING.md file. */ public final class ReadableFile implements FileOperations { - @NotNull private final File file; + @Nonnull private final File file; private final FileStream fileStream; private final StreamReader reader; private final Clock clock; private final long expireTimeMillis; private final AtomicBoolean isClosed = new AtomicBoolean(false); - @Nullable private ReadResult unconsumedResult; public ReadableFile( File file, long createdTimeMillis, Clock clock, StorageConfiguration configuration) @@ -52,7 +47,7 @@ public ReadableFile( } public ReadableFile( - @NotNull File file, + @Nonnull File file, long createdTimeMillis, Clock clock, StorageConfiguration configuration, @@ -68,49 +63,21 @@ public ReadableFile( /** * Reads the next line available in the file and provides it to a {@link Function processing} * which will determine whether to remove the provided line or not. - * - * @param processing - A function that receives the line that has been read and returns a boolean. - * If the processing function returns TRUE, then the provided line will be deleted from the - * source file. If the function returns FALSE, no changes will be applied to the source file. */ - public synchronized ReadableResult readAndProcess(Function processing) - throws IOException { + public synchronized byte[] readNext() throws IOException { if (isClosed.get()) { - return ReadableResult.FAILED; + return null; } if (hasExpired()) { close(); - return ReadableResult.FAILED; - } - ReadResult read = readNextItem(); - if (read == null) { - cleanUp(); - return ReadableResult.FAILED; + return null; } - switch (processing.apply(read.content)) { - case SUCCEEDED: - unconsumedResult = null; - fileStream.truncateTop(); - if (fileStream.size() == 0) { - cleanUp(); - } - return ReadableResult.SUCCEEDED; - case TRY_LATER: - unconsumedResult = read; - return ReadableResult.TRY_LATER; - case CONTENT_INVALID: - cleanUp(); - return ReadableResult.FAILED; - } - return ReadableResult.FAILED; - } - - @Nullable - private ReadResult readNextItem() throws IOException { - if (unconsumedResult != null) { - return unconsumedResult; + byte[] resultBytes = reader.readNext(); + if (resultBytes == null) { + clear(); + return null; } - return reader.readNext(); + return resultBytes; } @Override @@ -123,23 +90,29 @@ public synchronized boolean isClosed() { return isClosed.get(); } - @NotNull + @Nonnull @Override public File getFile() { return file; } - private void cleanUp() throws IOException { + public synchronized void clear() throws IOException { close(); if (!file.delete()) { throw new IOException("Could not delete file: " + file); } } + public synchronized void removeTopItem() throws IOException { + fileStream.truncateTop(); + if (fileStream.size() == 0) { + clear(); + } + } + @Override public synchronized void close() throws IOException { if (isClosed.compareAndSet(false, true)) { - unconsumedResult = null; reader.close(); } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java index 60a8e4f45..81e31ef88 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java @@ -19,7 +19,7 @@ public DelimitedProtoStreamReader(InputStream inputStream) { @Override @Nullable - public ReadResult readNext() throws IOException { + public byte[] readNext() throws IOException { int itemSize = getNextItemSize(); if (itemSize < 1) { return null; @@ -28,7 +28,7 @@ public ReadResult readNext() throws IOException { if (inputStream.read(bytes) < 0) { return null; } - return new ReadResult(bytes); + return bytes; } private int getNextItemSize() { diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/ProcessResult.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/ProcessResult.java deleted file mode 100644 index 696d98d2c..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/ProcessResult.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader; - -/** Result of processing the contents of a file. */ -public enum ProcessResult { - SUCCEEDED, - TRY_LATER, - CONTENT_INVALID -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/ReadResult.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/ReadResult.java deleted file mode 100644 index a9f5d1116..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/ReadResult.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader; - -public final class ReadResult { - /** The consumable data. */ - public final byte[] content; - - public ReadResult(byte[] content) { - this.content = content; - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/StreamReader.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/StreamReader.java index 447315f1e..925422f67 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/StreamReader.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/StreamReader.java @@ -12,7 +12,7 @@ public interface StreamReader extends Closeable { @Nullable - ReadResult readNext() throws IOException; + byte[] readNext() throws IOException; interface Factory { StreamReader create(InputStream stream); diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/responses/ReadableResult.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/responses/ReadableResult.java index 295bc2289..1e2877c7b 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/responses/ReadableResult.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/responses/ReadableResult.java @@ -5,8 +5,14 @@ package io.opentelemetry.contrib.disk.buffering.internal.storage.responses; -public enum ReadableResult { - SUCCEEDED, - FAILED, - TRY_LATER +import java.io.Closeable; +import java.io.IOException; +import java.util.Collection; + +public interface ReadableResult extends Closeable { + /** The consumable data. */ + Collection getContent(); + + /** Delete the items provided in {@link #getContent()} */ + void delete() throws IOException; } From a67f72ec51ff5699d597369b73db601fb3645903 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:55:52 +0200 Subject: [PATCH 04/30] Clean up from disk exporters --- .../buffering/LogRecordFromDiskExporter.java | 44 ------ .../buffering/MetricFromDiskExporter.java | 44 ------ .../disk/buffering/SpanFromDiskExporter.java | 44 ------ .../internal/exporter/FromDiskExporter.java | 15 -- .../exporter/FromDiskExporterBuilder.java | 55 ------- .../exporter/FromDiskExporterImpl.java | 86 ----------- .../buffering/FromDiskExporterImplTest.java | 114 -------------- .../buffering/SpanFromDiskExporterTest.java | 145 ------------------ 8 files changed, 547 deletions(-) delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordFromDiskExporter.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricFromDiskExporter.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanFromDiskExporter.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporter.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporterBuilder.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporterImpl.java delete mode 100644 disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/FromDiskExporterImplTest.java delete mode 100644 disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/SpanFromDiskExporterTest.java diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordFromDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordFromDiskExporter.java deleted file mode 100644 index c26a383d6..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordFromDiskExporter.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporter; -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporterImpl; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -public class LogRecordFromDiskExporter implements FromDiskExporter { - - private final FromDiskExporterImpl delegate; - - public static LogRecordFromDiskExporter create(LogRecordExporter exporter, Storage storage) - throws IOException { - FromDiskExporterImpl delegate = - FromDiskExporterImpl.builder(storage) - .setDeserializer(SignalDeserializer.ofLogs()) - .setExportFunction(exporter::export) - .build(); - return new LogRecordFromDiskExporter(delegate); - } - - private LogRecordFromDiskExporter(FromDiskExporterImpl delegate) { - this.delegate = delegate; - } - - @Override - public boolean exportStoredBatch(long timeout, TimeUnit unit) throws IOException { - return delegate.exportStoredBatch(timeout, unit); - } - - @Override - public void shutdown() throws IOException { - delegate.shutdown(); - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricFromDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricFromDiskExporter.java deleted file mode 100644 index 8bb4f3dcd..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricFromDiskExporter.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporter; -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporterImpl; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.export.MetricExporter; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -public class MetricFromDiskExporter implements FromDiskExporter { - - private final FromDiskExporterImpl delegate; - - public static MetricFromDiskExporter create(MetricExporter exporter, Storage storage) - throws IOException { - FromDiskExporterImpl delegate = - FromDiskExporterImpl.builder(storage) - .setDeserializer(SignalDeserializer.ofMetrics()) - .setExportFunction(exporter::export) - .build(); - return new MetricFromDiskExporter(delegate); - } - - private MetricFromDiskExporter(FromDiskExporterImpl delegate) { - this.delegate = delegate; - } - - @Override - public boolean exportStoredBatch(long timeout, TimeUnit unit) throws IOException { - return delegate.exportStoredBatch(timeout, unit); - } - - @Override - public void shutdown() throws IOException { - delegate.shutdown(); - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanFromDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanFromDiskExporter.java deleted file mode 100644 index e3c7992ba..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanFromDiskExporter.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporter; -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporterImpl; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -public class SpanFromDiskExporter implements FromDiskExporter { - - private final FromDiskExporterImpl delegate; - - public static SpanFromDiskExporter create(SpanExporter exporter, Storage storage) - throws IOException { - FromDiskExporterImpl delegate = - FromDiskExporterImpl.builder(storage) - .setDeserializer(SignalDeserializer.ofSpans()) - .setExportFunction(exporter::export) - .build(); - return new SpanFromDiskExporter(delegate); - } - - private SpanFromDiskExporter(FromDiskExporterImpl delegate) { - this.delegate = delegate; - } - - @Override - public boolean exportStoredBatch(long timeout, TimeUnit unit) throws IOException { - return delegate.exportStoredBatch(timeout, unit); - } - - @Override - public void shutdown() throws IOException { - delegate.shutdown(); - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporter.java deleted file mode 100644 index fdc3bb796..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporter.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.exporter; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -public interface FromDiskExporter { - boolean exportStoredBatch(long timeout, TimeUnit unit) throws IOException; - - void shutdown() throws IOException; -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporterBuilder.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporterBuilder.java deleted file mode 100644 index a91ded1f3..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporterBuilder.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.exporter; - -import static java.util.Collections.emptyList; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.sdk.common.CompletableResultCode; -import java.io.IOException; -import java.util.Collection; -import java.util.function.Function; -import org.jetbrains.annotations.NotNull; - -public class FromDiskExporterBuilder { - - private SignalDeserializer serializer = noopDeserializer(); - private final Storage storage; - - private Function, CompletableResultCode> exportFunction = - x -> CompletableResultCode.ofFailure(); - - public FromDiskExporterBuilder(Storage storage) { - if (storage == null) { - throw new NullPointerException("Storage cannot be null"); - } - this.storage = storage; - } - - @NotNull - private static SignalDeserializer noopDeserializer() { - return x -> emptyList(); - } - - @CanIgnoreReturnValue - public FromDiskExporterBuilder setDeserializer(SignalDeserializer serializer) { - this.serializer = serializer; - return this; - } - - @CanIgnoreReturnValue - public FromDiskExporterBuilder setExportFunction( - Function, CompletableResultCode> exportFunction) { - this.exportFunction = exportFunction; - return this; - } - - public FromDiskExporterImpl build() throws IOException { - return new FromDiskExporterImpl<>(serializer, exportFunction, storage); - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporterImpl.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporterImpl.java deleted file mode 100644 index 5ba5c2390..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/FromDiskExporterImpl.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.exporter; - -import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.DeserializationException; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.ProcessResult; -import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.ReadableResult; -import io.opentelemetry.contrib.disk.buffering.internal.utils.DebugLogger; -import io.opentelemetry.sdk.common.CompletableResultCode; -import java.io.IOException; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.logging.Logger; - -/** - * Signal-type generic class that can read telemetry previously buffered on disk and send it to - * another delegated exporter. - */ -public final class FromDiskExporterImpl implements FromDiskExporter { - private final DebugLogger logger; - private final Storage storage; - private final SignalDeserializer deserializer; - private final Function, CompletableResultCode> exportFunction; - - FromDiskExporterImpl( - SignalDeserializer deserializer, - Function, CompletableResultCode> exportFunction, - Storage storage) { - this.deserializer = deserializer; - this.exportFunction = exportFunction; - this.storage = storage; - this.logger = - DebugLogger.wrap( - Logger.getLogger(FromDiskExporterImpl.class.getName()), storage.isDebugEnabled()); - } - - public static FromDiskExporterBuilder builder(Storage storage) { - return new FromDiskExporterBuilder<>(storage); - } - - /** - * Reads data from the disk and attempts to export it. - * - * @param timeout The amount of time to wait for the wrapped exporter to finish. - * @param unit The unit of the time provided. - * @return true if there was data available, and it was successfully exported within the timeout - * provided. false otherwise. - * @throws IOException If an unexpected error happens. - */ - @Override - public boolean exportStoredBatch(long timeout, TimeUnit unit) throws IOException { - logger.log("Attempting to export " + deserializer.signalType() + " batch from disk."); - ReadableResult result = - storage.readAndProcess( - bytes -> { - logger.log( - "Read " - + bytes.length - + " " - + deserializer.signalType() - + " bytes from storage."); - try { - List telemetry = deserializer.deserialize(bytes); - logger.log( - "Now exporting batch of " + telemetry.size() + " " + deserializer.signalType()); - CompletableResultCode join = exportFunction.apply(telemetry).join(timeout, unit); - return join.isSuccess() ? ProcessResult.SUCCEEDED : ProcessResult.TRY_LATER; - } catch (DeserializationException e) { - return ProcessResult.CONTENT_INVALID; - } - }); - return result == ReadableResult.SUCCEEDED; - } - - @Override - public void shutdown() throws IOException { - storage.close(); - } -} diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/FromDiskExporterImplTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/FromDiskExporterImplTest.java deleted file mode 100644 index e7995c675..000000000 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/FromDiskExporterImplTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.MIN_FILE_AGE_FOR_READ_MILLIS; -import static java.util.Collections.singletonList; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporterImpl; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.DeserializationException; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.TestData; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; -import io.opentelemetry.sdk.common.Clock; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -@SuppressWarnings("unchecked") -class FromDiskExporterImplTest { - private SpanExporter wrapped; - private SignalDeserializer deserializer; - private Clock clock; - private FromDiskExporterImpl exporter; - private final List deserializedData = Collections.emptyList(); - @TempDir File rootDir; - private static final String STORAGE_FOLDER_NAME = SignalTypes.spans.name(); - - @BeforeEach - void setUp() throws IOException { - clock = createClockMock(); - - setUpSerializer(); - wrapped = mock(); - exporter = - FromDiskExporterImpl.builder( - TestData.getStorage(rootDir, SignalTypes.spans, clock)) - .setDeserializer(deserializer) - .setExportFunction(wrapped::export) - .build(); - } - - @Test - void whenExportingStoredBatch_withAvailableData_andSuccessfullyProcessed_returnTrue() - throws IOException { - when(wrapped.export(deserializedData)).thenReturn(CompletableResultCode.ofSuccess()); - - createDummyFile(); - when(clock.now()).thenReturn(MILLISECONDS.toNanos(1000L + MIN_FILE_AGE_FOR_READ_MILLIS)); - - assertThat(exporter.exportStoredBatch(1, TimeUnit.SECONDS)).isTrue(); - } - - @Test - void whenExportingStoredBatch_withAvailableData_andUnsuccessfullyProcessed_returnFalse() - throws IOException { - when(wrapped.export(deserializedData)).thenReturn(CompletableResultCode.ofSuccess()); - - createDummyFile(); - when(clock.now()).thenReturn(1000L + MIN_FILE_AGE_FOR_READ_MILLIS); - - assertThat(exporter.exportStoredBatch(1, TimeUnit.SECONDS)).isFalse(); - } - - @Test - void whenExportingStoredBatch_withNoAvailableData_returnFalse() throws IOException { - assertThat(exporter.exportStoredBatch(1, TimeUnit.SECONDS)).isFalse(); - } - - @Test - void verifyStorageFolderIsCreated() { - assertThat(new File(rootDir, STORAGE_FOLDER_NAME).exists()).isTrue(); - } - - @Test - void whenDeserializationFails_returnFalse() throws IOException { - doThrow(DeserializationException.class).when(deserializer).deserialize(any()); - - assertThat(exporter.exportStoredBatch(1, TimeUnit.SECONDS)).isFalse(); - } - - private void createDummyFile() throws IOException { - File file = new File(rootDir, STORAGE_FOLDER_NAME + "/" + 1000L); - Files.write(file.toPath(), singletonList("First line")); - } - - private void setUpSerializer() throws DeserializationException { - deserializer = mock(); - when(deserializer.deserialize(any())).thenReturn(deserializedData); - } - - private static Clock createClockMock() { - Clock mock = mock(); - when(mock.now()).thenReturn(MILLISECONDS.toNanos(1000L)); - return mock; - } -} diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/SpanFromDiskExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/SpanFromDiskExporterTest.java deleted file mode 100644 index 2ea0d2b8a..000000000 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/SpanFromDiskExporterTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.api.trace.StatusCode; -import io.opentelemetry.api.trace.TraceFlags; -import io.opentelemetry.api.trace.TraceState; -import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.spans.models.SpanDataImpl; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; -import io.opentelemetry.contrib.disk.buffering.testutils.TestData; -import io.opentelemetry.sdk.common.Clock; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.io.File; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.ArgumentCaptor; - -class SpanFromDiskExporterTest { - - @TempDir File tempDir; - - @SuppressWarnings("unchecked") - @Test - void fromDisk() throws Exception { - Clock clock = mock(Clock.class); - long start = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); - when(clock.now()).thenReturn(start); - Storage storage = - Storage.builder(SignalTypes.spans) - .setStorageConfiguration(StorageConfiguration.builder().setRootDir(tempDir).build()) - .setStorageClock(clock) - .build(); - - List spans = writeSomeSpans(storage); - - when(clock.now()).thenReturn(start + TimeUnit.SECONDS.toNanos(60)); - - SpanExporter exporter = mock(); - ArgumentCaptor> capture = ArgumentCaptor.forClass(Collection.class); - when(exporter.export(capture.capture())).thenReturn(CompletableResultCode.ofSuccess()); - - SpanFromDiskExporter testClass = SpanFromDiskExporter.create(exporter, storage); - boolean result = testClass.exportStoredBatch(30, TimeUnit.SECONDS); - assertThat(result).isTrue(); - List exportedSpans = (List) capture.getValue(); - - long now = spans.get(0).getStartEpochNanos(); - SpanData expected1 = makeSpan1(TraceFlags.getSampled(), now); - SpanData expected2 = makeSpan2(TraceFlags.getSampled(), now); - - assertThat(exportedSpans.get(0)).isEqualTo(expected1); - assertThat(exportedSpans.get(1)).isEqualTo(expected2); - assertThat(exportedSpans).containsExactly(expected1, expected2); - - verify(exporter).export(eq(Arrays.asList(expected1, expected2))); - } - - private static List writeSomeSpans(Storage storage) throws Exception { - long now = System.currentTimeMillis() * 1_000_000; - SpanData span1 = makeSpan1(TraceFlags.getDefault(), now); - SpanData span2 = makeSpan2(TraceFlags.getSampled(), now); - List spans = Arrays.asList(span1, span2); - - storage.write(SignalSerializer.ofSpans().initialize(spans)); - storage.flush(); - return spans; - } - - private static SpanData makeSpan1(TraceFlags parentSpanContextFlags, long now) { - Attributes attributes = Attributes.of(AttributeKey.stringKey("foo"), "bar"); - SpanContext parentContext = TestData.makeContext(parentSpanContextFlags, TestData.SPAN_ID); - return SpanDataImpl.builder() - .setName("span1") - .setSpanContext( - SpanContext.create( - TestData.TRACE_ID, - TestData.SPAN_ID, - TraceFlags.getDefault(), - TraceState.getDefault())) - .setParentSpanContext(parentContext) - .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_FULL) - .setStatus(StatusData.create(StatusCode.OK, "whatever")) - .setAttributes(attributes) - .setKind(SpanKind.SERVER) - .setStartEpochNanos(now) - .setEndEpochNanos(now + 50_000_000) - .setTotalRecordedEvents(0) - .setTotalRecordedLinks(0) - .setTotalAttributeCount(attributes.size()) - .setLinks(Collections.emptyList()) - .setEvents(Collections.emptyList()) - .setResource(Resource.getDefault()) - .build(); - } - - private static SpanData makeSpan2(TraceFlags parentSpanContextFlags, long now) { - Attributes attributes = Attributes.of(AttributeKey.stringKey("bar"), "baz"); - String spanId = "aaaaaaaaa12312312"; - SpanContext parentContext = TestData.makeContext(parentSpanContextFlags, spanId); - return SpanDataImpl.builder() - .setName("span2") - .setSpanContext( - SpanContext.create( - TestData.TRACE_ID, spanId, TraceFlags.getSampled(), TraceState.getDefault())) - .setParentSpanContext(parentContext) - .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_FULL) - .setStatus(StatusData.create(StatusCode.OK, "excellent")) - .setAttributes(attributes) - .setKind(SpanKind.CLIENT) - .setStartEpochNanos(now + 12) - .setEndEpochNanos(now + 12 + 40_000_000) - .setTotalRecordedEvents(0) - .setTotalRecordedLinks(0) - .setTotalAttributeCount(attributes.size()) - .setLinks(Collections.emptyList()) - .setEvents(Collections.emptyList()) - .setResource(Resource.getDefault()) - .build(); - } -} From 01993c3d73a91abd49de8d80d17f084e792afb32 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:02:52 +0200 Subject: [PATCH 05/30] Specifying storage type --- .../contrib/disk/buffering/LogRecordToDiskExporter.java | 5 +++-- .../contrib/disk/buffering/MetricToDiskExporter.java | 4 ++-- .../contrib/disk/buffering/SpanToDiskExporter.java | 4 ++-- .../disk/buffering/internal/exporter/ToDiskExporter.java | 6 +++--- .../buffering/internal/exporter/ToDiskExporterBuilder.java | 4 ++-- .../disk/buffering/internal/storage/FileSpanStorage.java | 6 +++--- .../contrib/disk/buffering/internal/storage/Storage.java | 4 +--- .../disk/buffering/exporters/SignalStorageExporterTest.java | 1 - 8 files changed, 16 insertions(+), 18 deletions(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java index 665e90f76..9af9d9d27 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java @@ -28,9 +28,10 @@ public class LogRecordToDiskExporter implements LogRecordExporter { * @param storage - The Storage instance that specifies how storage is managed. * @return A new LogRecordToDiskExporter instance. */ - public static LogRecordToDiskExporter create(LogRecordExporter delegate, Storage storage) { + public static LogRecordToDiskExporter create( + LogRecordExporter delegate, Storage storage) { ToDiskExporter toDisk = - ToDiskExporter.builder(storage) + ToDiskExporter.builder(storage) .setSerializer(SignalSerializer.ofLogs()) .setExportFunction(delegate::export) .build(); diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java index 83d2fc73c..5a4047ba8 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java @@ -33,9 +33,9 @@ public class MetricToDiskExporter implements MetricExporter { * @param storage - The Storage instance that specifies how storage is managed. * @return A new MetricToDiskExporter instance. */ - public static MetricToDiskExporter create(MetricExporter delegate, Storage storage) { + public static MetricToDiskExporter create(MetricExporter delegate, Storage storage) { ToDiskExporter toDisk = - ToDiskExporter.builder(storage) + ToDiskExporter.builder(storage) .setSerializer(SignalSerializer.ofMetrics()) .setExportFunction(delegate::export) .build(); diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java index d5ca81518..b75a9da76 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java @@ -29,9 +29,9 @@ public class SpanToDiskExporter implements SpanExporter { * @param storage - The Storage instance that specifies how storage is managed. * @return A new SpanToDiskExporter instance. */ - public static SpanToDiskExporter create(SpanExporter delegate, Storage storage) { + public static SpanToDiskExporter create(SpanExporter delegate, Storage storage) { ToDiskExporter toDisk = - ToDiskExporter.builder(storage) + ToDiskExporter.builder(storage) .setSerializer(SignalSerializer.ofSpans()) .setExportFunction(delegate::export) .build(); diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporter.java index 5b2dcd186..35cd4f444 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporter.java @@ -18,14 +18,14 @@ public class ToDiskExporter { private final DebugLogger logger; - private final Storage storage; + private final Storage storage; private final SignalSerializer serializer; private final Function, CompletableResultCode> exportFunction; ToDiskExporter( SignalSerializer serializer, Function, CompletableResultCode> exportFunction, - Storage storage) { + Storage storage) { this.serializer = serializer; this.exportFunction = exportFunction; this.storage = storage; @@ -34,7 +34,7 @@ public class ToDiskExporter { Logger.getLogger(ToDiskExporter.class.getName()), storage.isDebugEnabled()); } - public static ToDiskExporterBuilder builder(Storage storage) { + public static ToDiskExporterBuilder builder(Storage storage) { return new ToDiskExporterBuilder<>(storage); } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterBuilder.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterBuilder.java index be75a3976..5f5252b2d 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterBuilder.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterBuilder.java @@ -16,12 +16,12 @@ public final class ToDiskExporterBuilder { private SignalSerializer serializer = new NoopSerializer(); - private final Storage storage; + private final Storage storage; private Function, CompletableResultCode> exportFunction = x -> CompletableResultCode.ofFailure(); - ToDiskExporterBuilder(Storage storage) { + ToDiskExporterBuilder(Storage storage) { if (storage == null) { throw new NullPointerException("Storage cannot be null"); } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java index 01e4ab7ce..ae6e6e19d 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java @@ -23,7 +23,7 @@ /** Default storage implementation where items are stored in multiple protobuf files. */ public final class FileSpanStorage implements SignalStorage.Span { - private final Storage storage; + private final Storage storage; private final SignalSerializer serializer; private final Logger logger = Logger.getLogger(FileSpanStorage.class.getName()); private final AtomicBoolean isClosed = new AtomicBoolean(false); @@ -36,10 +36,10 @@ public static FileSpanStorage create( File destinationDir, StorageConfiguration configuration, Clock clock) { FolderManager folderManager = FolderManager.create(destinationDir, configuration, clock); return new FileSpanStorage( - new Storage(folderManager, configuration.isDebugEnabled()), SignalSerializer.ofSpans()); + new Storage<>(folderManager, configuration.isDebugEnabled()), SignalSerializer.ofSpans()); } - FileSpanStorage(Storage storage, SignalSerializer serializer) { + FileSpanStorage(Storage storage, SignalSerializer serializer) { this.storage = storage; this.serializer = serializer; } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java index 00382895c..c082fcb6c 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java @@ -7,7 +7,6 @@ import static java.util.logging.Level.WARNING; -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporterImpl; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.DeserializationException; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; @@ -36,8 +35,7 @@ public final class Storage implements Closeable { public Storage(FolderManager folderManager, boolean debugEnabled) { this.folderManager = folderManager; - this.logger = - DebugLogger.wrap(Logger.getLogger(FromDiskExporterImpl.class.getName()), debugEnabled); + this.logger = DebugLogger.wrap(Logger.getLogger(Storage.class.getName()), debugEnabled); this.debugEnabled = debugEnabled; } diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java index 1283c5cdd..f09489e47 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java @@ -68,7 +68,6 @@ void verifyExportToStorage_success() { assertThat(storedItems).containsExactly(item1, item2, item3); } - @SuppressWarnings("ThrowableNotThrown") @Test void verifyExportToStorage_failure() { SignalStorage.Span storage = mock(); From 87b1162aee3b6219699fdfde9a4491a88456122e Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:51:37 +0200 Subject: [PATCH 06/30] Updating storage tests --- .../buffering/LogRecordToDiskExporter.java | 65 --- .../disk/buffering/MetricToDiskExporter.java | 76 ---- .../disk/buffering/SpanToDiskExporter.java | 65 --- .../config/StorageConfiguration.java | 12 +- .../internal/exporter/NoopSerializer.java | 30 -- .../internal/exporter/ToDiskExporter.java | 64 --- .../exporter/ToDiskExporterBuilder.java | 47 --- .../buffering/internal/storage/Storage.java | 21 +- .../internal/storage/StorageBuilder.java | 65 --- .../internal/storage/StorageIterator.java | 3 +- .../internal/storage/files/ReadableFile.java | 2 + .../LogRecordToDiskExporterTest.java | 64 --- .../buffering/MetricToDiskExporterTest.java | 77 ---- .../buffering/SpanToDiskExporterTest.java | 63 --- .../internal/exporter/ToDiskExporterTest.java | 90 ---- .../internal/storage/StorageTest.java | 384 ++++++++---------- .../buffering/internal/storage/TestData.java | 70 +++- .../storage/files/ReadableFileTest.java | 147 +------ .../testutils/BaseSignalSerializerTest.java | 2 +- 19 files changed, 280 insertions(+), 1067 deletions(-) delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/NoopSerializer.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporter.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterBuilder.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageBuilder.java delete mode 100644 disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporterTest.java delete mode 100644 disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporterTest.java delete mode 100644 disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporterTest.java delete mode 100644 disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterTest.java diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java deleted file mode 100644 index 9af9d9d27..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.ToDiskExporter; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import java.io.IOException; -import java.util.Collection; - -/** - * This class implements a {@link LogRecordExporter} that delegates to an instance of {@code - * ToDiskExporter}. - */ -public class LogRecordToDiskExporter implements LogRecordExporter { - private final ToDiskExporter delegate; - - /** - * Creates a new LogRecordToDiskExporter that will buffer LogRecordData telemetry on disk storage. - * - * @param delegate - The LogRecordExporter to delegate to if disk writing fails. - * @param storage - The Storage instance that specifies how storage is managed. - * @return A new LogRecordToDiskExporter instance. - */ - public static LogRecordToDiskExporter create( - LogRecordExporter delegate, Storage storage) { - ToDiskExporter toDisk = - ToDiskExporter.builder(storage) - .setSerializer(SignalSerializer.ofLogs()) - .setExportFunction(delegate::export) - .build(); - return new LogRecordToDiskExporter(toDisk); - } - - // Visible for testing - LogRecordToDiskExporter(ToDiskExporter delegate) { - this.delegate = delegate; - } - - @Override - public CompletableResultCode export(Collection logs) { - return delegate.export(logs); - } - - @Override - public CompletableResultCode flush() { - return CompletableResultCode.ofSuccess(); - } - - @Override - public CompletableResultCode shutdown() { - try { - delegate.shutdown(); - return CompletableResultCode.ofSuccess(); - } catch (IOException e) { - return CompletableResultCode.ofFailure(); - } - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java deleted file mode 100644 index 5a4047ba8..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.ToDiskExporter; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.metrics.InstrumentType; -import io.opentelemetry.sdk.metrics.data.AggregationTemporality; -import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; -import io.opentelemetry.sdk.metrics.export.MetricExporter; -import java.io.IOException; -import java.util.Collection; - -/** - * This class implements a {@link MetricExporter} that delegates to an instance of {@code - * ToDiskExporter}. - */ -public class MetricToDiskExporter implements MetricExporter { - - private final ToDiskExporter delegate; - private final AggregationTemporalitySelector aggregationTemporalitySelector; - - /** - * Creates a new MetricToDiskExporter that will buffer Metric telemetry on disk storage. - * - * @param delegate - The MetricExporter to delegate to if disk writing fails. - * @param storage - The Storage instance that specifies how storage is managed. - * @return A new MetricToDiskExporter instance. - */ - public static MetricToDiskExporter create(MetricExporter delegate, Storage storage) { - ToDiskExporter toDisk = - ToDiskExporter.builder(storage) - .setSerializer(SignalSerializer.ofMetrics()) - .setExportFunction(delegate::export) - .build(); - return new MetricToDiskExporter(toDisk, delegate); - } - - // VisibleForTesting - MetricToDiskExporter( - ToDiskExporter delegate, AggregationTemporalitySelector selector) { - this.delegate = delegate; - this.aggregationTemporalitySelector = selector; - } - - @Override - public CompletableResultCode export(Collection metrics) { - return delegate.export(metrics); - } - - @Override - public CompletableResultCode flush() { - return CompletableResultCode.ofSuccess(); - } - - @Override - public CompletableResultCode shutdown() { - try { - delegate.shutdown(); - } catch (IOException e) { - return CompletableResultCode.ofFailure(); - } - return CompletableResultCode.ofSuccess(); - } - - @Override - public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { - return aggregationTemporalitySelector.getAggregationTemporality(instrumentType); - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java deleted file mode 100644 index b75a9da76..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.ToDiskExporter; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.io.IOException; -import java.util.Collection; - -/** - * This class implements a SpanExporter that delegates to an instance of {@code - * ToDiskExporter}. - */ -public class SpanToDiskExporter implements SpanExporter { - - private final ToDiskExporter delegate; - - /** - * Creates a new SpanToDiskExporter that will buffer Span telemetry on disk storage. - * - * @param delegate - The SpanExporter to delegate to if disk writing fails. - * @param storage - The Storage instance that specifies how storage is managed. - * @return A new SpanToDiskExporter instance. - */ - public static SpanToDiskExporter create(SpanExporter delegate, Storage storage) { - ToDiskExporter toDisk = - ToDiskExporter.builder(storage) - .setSerializer(SignalSerializer.ofSpans()) - .setExportFunction(delegate::export) - .build(); - return new SpanToDiskExporter(toDisk); - } - - // Visible for testing - SpanToDiskExporter(ToDiskExporter delegate) { - this.delegate = delegate; - } - - @Override - public CompletableResultCode export(Collection spans) { - return delegate.export(spans); - } - - @Override - public CompletableResultCode flush() { - return CompletableResultCode.ofSuccess(); - } - - @Override - public CompletableResultCode shutdown() { - try { - delegate.shutdown(); - } catch (IOException e) { - return CompletableResultCode.ofFailure(); - } - return CompletableResultCode.ofSuccess(); - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java index 4853ee72f..ce12fb249 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java @@ -78,6 +78,16 @@ public abstract static class Builder { public abstract Builder setDebugEnabled(boolean debugEnabled); - public abstract StorageConfiguration build(); + abstract StorageConfiguration autoBuild(); + + public final StorageConfiguration build() { + StorageConfiguration configuration = autoBuild(); + if (configuration.getMinFileAgeForReadMillis() + <= configuration.getMaxFileAgeForWriteMillis()) { + throw new IllegalArgumentException( + "The configured max file age for writing must be lower than the configured min file age for reading"); + } + return configuration; + } } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/NoopSerializer.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/NoopSerializer.java deleted file mode 100644 index 715f538c4..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/NoopSerializer.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.exporter; - -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Collection; - -class NoopSerializer implements SignalSerializer { - - @Override - public NoopSerializer initialize(Collection data) { - return this; - } - - @Override - public void writeBinaryTo(OutputStream output) throws IOException {} - - @Override - public int getBinarySerializedSize() { - return 0; - } - - @Override - public void reset() {} -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporter.java deleted file mode 100644 index 35cd4f444..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporter.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.exporter; - -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.contrib.disk.buffering.internal.utils.DebugLogger; -import io.opentelemetry.sdk.common.CompletableResultCode; -import java.io.IOException; -import java.util.Collection; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class ToDiskExporter { - - private final DebugLogger logger; - private final Storage storage; - private final SignalSerializer serializer; - private final Function, CompletableResultCode> exportFunction; - - ToDiskExporter( - SignalSerializer serializer, - Function, CompletableResultCode> exportFunction, - Storage storage) { - this.serializer = serializer; - this.exportFunction = exportFunction; - this.storage = storage; - this.logger = - DebugLogger.wrap( - Logger.getLogger(ToDiskExporter.class.getName()), storage.isDebugEnabled()); - } - - public static ToDiskExporterBuilder builder(Storage storage) { - return new ToDiskExporterBuilder<>(storage); - } - - public synchronized CompletableResultCode export(Collection data) { - logger.log("Intercepting exporter batch.", Level.FINER); - try { - serializer.initialize(data); - if (storage.write(serializer)) { - return CompletableResultCode.ofSuccess(); - } - logger.log("Could not store batch in disk. Exporting it right away."); - return exportFunction.apply(data); - } catch (IOException e) { - logger.log( - "An unexpected error happened while attempting to write the data in disk. Exporting it right away.", - Level.WARNING, - e); - return exportFunction.apply(data); - } finally { - serializer.reset(); - } - } - - public void shutdown() throws IOException { - storage.close(); - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterBuilder.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterBuilder.java deleted file mode 100644 index 5f5252b2d..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterBuilder.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.exporter; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.sdk.common.CompletableResultCode; -import java.util.Collection; -import java.util.function.Function; - -public final class ToDiskExporterBuilder { - - private SignalSerializer serializer = new NoopSerializer(); - - private final Storage storage; - - private Function, CompletableResultCode> exportFunction = - x -> CompletableResultCode.ofFailure(); - - ToDiskExporterBuilder(Storage storage) { - if (storage == null) { - throw new NullPointerException("Storage cannot be null"); - } - this.storage = storage; - } - - @CanIgnoreReturnValue - public ToDiskExporterBuilder setSerializer(SignalSerializer serializer) { - this.serializer = serializer; - return this; - } - - @CanIgnoreReturnValue - public ToDiskExporterBuilder setExportFunction( - Function, CompletableResultCode> exportFunction) { - this.exportFunction = exportFunction; - return this; - } - - public ToDiskExporter build() { - return new ToDiskExporter<>(serializer, exportFunction, storage); - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java index c082fcb6c..7797e6fe0 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java @@ -15,13 +15,15 @@ import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.ReadableResult; import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.WritableResult; import io.opentelemetry.contrib.disk.buffering.internal.utils.DebugLogger; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; import java.io.Closeable; import java.io.IOException; import java.util.Collection; +import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; +import javax.annotation.Nullable; public final class Storage implements Closeable { private static final int MAX_ATTEMPTS = 3; @@ -39,10 +41,6 @@ public Storage(FolderManager folderManager, boolean debugEnabled) { this.debugEnabled = debugEnabled; } - public static StorageBuilder builder(SignalTypes types) { - return new StorageBuilder(types); - } - public boolean isDebugEnabled() { return debugEnabled; } @@ -95,15 +93,17 @@ public void flush() throws IOException { * * @throws IOException If an unexpected error happens. */ + @Nullable public ReadableResult readNext(SignalDeserializer deserializer) throws IOException { if (activeReadResultAvailable.get()) { throw new IllegalStateException( "You must close any previous ReadableResult before requesting a new one"); } - return doReadAndProcess(deserializer, 1); + return doReadNext(deserializer, 1); } - private ReadableResult doReadAndProcess(SignalDeserializer deserializer, int attemptNumber) + @Nullable + private ReadableResult doReadNext(SignalDeserializer deserializer, int attemptNumber) throws IOException { if (isClosed.get()) { logger.log("Refusing to read from storage after being closed."); @@ -128,7 +128,8 @@ private ReadableResult doReadAndProcess(SignalDeserializer deserializer, i byte[] result = readableFile.readNext(); if (result != null) { try { - Collection items = deserializer.deserialize(result); + List items = deserializer.deserialize(result); + activeReadResultAvailable.set(true); return new FileReadResult(items, readableFile); } catch (DeserializationException e) { // Data corrupted, clear file. @@ -138,7 +139,7 @@ private ReadableResult doReadAndProcess(SignalDeserializer deserializer, i // Retry with new file readableFileRef.set(null); - return doReadAndProcess(deserializer, ++attemptNumber); + return doReadNext(deserializer, ++attemptNumber); } public void clear() throws IOException { @@ -182,7 +183,7 @@ public void delete() throws IOException { } if (itemDeleted.compareAndSet(false, true)) { try { - readableFile.get().removeTopItem(); + Objects.requireNonNull(readableFile.get()).removeTopItem(); } catch (IOException e) { itemDeleted.set(false); throw e; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageBuilder.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageBuilder.java deleted file mode 100644 index d43bc18b2..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageBuilder.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.storage; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; -import io.opentelemetry.sdk.common.Clock; -import java.io.File; -import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class StorageBuilder { - - private static final Logger logger = Logger.getLogger(StorageBuilder.class.getName()); - - private final String folderName; - private StorageConfiguration configuration = StorageConfiguration.getDefault(new File(".")); - private Clock clock = Clock.getDefault(); - - StorageBuilder(SignalTypes types) { - folderName = types.name(); - } - - @CanIgnoreReturnValue - public StorageBuilder setStorageConfiguration(StorageConfiguration configuration) { - validateConfiguration(configuration); - this.configuration = configuration; - return this; - } - - @CanIgnoreReturnValue - public StorageBuilder setStorageClock(Clock clock) { - this.clock = clock; - return this; - } - - public Storage build() throws IOException { - File folder = ensureSubdir(configuration.getRootDir(), folderName); - FolderManager folderManager = new FolderManager(folder, configuration, clock); - if (configuration.isDebugEnabled()) { - logger.log(Level.INFO, "Building storage with configuration => " + configuration); - } - return new Storage(folderManager, configuration.isDebugEnabled()); - } - - private static File ensureSubdir(File rootDir, String child) throws IOException { - File subdir = new File(rootDir, child); - if (subdir.exists() || subdir.mkdirs()) { - return subdir; - } - throw new IOException("Could not create the subdir: '" + child + "' inside: " + rootDir); - } - - private static void validateConfiguration(StorageConfiguration configuration) { - if (configuration.getMinFileAgeForReadMillis() <= configuration.getMaxFileAgeForWriteMillis()) { - throw new IllegalArgumentException( - "The configured max file age for writing must be lower than the configured min file age for reading"); - } - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java index 73ea80ec6..714a01820 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java @@ -37,6 +37,7 @@ public synchronized boolean hasNext() { } @Override + @Nullable public synchronized Collection next() { if (findNext()) { currentResultConsumed = true; @@ -56,7 +57,7 @@ public synchronized void remove() { } } - private boolean findNext() { + private synchronized boolean findNext() { try { if (currentResult != null) { if (!currentResultConsumed) { diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java index 2a10ac73c..ef456d343 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java @@ -17,6 +17,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Reads from a file and updates it in parallel in order to avoid re-reading the same items later. @@ -64,6 +65,7 @@ public ReadableFile( * Reads the next line available in the file and provides it to a {@link Function processing} * which will determine whether to remove the provided line or not. */ + @Nullable public synchronized byte[] readNext() throws IOException { if (isClosed.get()) { return null; diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporterTest.java deleted file mode 100644 index 6409cf067..000000000 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporterTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.ToDiskExporter; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.logs.data.LogRecordData; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class LogRecordToDiskExporterTest { - - @Mock private ToDiskExporter delegate; - - @Test - void delegateShutdown_success() throws IOException { - LogRecordToDiskExporter testClass = new LogRecordToDiskExporter(delegate); - CompletableResultCode result = testClass.shutdown(); - assertThat(result.isSuccess()).isTrue(); - verify(delegate).shutdown(); - } - - @Test - void delegateShutdown_fail() throws IOException { - doThrow(new IOException("boom")).when(delegate).shutdown(); - LogRecordToDiskExporter testClass = new LogRecordToDiskExporter(delegate); - CompletableResultCode result = testClass.shutdown(); - assertThat(result.isSuccess()).isFalse(); - verify(delegate).shutdown(); - } - - @Test - void delegateExport() { - LogRecordData log1 = mock(); - LogRecordData log2 = mock(); - List logRecords = Arrays.asList(log1, log2); - - LogRecordToDiskExporter testClass = new LogRecordToDiskExporter(delegate); - testClass.export(logRecords); - - verify(delegate).export(logRecords); - } - - @Test - void flushReturnsSuccess() { - LogRecordToDiskExporter testClass = new LogRecordToDiskExporter(delegate); - CompletableResultCode result = testClass.flush(); - assertThat(result.isSuccess()).isTrue(); - } -} diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporterTest.java deleted file mode 100644 index 9ba84f67c..000000000 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporterTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import static io.opentelemetry.sdk.metrics.data.AggregationTemporality.CUMULATIVE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.ToDiskExporter; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.metrics.InstrumentType; -import io.opentelemetry.sdk.metrics.data.AggregationTemporality; -import io.opentelemetry.sdk.metrics.data.MetricData; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class MetricToDiskExporterTest { - - @Mock private ToDiskExporter delegate; - - @Test - void delegateShutdown_success() throws IOException { - MetricToDiskExporter testClass = - new MetricToDiskExporter(delegate, MetricToDiskExporterTest::temporalityFn); - CompletableResultCode result = testClass.shutdown(); - assertThat(result.isSuccess()).isTrue(); - verify(delegate).shutdown(); - } - - private static AggregationTemporality temporalityFn(InstrumentType instrumentType) { - return CUMULATIVE; - } - - @Test - void delegateShutdown_fail() throws IOException { - doThrow(new IOException("boom")).when(delegate).shutdown(); - MetricToDiskExporter testClass = - new MetricToDiskExporter(delegate, MetricToDiskExporterTest::temporalityFn); - CompletableResultCode result = testClass.shutdown(); - assertThat(result.isSuccess()).isFalse(); - verify(delegate).shutdown(); - } - - @Test - void delegateExport() { - MetricData metric1 = mock(); - MetricData metric2 = mock(); - List metrics = Arrays.asList(metric1, metric2); - - MetricToDiskExporter testClass = - new MetricToDiskExporter(delegate, MetricToDiskExporterTest::temporalityFn); - - testClass.export(metrics); - - verify(delegate).export(metrics); - } - - @Test - void flushReturnsSuccess() { - MetricToDiskExporter testClass = - new MetricToDiskExporter(delegate, MetricToDiskExporterTest::temporalityFn); - - CompletableResultCode result = testClass.flush(); - assertThat(result.isSuccess()).isTrue(); - } -} diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporterTest.java deleted file mode 100644 index 96dcfcaa9..000000000 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporterTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import io.opentelemetry.contrib.disk.buffering.internal.exporter.ToDiskExporter; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.trace.data.SpanData; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class SpanToDiskExporterTest { - @Mock private ToDiskExporter delegate; - - @Test - void delegateShutdown_success() throws IOException { - SpanToDiskExporter testClass = new SpanToDiskExporter(delegate); - CompletableResultCode result = testClass.shutdown(); - assertThat(result.isSuccess()).isTrue(); - verify(delegate).shutdown(); - } - - @Test - void delegateShutdown_fail() throws IOException { - doThrow(new IOException("boom")).when(delegate).shutdown(); - SpanToDiskExporter testClass = new SpanToDiskExporter(delegate); - CompletableResultCode result = testClass.shutdown(); - assertThat(result.isSuccess()).isFalse(); - verify(delegate).shutdown(); - } - - @Test - void delegateExport() { - SpanData span1 = mock(); - SpanData span2 = mock(); - List spans = Arrays.asList(span1, span2); - - SpanToDiskExporter testClass = new SpanToDiskExporter(delegate); - testClass.export(spans); - - verify(delegate).export(spans); - } - - @Test - void flushReturnsSuccess() { - SpanToDiskExporter testClass = new SpanToDiskExporter(delegate); - CompletableResultCode result = testClass.flush(); - assertThat(result.isSuccess()).isTrue(); - } -} diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterTest.java deleted file mode 100644 index 0a98061ac..000000000 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporterTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.exporter; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.sdk.common.CompletableResultCode; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -class ToDiskExporterTest { - - private final List records = Arrays.asList("one", "two", "three"); - - @Mock private SignalSerializer serializer; - - @Mock private Storage storage; - private ToDiskExporter toDiskExporter; - private Function, CompletableResultCode> exportFn; - private Collection exportedFnSeen; - private AtomicReference exportFnResultToReturn; - - @BeforeEach - void setup() { - exportedFnSeen = null; - exportFnResultToReturn = new AtomicReference<>(null); - exportFn = - (Collection x) -> { - exportedFnSeen = x; - return exportFnResultToReturn.get(); - }; - toDiskExporter = new ToDiskExporter<>(serializer, exportFn, storage); - } - - @Test - void whenWritingSucceedsOnExport_returnSuccessfulResultCode() throws Exception { - when(storage.write(any())).thenReturn(true); - CompletableResultCode completableResultCode = toDiskExporter.export(records); - assertThat(completableResultCode.isSuccess()).isTrue(); - verify(storage).write(any()); - assertThat(exportedFnSeen).isNull(); - } - - @Test - void whenWritingFailsOnExport_doExportRightAway() throws Exception { - when(storage.write(any())).thenReturn(false); - exportFnResultToReturn.set(CompletableResultCode.ofSuccess()); - - CompletableResultCode completableResultCode = toDiskExporter.export(records); - - assertThat(completableResultCode.isSuccess()).isTrue(); - assertThat(exportedFnSeen).isEqualTo(records); - } - - @Test - void whenExceptionInWrite_doExportRightAway() throws Exception { - when(storage.write(any())).thenThrow(new IOException("boom")); - exportFnResultToReturn.set(CompletableResultCode.ofFailure()); - - CompletableResultCode completableResultCode = toDiskExporter.export(records); - - assertThat(completableResultCode.isSuccess()).isFalse(); - assertThat(exportedFnSeen).isEqualTo(records); - } - - @Test - void shutdownClosesStorage() throws Exception { - toDiskExporter.export(records); - toDiskExporter.shutdown(); - verify(storage).close(); - } -} diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageTest.java index d96b9a1bc..4379b20bc 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageTest.java @@ -5,48 +5,52 @@ package io.opentelemetry.contrib.disk.buffering.internal.storage; -import static io.opentelemetry.contrib.disk.buffering.internal.storage.responses.ReadableResult.TRY_LATER; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.ByteArraySerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.files.ReadableFile; -import io.opentelemetry.contrib.disk.buffering.internal.storage.files.WritableFile; -import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.ProcessResult; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.FIRST_LOG_RECORD; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.MAX_FILE_AGE_FOR_READ_MILLIS; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.MAX_FILE_AGE_FOR_WRITE_MILLIS; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.MIN_FILE_AGE_FOR_READ_MILLIS; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.SECOND_LOG_RECORD; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.THIRD_LOG_RECORD; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.getConfiguration; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; +import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.ReadableResult; -import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.WritableResult; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.logs.data.LogRecordData; import java.io.File; import java.io.IOException; -import java.util.function.Function; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; -@SuppressWarnings("unchecked") class StorageTest { + @TempDir private File destinationDir; private FolderManager folderManager; - private Storage storage; - private Function processing; - private ReadableFile readableFile; - private WritableFile writableFile; + private Storage storage; + private SignalSerializer serializer; + private AtomicLong currentTimeMillis; + private static final SignalDeserializer DESERIALIZER = SignalDeserializer.ofLogs(); @BeforeEach - void setUp() throws IOException { - folderManager = mock(); - readableFile = mock(); - writableFile = createWritableFile(); - processing = mock(); - when(readableFile.readAndProcess(processing)).thenReturn(ReadableResult.SUCCEEDED); - storage = new Storage(folderManager, true); + void setUp() { + currentTimeMillis = new AtomicLong(0); + serializer = SignalSerializer.ofLogs(); + folderManager = + FolderManager.create(destinationDir, getConfiguration(destinationDir), new TestClock()); + storage = new Storage<>(folderManager, true); } @AfterEach @@ -55,203 +59,167 @@ void tearDown() throws IOException { } @Test - void whenReadingAndProcessingSuccessfully_returnSuccess() throws IOException { - when(folderManager.getReadableFile()).thenReturn(readableFile); + void writeAndRead() throws IOException { + assertThat(write(Arrays.asList(FIRST_LOG_RECORD, SECOND_LOG_RECORD))).isTrue(); + assertThat(write(Collections.singletonList(THIRD_LOG_RECORD))).isTrue(); + assertThat(destinationDir.list()).hasSize(1); + forwardToReadTime(); - assertEquals(ReadableResult.SUCCEEDED, storage.readAndProcess(processing)); + ReadableResult readResult = storage.readNext(DESERIALIZER); + assertNotNull(readResult); + assertThat(readResult.getContent()).containsExactly(FIRST_LOG_RECORD, SECOND_LOG_RECORD); + assertThat(destinationDir.list()).hasSize(1); - verify(readableFile).readAndProcess(processing); - } - - @Test - void whenReadableFileProcessingFails_returnTryLater() throws IOException { - when(folderManager.getReadableFile()).thenReturn(readableFile); - when(readableFile.readAndProcess(processing)).thenReturn(TRY_LATER); + // Delete result and read again + readResult.delete(); + readResult.close(); + ReadableResult readResult2 = storage.readNext(DESERIALIZER); + assertNotNull(readResult2); + assertThat(readResult2.getContent()).containsExactly(THIRD_LOG_RECORD); + assertThat(destinationDir.list()).hasSize(1); - assertEquals(TRY_LATER, storage.readAndProcess(processing)); + // Read again without closing previous result + try { + storage.readNext(DESERIALIZER); + fail(); + } catch (IllegalStateException e) { + assertThat(e) + .hasMessage("You must close any previous ReadableResult before requesting a new one"); + } - verify(readableFile).readAndProcess(processing); + // Read again when no more data is available (delete file) + readResult2.close(); + assertNull(storage.readNext(DESERIALIZER)); + assertThat(destinationDir.list()).isEmpty(); } @Test - void whenReadingMultipleTimes_reuseReader() throws IOException { - ReadableFile anotherReadable = mock(); - when(folderManager.getReadableFile()).thenReturn(readableFile).thenReturn(anotherReadable); - - assertEquals(ReadableResult.SUCCEEDED, storage.readAndProcess(processing)); - assertEquals(ReadableResult.SUCCEEDED, storage.readAndProcess(processing)); - - verify(readableFile, times(2)).readAndProcess(processing); - verify(folderManager, times(1)).getReadableFile(); - verifyNoInteractions(anotherReadable); - } - - @Test - void whenWritingMultipleTimes_reuseWriter() throws IOException { - ByteArraySerializer data = new ByteArraySerializer(new byte[1]); - WritableFile anotherWriter = createWritableFile(); - when(folderManager.createWritableFile()).thenReturn(writableFile).thenReturn(anotherWriter); - - storage.write(data); - storage.write(data); - - verify(writableFile, times(2)).append(data); - verify(folderManager, times(1)).createWritableFile(); - verifyNoInteractions(anotherWriter); - } - - @Test - void whenAttemptingToReadAfterClosed_returnFailed() throws IOException { + void interactionAfterClosed() throws IOException { + assertThat(write(Arrays.asList(FIRST_LOG_RECORD, SECOND_LOG_RECORD))).isTrue(); storage.close(); - assertEquals(ReadableResult.FAILED, storage.readAndProcess(processing)); - } + assertThat(destinationDir.list()).hasSize(1); + forwardToReadTime(); - @Test - void whenAttemptingToWriteAfterClosed_returnFalse() throws IOException { - storage.close(); - assertFalse(storage.write(new ByteArraySerializer(new byte[1]))); - } + // Reading + assertNull(storage.readNext(DESERIALIZER)); - @Test - void whenNoFileAvailableForReading_returnFailed() throws IOException { - assertEquals(ReadableResult.FAILED, storage.readAndProcess(processing)); + // Writing + assertThat(write(Collections.singletonList(THIRD_LOG_RECORD))).isFalse(); } @Test void whenTheReadTimeExpires_lookForNewFileToRead() throws IOException { - when(folderManager.getReadableFile()).thenReturn(readableFile).thenReturn(null); - when(readableFile.readAndProcess(processing)).thenReturn(ReadableResult.FAILED); - - storage.readAndProcess(processing); - - verify(folderManager, times(2)).getReadableFile(); - } - - @Test - void whenNoMoreLinesToRead_lookForNewFileToRead() throws IOException { - when(folderManager.getReadableFile()).thenReturn(readableFile).thenReturn(null); - when(readableFile.readAndProcess(processing)).thenReturn(ReadableResult.FAILED); - - storage.readAndProcess(processing); - - verify(folderManager, times(2)).getReadableFile(); - } - - @Test - void whenResourceClosed_lookForNewFileToRead() throws IOException { - when(folderManager.getReadableFile()).thenReturn(readableFile).thenReturn(null); - when(readableFile.readAndProcess(processing)).thenReturn(ReadableResult.FAILED); - - storage.readAndProcess(processing); - - verify(folderManager, times(2)).getReadableFile(); - } - - @Test - void whenEveryNewFileFoundCannotBeRead_returnContentNotAvailable() throws IOException { - when(folderManager.getReadableFile()).thenReturn(readableFile); - when(readableFile.readAndProcess(processing)).thenReturn(ReadableResult.FAILED); - - assertEquals(ReadableResult.FAILED, storage.readAndProcess(processing)); + long firstFileWriteTime = 1000; + long secondFileWriteTime = firstFileWriteTime + MAX_FILE_AGE_FOR_WRITE_MILLIS + 1; + currentTimeMillis.set(firstFileWriteTime); + assertThat(write(Arrays.asList(FIRST_LOG_RECORD, SECOND_LOG_RECORD))).isTrue(); - verify(folderManager, times(3)).getReadableFile(); - } - - @Test - void appendDataToFile() throws IOException { - when(folderManager.createWritableFile()).thenReturn(writableFile); - ByteArraySerializer data = new ByteArraySerializer(new byte[1]); + // Forward past first file write time + currentTimeMillis.set(secondFileWriteTime); + assertThat(write(Collections.singletonList(THIRD_LOG_RECORD))).isTrue(); + assertThat(destinationDir.list()) + .containsExactlyInAnyOrder( + String.valueOf(firstFileWriteTime), String.valueOf(secondFileWriteTime)); - storage.write(data); - - verify(writableFile).append(data); - } - - @Test - void whenWritingTimeoutHappens_retryWithNewFile() throws IOException { - ByteArraySerializer data = new ByteArraySerializer(new byte[1]); - WritableFile workingWritableFile = createWritableFile(); - when(folderManager.createWritableFile()) - .thenReturn(writableFile) - .thenReturn(workingWritableFile); - when(writableFile.append(data)).thenReturn(WritableResult.FAILED); + // Forward past first time read + currentTimeMillis.set(firstFileWriteTime + MAX_FILE_AGE_FOR_READ_MILLIS + 1); - storage.write(data); + // Read + ReadableResult result = storage.readNext(DESERIALIZER); + assertNotNull(result); + assertThat(result.getContent()).containsExactly(THIRD_LOG_RECORD); + assertThat(destinationDir.list()) + .containsExactlyInAnyOrder( + String.valueOf(firstFileWriteTime), String.valueOf(secondFileWriteTime)); - verify(folderManager, times(2)).createWritableFile(); + // Purge expired files on write + currentTimeMillis.set(50000); + assertThat(write(Collections.singletonList(FIRST_LOG_RECORD))).isTrue(); + assertThat(destinationDir.list()).containsExactly("50000"); } @Test - void whenThereIsNoSpaceAvailableForWriting_retryWithNewFile() throws IOException { - ByteArraySerializer data = new ByteArraySerializer(new byte[1]); - WritableFile workingWritableFile = createWritableFile(); - when(folderManager.createWritableFile()) - .thenReturn(writableFile) - .thenReturn(workingWritableFile); - when(writableFile.append(data)).thenReturn(WritableResult.FAILED); - - storage.write(data); - - verify(folderManager, times(2)).createWritableFile(); - } - - @Test - void whenWritingResourceIsClosed_retryWithNewFile() throws IOException { - ByteArraySerializer data = new ByteArraySerializer(new byte[1]); - WritableFile workingWritableFile = createWritableFile(); - when(folderManager.createWritableFile()) - .thenReturn(writableFile) - .thenReturn(workingWritableFile); - when(writableFile.append(data)).thenReturn(WritableResult.FAILED); - - storage.write(data); - - verify(folderManager, times(2)).createWritableFile(); - } - - @Test - void whenEveryAttemptToWriteFails_returnFalse() throws IOException { - ByteArraySerializer data = new ByteArraySerializer(new byte[1]); - when(folderManager.createWritableFile()).thenReturn(writableFile); - when(writableFile.append(data)).thenReturn(WritableResult.FAILED); - - assertFalse(storage.write(data)); - - verify(folderManager, times(3)).createWritableFile(); - } - - @Test - void whenClosing_closeWriterAndReaderIfNotNull() throws IOException { - when(folderManager.createWritableFile()).thenReturn(writableFile); - when(folderManager.getReadableFile()).thenReturn(readableFile); - storage.write(new ByteArraySerializer(new byte[1])); - storage.readAndProcess(processing); - - storage.close(); - - verify(writableFile).close(); - verify(readableFile).close(); - } - - @Test - void whenMinFileReadIsNotGraterThanMaxFileWrite_throwException() { - StorageConfiguration invalidConfig = - StorageConfiguration.builder() - .setMaxFileAgeForWriteMillis(2) - .setMinFileAgeForReadMillis(1) - .setRootDir(new File(".")) - .build(); - - assertThatThrownBy( - () -> Storage.builder(SignalTypes.logs).setStorageConfiguration(invalidConfig)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "The configured max file age for writing must be lower than the configured min file age for reading"); - } - - private static WritableFile createWritableFile() throws IOException { - WritableFile mock = mock(); - when(mock.append(any())).thenReturn(WritableResult.SUCCEEDED); - return mock; + void whenNoMoreLinesToRead_lookForNewFileToRead() throws IOException { + long firstFileWriteTime = 1000; + long secondFileWriteTime = firstFileWriteTime + MAX_FILE_AGE_FOR_WRITE_MILLIS + 1; + currentTimeMillis.set(firstFileWriteTime); + assertThat(write(Arrays.asList(FIRST_LOG_RECORD, SECOND_LOG_RECORD))).isTrue(); + + // Forward past first file write time + currentTimeMillis.set(secondFileWriteTime); + assertThat(write(Collections.singletonList(THIRD_LOG_RECORD))).isTrue(); + assertThat(destinationDir.list()) + .containsExactlyInAnyOrder( + String.valueOf(firstFileWriteTime), String.valueOf(secondFileWriteTime)); + + // Forward to all files read time + currentTimeMillis.set(secondFileWriteTime + MIN_FILE_AGE_FOR_READ_MILLIS); + + // Read + ReadableResult result = storage.readNext(DESERIALIZER); + assertNotNull(result); + assertThat(result.getContent()).containsExactly(FIRST_LOG_RECORD, SECOND_LOG_RECORD); + assertThat(destinationDir.list()) + .containsExactlyInAnyOrder( + String.valueOf(firstFileWriteTime), String.valueOf(secondFileWriteTime)); + result.delete(); + result.close(); + + // Read again + ReadableResult result2 = storage.readNext(DESERIALIZER); + assertNotNull(result2); + assertThat(result2.getContent()).containsExactly(THIRD_LOG_RECORD); + assertThat(destinationDir.list()).containsExactly(String.valueOf(secondFileWriteTime)); + result2.close(); + } + + @Test + void deleteFilesWithCorruptedData() throws IOException { + // Add files with invalid data + Files.write( + new File(destinationDir, "1000").toPath(), "random data".getBytes(StandardCharsets.UTF_8)); + Files.write( + new File(destinationDir, "2000").toPath(), "random data".getBytes(StandardCharsets.UTF_8)); + Files.write( + new File(destinationDir, "3000").toPath(), "random data".getBytes(StandardCharsets.UTF_8)); + Files.write( + new File(destinationDir, "4000").toPath(), "random data".getBytes(StandardCharsets.UTF_8)); + + // Set time ready to read all files + currentTimeMillis.set(4000 + MIN_FILE_AGE_FOR_READ_MILLIS); + + // Read + assertNull(storage.readNext(DESERIALIZER)); + assertThat(destinationDir.list()).containsExactly("4000"); // it tries 3 times max per call. + } + + private void forwardToReadTime() { + forwardCurrentTimeByMillis(MIN_FILE_AGE_FOR_READ_MILLIS); + } + + private void forwardCurrentTimeByMillis(long millis) { + currentTimeMillis.set(currentTimeMillis.get() + millis); + } + + private boolean write(Collection items) throws IOException { + serializer.initialize(items); + try { + return storage.write(serializer); + } finally { + serializer.reset(); + } + } + + private class TestClock implements Clock { + + @Override + public long now() { + return TimeUnit.MILLISECONDS.toNanos(currentTimeMillis.get()); + } + + @Override + public long nanoTime() { + return 0; + } } } diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java index 75d86726e..0376ce06b 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java @@ -5,27 +5,71 @@ package io.opentelemetry.contrib.disk.buffering.internal.storage; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.logs.Severity; import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; -import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.logs.models.LogRecordDataImpl; +import io.opentelemetry.sdk.logs.data.LogRecordData; import java.io.File; -import java.io.IOException; public final class TestData { + public static final LogRecordData FIRST_LOG_RECORD = + LogRecordDataImpl.builder() + .setResource(io.opentelemetry.contrib.disk.buffering.testutils.TestData.RESOURCE_FULL) + .setSpanContext(io.opentelemetry.contrib.disk.buffering.testutils.TestData.SPAN_CONTEXT) + .setInstrumentationScopeInfo( + io.opentelemetry.contrib.disk.buffering.testutils.TestData + .INSTRUMENTATION_SCOPE_INFO_FULL) + .setAttributes(io.opentelemetry.contrib.disk.buffering.testutils.TestData.ATTRIBUTES) + .setBodyValue(Value.of("First log body")) + .setSeverity(Severity.DEBUG) + .setSeverityText("Log severity text") + .setTimestampEpochNanos(100L) + .setObservedTimestampEpochNanos(200L) + .setTotalAttributeCount(3) + .setEventName("") + .build(); + + public static final LogRecordData SECOND_LOG_RECORD = + LogRecordDataImpl.builder() + .setResource(io.opentelemetry.contrib.disk.buffering.testutils.TestData.RESOURCE_FULL) + .setSpanContext(io.opentelemetry.contrib.disk.buffering.testutils.TestData.SPAN_CONTEXT) + .setInstrumentationScopeInfo( + io.opentelemetry.contrib.disk.buffering.testutils.TestData + .INSTRUMENTATION_SCOPE_INFO_FULL) + .setAttributes(io.opentelemetry.contrib.disk.buffering.testutils.TestData.ATTRIBUTES) + .setBodyValue(Value.of("Second log body")) + .setSeverity(Severity.DEBUG) + .setSeverityText("Log severity text") + .setTimestampEpochNanos(100L) + .setObservedTimestampEpochNanos(200L) + .setTotalAttributeCount(3) + .setEventName("event") + .build(); + + public static final LogRecordData THIRD_LOG_RECORD = + LogRecordDataImpl.builder() + .setResource(io.opentelemetry.contrib.disk.buffering.testutils.TestData.RESOURCE_FULL) + .setSpanContext(io.opentelemetry.contrib.disk.buffering.testutils.TestData.SPAN_CONTEXT) + .setInstrumentationScopeInfo( + io.opentelemetry.contrib.disk.buffering.testutils.TestData + .INSTRUMENTATION_SCOPE_INFO_FULL) + .setAttributes(io.opentelemetry.contrib.disk.buffering.testutils.TestData.ATTRIBUTES) + .setBodyValue(Value.of("Third log body")) + .setSeverity(Severity.DEBUG) + .setSeverityText("Log severity text") + .setTimestampEpochNanos(100L) + .setObservedTimestampEpochNanos(200L) + .setTotalAttributeCount(3) + .setEventName("") + .build(); + public static final long MAX_FILE_AGE_FOR_WRITE_MILLIS = 1000; public static final long MIN_FILE_AGE_FOR_READ_MILLIS = MAX_FILE_AGE_FOR_WRITE_MILLIS + 500; public static final long MAX_FILE_AGE_FOR_READ_MILLIS = 10_000; - public static final int MAX_FILE_SIZE = 100; - public static final int MAX_FOLDER_SIZE = 300; - - public static Storage getStorage(File rootDir, SignalTypes types, Clock clock) - throws IOException { - return Storage.builder(types) - .setStorageConfiguration(getConfiguration(rootDir)) - .setStorageClock(clock) - .build(); - } + public static final int MAX_FILE_SIZE = 2000; + public static final int MAX_FOLDER_SIZE = 6000; public static StorageConfiguration getConfiguration(File rootDir) { return StorageConfiguration.builder() diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java index a9d0eb5da..d585b4f7e 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java @@ -5,25 +5,23 @@ package io.opentelemetry.contrib.disk.buffering.internal.storage.files; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.FIRST_LOG_RECORD; import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.MAX_FILE_AGE_FOR_READ_MILLIS; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.SECOND_LOG_RECORD; +import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.THIRD_LOG_RECORD; import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.getConfiguration; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import io.opentelemetry.api.common.Value; -import io.opentelemetry.api.logs.Severity; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.DeserializationException; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.logs.models.LogRecordDataImpl; import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.ProcessResult; -import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.ReadableResult; -import io.opentelemetry.contrib.disk.buffering.testutils.TestData; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.logs.data.LogRecordData; import java.io.File; @@ -47,50 +45,6 @@ class ReadableFileTest { private static final long CREATED_TIME_MILLIS = 1000L; private static final SignalSerializer SERIALIZER = SignalSerializer.ofLogs(); private static final SignalDeserializer DESERIALIZER = SignalDeserializer.ofLogs(); - private static final LogRecordData FIRST_LOG_RECORD = - LogRecordDataImpl.builder() - .setResource(TestData.RESOURCE_FULL) - .setSpanContext(TestData.SPAN_CONTEXT) - .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_FULL) - .setAttributes(TestData.ATTRIBUTES) - .setBodyValue(Value.of("First log body")) - .setSeverity(Severity.DEBUG) - .setSeverityText("Log severity text") - .setTimestampEpochNanos(100L) - .setObservedTimestampEpochNanos(200L) - .setTotalAttributeCount(3) - .setEventName("") - .build(); - - private static final LogRecordData SECOND_LOG_RECORD = - LogRecordDataImpl.builder() - .setResource(TestData.RESOURCE_FULL) - .setSpanContext(TestData.SPAN_CONTEXT) - .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_FULL) - .setAttributes(TestData.ATTRIBUTES) - .setBodyValue(Value.of("Second log body")) - .setSeverity(Severity.DEBUG) - .setSeverityText("Log severity text") - .setTimestampEpochNanos(100L) - .setObservedTimestampEpochNanos(200L) - .setTotalAttributeCount(3) - .setEventName("event") - .build(); - - private static final LogRecordData THIRD_LOG_RECORD = - LogRecordDataImpl.builder() - .setResource(TestData.RESOURCE_FULL) - .setSpanContext(TestData.SPAN_CONTEXT) - .setInstrumentationScopeInfo(TestData.INSTRUMENTATION_SCOPE_INFO_FULL) - .setAttributes(TestData.ATTRIBUTES) - .setBodyValue(Value.of("Third log body")) - .setSeverity(Severity.DEBUG) - .setSeverityText("Log severity text") - .setTimestampEpochNanos(100L) - .setObservedTimestampEpochNanos(200L) - .setTotalAttributeCount(3) - .setEventName("") - .build(); @BeforeEach void setUp() throws IOException { @@ -117,12 +71,9 @@ private static void addFileContents(File source) throws IOException { } @Test - void readSingleItemAndRemoveIt() throws IOException { - readableFile.readAndProcess( - bytes -> { - assertEquals(FIRST_LOG_RECORD, deserialize(bytes)); - return ProcessResult.SUCCEEDED; - }); + void readAndRemoveItems() throws IOException { + assertEquals(FIRST_LOG_RECORD, deserialize(readableFile.readNext())); + readableFile.removeTopItem(); List logs = getRemainingDataAndClose(readableFile); @@ -132,39 +83,8 @@ void readSingleItemAndRemoveIt() throws IOException { } @Test - void whenProcessingSucceeds_returnSuccessStatus() throws IOException { - assertEquals( - ReadableResult.SUCCEEDED, readableFile.readAndProcess(bytes -> ProcessResult.SUCCEEDED)); - } - - @Test - void whenProcessingFails_returnTryLaterStatus() throws IOException { - assertEquals( - ReadableResult.TRY_LATER, readableFile.readAndProcess(bytes -> ProcessResult.TRY_LATER)); - } - - @Test - void readMultipleLinesAndRemoveThem() throws IOException { - readableFile.readAndProcess( - bytes -> { - assertDeserializedData(FIRST_LOG_RECORD, bytes); - return ProcessResult.SUCCEEDED; - }); - readableFile.readAndProcess( - bytes -> { - assertDeserializedData(SECOND_LOG_RECORD, bytes); - return ProcessResult.SUCCEEDED; - }); - - List logs = getRemainingDataAndClose(readableFile); - - assertEquals(1, logs.size()); - assertEquals(THIRD_LOG_RECORD, logs.get(0)); - } - - @Test - void whenConsumerReturnsFalse_doNotRemoveLineFromSource() throws IOException { - readableFile.readAndProcess(bytes -> ProcessResult.TRY_LATER); + void readWithoutRemoving() throws IOException { + readableFile.readNext(); List logs = getRemainingDataAndClose(readableFile); @@ -179,15 +99,6 @@ void whenReadingLastLine_deleteOriginalFile_and_close() throws IOException { assertTrue(readableFile.isClosed()); } - @Test - void whenTheFileContentIsInvalid_deleteOriginalFile_and_close() throws IOException { - assertEquals( - ReadableResult.FAILED, readableFile.readAndProcess(bytes -> ProcessResult.CONTENT_INVALID)); - - assertFalse(source.exists()); - assertTrue(readableFile.isClosed()); - } - @Test void whenNoMoreLinesAvailableToRead_deleteOriginalFile_close_and_returnNoContentStatus() throws IOException { @@ -199,56 +110,38 @@ void whenNoMoreLinesAvailableToRead_deleteOriginalFile_close_and_returnNoContent ReadableFile emptyReadableFile = new ReadableFile(emptyFile, CREATED_TIME_MILLIS, clock, getConfiguration(dir)); - assertEquals( - ReadableResult.FAILED, emptyReadableFile.readAndProcess(bytes -> ProcessResult.SUCCEEDED)); + assertNull(emptyReadableFile.readNext()); assertTrue(emptyReadableFile.isClosed()); assertFalse(emptyFile.exists()); } @Test - void - whenReadingAfterTheConfiguredReadingTimeExpired_deleteOriginalFile_close_and_returnFileExpiredException() - throws IOException { - readableFile.readAndProcess(bytes -> ProcessResult.SUCCEEDED); + void whenReadingAfterTheConfiguredReadingTimeExpired_deleteFile_and_close() throws IOException { when(clock.now()) .thenReturn(MILLISECONDS.toNanos(CREATED_TIME_MILLIS + MAX_FILE_AGE_FOR_READ_MILLIS)); - assertEquals( - ReadableResult.FAILED, readableFile.readAndProcess(bytes -> ProcessResult.SUCCEEDED)); + assertNull(readableFile.readNext()); assertTrue(readableFile.isClosed()); + assertFalse(source.exists()); } @Test - void whenReadingAfterClosed_returnFailedStatus() throws IOException { - readableFile.readAndProcess(bytes -> ProcessResult.SUCCEEDED); + void whenReadingAfterClosed_returnNull() throws IOException { readableFile.close(); - assertEquals( - ReadableResult.FAILED, readableFile.readAndProcess(bytes -> ProcessResult.SUCCEEDED)); - } - - private static void assertDeserializedData(LogRecordData expected, byte[] bytes) { - try { - List deserialized = DESERIALIZER.deserialize(bytes); - assertEquals(expected, deserialized.get(0)); - } catch (DeserializationException e) { - throw new RuntimeException(e); - } + assertNull(readableFile.readNext()); } private static List getRemainingDataAndClose(ReadableFile readableFile) throws IOException { List result = new ArrayList<>(); - ReadableResult readableResult = ReadableResult.SUCCEEDED; - while (readableResult == ReadableResult.SUCCEEDED) { - readableResult = - readableFile.readAndProcess( - bytes -> { - result.add(deserialize(bytes)); - return ProcessResult.SUCCEEDED; - }); + byte[] bytes = readableFile.readNext(); + while (bytes != null) { + result.add(deserialize(bytes)); + readableFile.removeTopItem(); + bytes = readableFile.readNext(); } readableFile.close(); diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/testutils/BaseSignalSerializerTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/testutils/BaseSignalSerializerTest.java index 69186f812..d113d9f22 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/testutils/BaseSignalSerializerTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/testutils/BaseSignalSerializerTest.java @@ -37,7 +37,7 @@ protected byte[] serialize(SIGNAL_SDK_ITEM... items) { protected List deserialize(byte[] source) { try (ByteArrayInputStream in = new ByteArrayInputStream(source)) { StreamReader streamReader = DelimitedProtoStreamReader.Factory.getInstance().create(in); - return getDeserializer().deserialize(Objects.requireNonNull(streamReader.readNext()).content); + return getDeserializer().deserialize(Objects.requireNonNull(streamReader.readNext())); } catch (IOException e) { throw new RuntimeException(e); } From cf51489e39a196ad624b38eb13b6d6dfca9e7d32 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:58:06 +0200 Subject: [PATCH 07/30] Updating tests --- .../internal/storage/files/ReadableFileTest.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java index d585b4f7e..cdd202280 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java @@ -82,15 +82,6 @@ void readAndRemoveItems() throws IOException { assertEquals(THIRD_LOG_RECORD, logs.get(1)); } - @Test - void readWithoutRemoving() throws IOException { - readableFile.readNext(); - - List logs = getRemainingDataAndClose(readableFile); - - assertEquals(3, logs.size()); - } - @Test void whenReadingLastLine_deleteOriginalFile_and_close() throws IOException { getRemainingDataAndClose(readableFile); @@ -117,14 +108,12 @@ void whenNoMoreLinesAvailableToRead_deleteOriginalFile_close_and_returnNoContent } @Test - void whenReadingAfterTheConfiguredReadingTimeExpired_deleteFile_and_close() throws IOException { + void whenReadingAfterTheConfiguredReadingTimeExpired_close() throws IOException { when(clock.now()) .thenReturn(MILLISECONDS.toNanos(CREATED_TIME_MILLIS + MAX_FILE_AGE_FOR_READ_MILLIS)); assertNull(readableFile.readNext()); - assertTrue(readableFile.isClosed()); - assertFalse(source.exists()); } @Test From a8e543d433869752ac223534dae375695ff2cd86 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:49:35 +0200 Subject: [PATCH 08/30] Removing unused types --- .../config/StorageConfiguration.java | 18 ++------- .../LogRecordDataDeserializer.java | 6 --- .../deserializers/MetricDataDeserializer.java | 6 --- .../deserializers/SignalDeserializer.java | 5 --- .../deserializers/SpanDataDeserializer.java | 6 --- .../internal/storage/FileSpanStorage.java | 3 +- .../buffering/internal/storage/Storage.java | 32 ++++++--------- .../internal/storage/StorageIterator.java | 4 +- .../buffering/internal/utils/DebugLogger.java | 39 ------------------- .../buffering/internal/utils/SignalTypes.java | 12 ------ .../internal/storage/FolderManagerTest.java | 2 +- .../internal/storage/StorageTest.java | 5 +-- .../buffering/internal/storage/TestData.java | 4 +- .../storage/files/ReadableFileTest.java | 4 +- .../storage/files/WritableFileTest.java | 2 +- 15 files changed, 25 insertions(+), 123 deletions(-) delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/utils/DebugLogger.java delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/utils/SignalTypes.java diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java index ce12fb249..126e4cc6d 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java @@ -6,19 +6,12 @@ package io.opentelemetry.contrib.disk.buffering.config; import com.google.auto.value.AutoValue; -import java.io.File; import java.util.concurrent.TimeUnit; /** Defines how the storage should be managed. */ @AutoValue public abstract class StorageConfiguration { - /** The root storage location for buffered telemetry. */ - public abstract File getRootDir(); - - /** Returns true if the storage has been configured with debug verbosity enabled. */ - public abstract boolean isDebugEnabled(); - /** The max amount of time a file can receive new data. */ public abstract long getMaxFileAgeForWriteMillis(); @@ -48,8 +41,8 @@ public abstract class StorageConfiguration { */ public abstract int getMaxFolderSize(); - public static StorageConfiguration getDefault(File rootDir) { - return builder().setRootDir(rootDir).build(); + public static StorageConfiguration getDefault() { + return builder().build(); } public static Builder builder() { @@ -58,8 +51,7 @@ public static Builder builder() { .setMaxFolderSize(10 * 1024 * 1024) // 10MB .setMaxFileAgeForWriteMillis(TimeUnit.SECONDS.toMillis(30)) .setMinFileAgeForReadMillis(TimeUnit.SECONDS.toMillis(33)) - .setMaxFileAgeForReadMillis(TimeUnit.HOURS.toMillis(18)) - .setDebugEnabled(false); + .setMaxFileAgeForReadMillis(TimeUnit.HOURS.toMillis(18)); } @AutoValue.Builder @@ -74,10 +66,6 @@ public abstract static class Builder { public abstract Builder setMaxFolderSize(int value); - public abstract Builder setRootDir(File rootDir); - - public abstract Builder setDebugEnabled(boolean debugEnabled); - abstract StorageConfiguration autoBuild(); public final StorageConfiguration build() { diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/LogRecordDataDeserializer.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/LogRecordDataDeserializer.java index cbbb4a0ad..19058fe2c 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/LogRecordDataDeserializer.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/LogRecordDataDeserializer.java @@ -6,7 +6,6 @@ package io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers; import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.logs.ProtoLogsDataMapper; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest; import io.opentelemetry.sdk.logs.data.LogRecordData; import java.io.IOException; @@ -30,9 +29,4 @@ public List deserialize(byte[] source) throws DeserializationExce throw new DeserializationException(e); } } - - @Override - public String signalType() { - return SignalTypes.logs.name(); - } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/MetricDataDeserializer.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/MetricDataDeserializer.java index d6410d4e7..eaa1463c0 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/MetricDataDeserializer.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/MetricDataDeserializer.java @@ -6,7 +6,6 @@ package io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers; import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.metrics.ProtoMetricsDataMapper; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; import io.opentelemetry.sdk.metrics.data.MetricData; import java.io.IOException; @@ -30,9 +29,4 @@ public List deserialize(byte[] source) throws DeserializationExcepti throw new DeserializationException(e); } } - - @Override - public String signalType() { - return SignalTypes.metrics.name(); - } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/SignalDeserializer.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/SignalDeserializer.java index dd56e356e..915868288 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/SignalDeserializer.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/SignalDeserializer.java @@ -26,9 +26,4 @@ static SignalDeserializer ofLogs() { /** Deserializes the given byte array into a list of telemetry items. */ List deserialize(byte[] source) throws DeserializationException; - - /** Returns the name of the stored type of signal -- one of "metrics", "spans", or "logs". */ - default String signalType() { - return "unknown"; - } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/SpanDataDeserializer.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/SpanDataDeserializer.java index eb4406ff3..6433650d9 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/SpanDataDeserializer.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/deserializers/SpanDataDeserializer.java @@ -6,7 +6,6 @@ package io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers; import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.spans.ProtoSpansDataMapper; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; import io.opentelemetry.sdk.trace.data.SpanData; import java.io.IOException; @@ -30,9 +29,4 @@ public List deserialize(byte[] source) throws DeserializationException throw new DeserializationException(e); } } - - @Override - public String signalType() { - return SignalTypes.spans.name(); - } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java index ae6e6e19d..d65524457 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java @@ -35,8 +35,7 @@ public static FileSpanStorage create(File destinationDir, StorageConfiguration c public static FileSpanStorage create( File destinationDir, StorageConfiguration configuration, Clock clock) { FolderManager folderManager = FolderManager.create(destinationDir, configuration, clock); - return new FileSpanStorage( - new Storage<>(folderManager, configuration.isDebugEnabled()), SignalSerializer.ofSpans()); + return new FileSpanStorage(new Storage<>(folderManager), SignalSerializer.ofSpans()); } FileSpanStorage(Storage storage, SignalSerializer serializer) { diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java index 7797e6fe0..7add97eac 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java @@ -14,7 +14,6 @@ import io.opentelemetry.contrib.disk.buffering.internal.storage.files.WritableFile; import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.ReadableResult; import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.WritableResult; -import io.opentelemetry.contrib.disk.buffering.internal.utils.DebugLogger; import java.io.Closeable; import java.io.IOException; import java.util.Collection; @@ -27,22 +26,15 @@ public final class Storage implements Closeable { private static final int MAX_ATTEMPTS = 3; - private final DebugLogger logger; + private final Logger logger = Logger.getLogger(Storage.class.getName()); private final FolderManager folderManager; - private final boolean debugEnabled; private final AtomicBoolean isClosed = new AtomicBoolean(false); private final AtomicBoolean activeReadResultAvailable = new AtomicBoolean(false); private final AtomicReference writableFileRef = new AtomicReference<>(); private final AtomicReference readableFileRef = new AtomicReference<>(); - public Storage(FolderManager folderManager, boolean debugEnabled) { + public Storage(FolderManager folderManager) { this.folderManager = folderManager; - this.logger = DebugLogger.wrap(Logger.getLogger(Storage.class.getName()), debugEnabled); - this.debugEnabled = debugEnabled; - } - - public boolean isDebugEnabled() { - return debugEnabled; } /** @@ -57,18 +49,18 @@ public boolean write(SignalSerializer marshaler) throws IOException { private boolean write(SignalSerializer marshaler, int attemptNumber) throws IOException { if (isClosed.get()) { - logger.log("Refusing to write to storage after being closed."); + logger.info("Refusing to write to storage after being closed."); return false; } if (attemptNumber > MAX_ATTEMPTS) { - logger.log("Max number of attempts to write buffered data exceeded.", WARNING); + logger.log(WARNING, "Max number of attempts to write buffered data exceeded."); return false; } WritableFile writableFile = writableFileRef.get(); if (writableFile == null) { writableFile = folderManager.createWritableFile(); writableFileRef.set(writableFile); - logger.log("Created new writableFile: " + writableFile); + logger.info("Created new writableFile: " + writableFile); } WritableResult result = writableFile.append(marshaler); if (result != WritableResult.SUCCEEDED) { @@ -84,7 +76,7 @@ public void flush() throws IOException { if (writableFile != null) { writableFile.flush(); } else { - logger.log("No writable file to flush."); + logger.info("No writable file to flush."); } } @@ -106,25 +98,25 @@ public ReadableResult readNext(SignalDeserializer deserializer) throws IOE private ReadableResult doReadNext(SignalDeserializer deserializer, int attemptNumber) throws IOException { if (isClosed.get()) { - logger.log("Refusing to read from storage after being closed."); + logger.info("Refusing to read from storage after being closed."); return null; } if (attemptNumber > MAX_ATTEMPTS) { - logger.log("Maximum number of attempts to read and process buffered data exceeded.", WARNING); + logger.log(WARNING, "Maximum number of attempts to read buffered data exceeded."); return null; } ReadableFile readableFile = readableFileRef.get(); if (readableFile == null) { - logger.log("Obtaining a new readableFile from the folderManager."); + logger.info("Obtaining a new readableFile from the folderManager."); readableFile = folderManager.getReadableFile(); readableFileRef.set(readableFile); if (readableFile == null) { - logger.log("Unable to get or create readable file."); + logger.info("Unable to get or create readable file."); return null; } } - logger.log("Attempting to read data from " + readableFile); + logger.info("Attempting to read data from " + readableFile); byte[] result = readableFile.readNext(); if (result != null) { try { @@ -152,7 +144,7 @@ public boolean isClosed() { @Override public void close() throws IOException { - logger.log("Closing disk buffering storage."); + logger.info("Closing disk buffering storage."); if (isClosed.compareAndSet(false, true)) { folderManager.close(); writableFileRef.set(null); diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java index 714a01820..f0a1516f3 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java @@ -11,7 +11,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; -public final class StorageIterator implements Iterator> { +final class StorageIterator implements Iterator> { private final Storage storage; private final SignalDeserializer deserializer; private final Logger logger = Logger.getLogger(StorageIterator.class.getName()); @@ -23,7 +23,7 @@ public final class StorageIterator implements Iterator> { @GuardedBy("this") private boolean currentResultConsumed = false; - public StorageIterator(Storage storage, SignalDeserializer deserializer) { + StorageIterator(Storage storage, SignalDeserializer deserializer) { this.storage = storage; this.deserializer = deserializer; } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/utils/DebugLogger.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/utils/DebugLogger.java deleted file mode 100644 index 46ff72ebf..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/utils/DebugLogger.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.utils; - -import java.util.logging.Level; -import java.util.logging.Logger; - -public class DebugLogger { - private final Logger logger; - private final boolean debugEnabled; - - private DebugLogger(Logger logger, boolean debugEnabled) { - this.logger = logger; - this.debugEnabled = debugEnabled; - } - - public static DebugLogger wrap(Logger logger, boolean debugEnabled) { - return new DebugLogger(logger, debugEnabled); - } - - public void log(String msg) { - log(msg, Level.INFO); - } - - public void log(String msg, Level level) { - if (debugEnabled) { - logger.log(level, msg); - } - } - - public void log(String msg, Level level, Throwable e) { - if (debugEnabled) { - logger.log(level, msg, e); - } - } -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/utils/SignalTypes.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/utils/SignalTypes.java deleted file mode 100644 index c0a7f5765..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/utils/SignalTypes.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering.internal.utils; - -public enum SignalTypes { - metrics, - spans, - logs -} diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java index 1d1d84c68..b074c1e34 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java @@ -40,7 +40,7 @@ class FolderManagerTest { @BeforeEach void setUp() { clock = mock(); - folderManager = new FolderManager(rootDir, TestData.getConfiguration(rootDir), clock); + folderManager = new FolderManager(rootDir, TestData.getConfiguration(), clock); } @AfterEach diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageTest.java index 4379b20bc..611506b1b 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageTest.java @@ -48,9 +48,8 @@ class StorageTest { void setUp() { currentTimeMillis = new AtomicLong(0); serializer = SignalSerializer.ofLogs(); - folderManager = - FolderManager.create(destinationDir, getConfiguration(destinationDir), new TestClock()); - storage = new Storage<>(folderManager, true); + folderManager = FolderManager.create(destinationDir, getConfiguration(), new TestClock()); + storage = new Storage<>(folderManager); } @AfterEach diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java index 0376ce06b..49a39e0fa 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java @@ -10,7 +10,6 @@ import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.logs.models.LogRecordDataImpl; import io.opentelemetry.sdk.logs.data.LogRecordData; -import java.io.File; public final class TestData { @@ -71,9 +70,8 @@ public final class TestData { public static final int MAX_FILE_SIZE = 2000; public static final int MAX_FOLDER_SIZE = 6000; - public static StorageConfiguration getConfiguration(File rootDir) { + public static StorageConfiguration getConfiguration() { return StorageConfiguration.builder() - .setRootDir(rootDir) .setMaxFileAgeForWriteMillis(MAX_FILE_AGE_FOR_WRITE_MILLIS) .setMinFileAgeForReadMillis(MIN_FILE_AGE_FOR_READ_MILLIS) .setMaxFileAgeForReadMillis(MAX_FILE_AGE_FOR_READ_MILLIS) diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java index cdd202280..b409565d2 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java @@ -51,7 +51,7 @@ void setUp() throws IOException { source = new File(dir, "sourceFile"); addFileContents(source); clock = mock(); - readableFile = new ReadableFile(source, CREATED_TIME_MILLIS, clock, getConfiguration(dir)); + readableFile = new ReadableFile(source, CREATED_TIME_MILLIS, clock, getConfiguration()); } @AfterEach @@ -99,7 +99,7 @@ void whenNoMoreLinesAvailableToRead_deleteOriginalFile_close_and_returnNoContent } ReadableFile emptyReadableFile = - new ReadableFile(emptyFile, CREATED_TIME_MILLIS, clock, getConfiguration(dir)); + new ReadableFile(emptyFile, CREATED_TIME_MILLIS, clock, getConfiguration()); assertNull(emptyReadableFile.readNext()); diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFileTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFileTest.java index cae1e9f64..162886f9d 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFileTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFileTest.java @@ -45,7 +45,7 @@ void setUp() throws IOException { new WritableFile( new File(rootDir, String.valueOf(CREATED_TIME_MILLIS)), CREATED_TIME_MILLIS, - TestData.getConfiguration(rootDir), + TestData.getConfiguration(), clock); } From 499910497c2ffef958fa2ddb1709e83f11a13180 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:36:11 +0200 Subject: [PATCH 09/30] Creating FileSignalStorage --- ...panStorage.java => FileSignalStorage.java} | 42 +++++++++---------- .../internal/storage/StorageIterator.java | 3 ++ 2 files changed, 24 insertions(+), 21 deletions(-) rename disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/{FileSpanStorage.java => FileSignalStorage.java} (62%) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java similarity index 62% rename from disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java rename to disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java index d65524457..937a18e07 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSpanStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java @@ -5,13 +5,10 @@ package io.opentelemetry.contrib.disk.buffering.internal.storage; -import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; +import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.contrib.disk.buffering.storage.result.WriteResult; -import io.opentelemetry.sdk.common.Clock; -import io.opentelemetry.sdk.trace.data.SpanData; -import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Iterator; @@ -20,31 +17,29 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; /** Default storage implementation where items are stored in multiple protobuf files. */ -public final class FileSpanStorage implements SignalStorage.Span { - private final Storage storage; - private final SignalSerializer serializer; - private final Logger logger = Logger.getLogger(FileSpanStorage.class.getName()); +public final class FileSignalStorage implements SignalStorage { + private final Storage storage; + private final SignalSerializer serializer; + private final SignalDeserializer deserializer; + private final Logger logger = Logger.getLogger(FileSignalStorage.class.getName()); private final AtomicBoolean isClosed = new AtomicBoolean(false); + private final Object iteratorLock = new Object(); - public static FileSpanStorage create(File destinationDir, StorageConfiguration configuration) { - return create(destinationDir, configuration, Clock.getDefault()); - } - - public static FileSpanStorage create( - File destinationDir, StorageConfiguration configuration, Clock clock) { - FolderManager folderManager = FolderManager.create(destinationDir, configuration, clock); - return new FileSpanStorage(new Storage<>(folderManager), SignalSerializer.ofSpans()); - } + @GuardedBy("iteratorLock") + private Iterator> iterator; - FileSpanStorage(Storage storage, SignalSerializer serializer) { + public FileSignalStorage( + Storage storage, SignalSerializer serializer, SignalDeserializer deserializer) { this.storage = storage; this.serializer = serializer; + this.deserializer = deserializer; } @Override - public CompletableFuture write(Collection items) { + public CompletableFuture write(Collection items) { logger.finer("Intercepting batch."); try { serializer.initialize(items); @@ -83,7 +78,12 @@ public void close() throws IOException { @Nonnull @Override - public Iterator> iterator() { - throw new UnsupportedOperationException("For next PR"); + public Iterator> iterator() { + synchronized (iteratorLock) { + if (iterator == null) { + iterator = new StorageIterator<>(storage, deserializer); + } + return iterator; + } } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java index f0a1516f3..f9a676da4 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java @@ -39,6 +39,9 @@ public synchronized boolean hasNext() { @Override @Nullable public synchronized Collection next() { + if (storage.isClosed()) { + return null; + } if (findNext()) { currentResultConsumed = true; return Objects.requireNonNull(currentResult).getContent(); From 75b41aad0dd2e19b1dbaa73bc4218abf728a2d49 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:46:24 +0200 Subject: [PATCH 10/30] Creating file signal storage implementations --- .../internal/storage/FolderManager.java | 8 +-- .../internal/storage/files/ReadableFile.java | 6 +- .../internal/storage/files/WritableFile.java | 6 +- .../storage/impl/FileLogRecordStorage.java | 58 ++++++++++++++++++ .../storage/impl/FileMetricStorage.java | 59 +++++++++++++++++++ .../storage/impl/FileSpanStorage.java | 58 ++++++++++++++++++ .../impl/FileStorageConfiguration.java} | 14 ++--- .../buffering/internal/storage/TestData.java | 6 +- 8 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java create mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java create mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java rename disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/{config/StorageConfiguration.java => storage/impl/FileStorageConfiguration.java} (86%) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java index cad1d8302..a1a3a5267 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java @@ -7,9 +7,9 @@ import static io.opentelemetry.contrib.disk.buffering.internal.storage.util.ClockBuddy.nowMillis; -import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.ReadableFile; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.WritableFile; +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileStorageConfiguration; import io.opentelemetry.sdk.common.Clock; import java.io.Closeable; import java.io.File; @@ -23,12 +23,12 @@ public final class FolderManager implements Closeable { private final File folder; private final Clock clock; - private final StorageConfiguration configuration; + private final FileStorageConfiguration configuration; @Nullable private ReadableFile currentReadableFile; @Nullable private WritableFile currentWritableFile; public static FolderManager create( - File destinationDir, StorageConfiguration configuration, Clock clock) { + File destinationDir, FileStorageConfiguration configuration, Clock clock) { if (destinationDir.isFile()) { throw new IllegalArgumentException("destinationDir must be a directory"); } else if (!destinationDir.exists()) { @@ -39,7 +39,7 @@ public static FolderManager create( return new FolderManager(destinationDir, configuration, clock); } - public FolderManager(File folder, StorageConfiguration configuration, Clock clock) { + public FolderManager(File folder, FileStorageConfiguration configuration, Clock clock) { this.folder = folder; this.configuration = configuration; this.clock = clock; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java index ef456d343..59429d187 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java @@ -7,10 +7,10 @@ import static io.opentelemetry.contrib.disk.buffering.internal.storage.util.ClockBuddy.nowMillis; -import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.DelimitedProtoStreamReader; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.reader.StreamReader; import io.opentelemetry.contrib.disk.buffering.internal.storage.files.utils.FileStream; +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileStorageConfiguration; import io.opentelemetry.sdk.common.Clock; import java.io.File; import java.io.IOException; @@ -37,7 +37,7 @@ public final class ReadableFile implements FileOperations { private final AtomicBoolean isClosed = new AtomicBoolean(false); public ReadableFile( - File file, long createdTimeMillis, Clock clock, StorageConfiguration configuration) + File file, long createdTimeMillis, Clock clock, FileStorageConfiguration configuration) throws IOException { this( file, @@ -51,7 +51,7 @@ public ReadableFile( @Nonnull File file, long createdTimeMillis, Clock clock, - StorageConfiguration configuration, + FileStorageConfiguration configuration, StreamReader.Factory readerFactory) throws IOException { this.file = file; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFile.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFile.java index ce4e87ddf..325d30856 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFile.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFile.java @@ -7,9 +7,9 @@ import static io.opentelemetry.contrib.disk.buffering.internal.storage.util.ClockBuddy.nowMillis; -import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; import io.opentelemetry.contrib.disk.buffering.internal.storage.responses.WritableResult; +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileStorageConfiguration; import io.opentelemetry.sdk.common.Clock; import java.io.File; import java.io.FileOutputStream; @@ -21,7 +21,7 @@ public final class WritableFile implements FileOperations { private final File file; - private final StorageConfiguration configuration; + private final FileStorageConfiguration configuration; private final Clock clock; private final long expireTimeMillis; private final OutputStream out; @@ -29,7 +29,7 @@ public final class WritableFile implements FileOperations { private int size; public WritableFile( - File file, long createdTimeMillis, StorageConfiguration configuration, Clock clock) + File file, long createdTimeMillis, FileStorageConfiguration configuration, Clock clock) throws IOException { this.file = file; this.configuration = configuration; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java new file mode 100644 index 000000000..2a65deb99 --- /dev/null +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java @@ -0,0 +1,58 @@ +package io.opentelemetry.contrib.disk.buffering.storage.impl; + +import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; +import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; +import io.opentelemetry.contrib.disk.buffering.internal.storage.FileSignalStorage; +import io.opentelemetry.contrib.disk.buffering.internal.storage.FolderManager; +import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; +import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; +import io.opentelemetry.contrib.disk.buffering.storage.result.WriteResult; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; + +public final class FileLogRecordStorage implements SignalStorage.LogRecord { + private final FileSignalStorage fileSignalStorage; + + public static FileLogRecordStorage create(File destinationDir) { + return create(destinationDir, FileStorageConfiguration.getDefault()); + } + + public static FileLogRecordStorage create( + File destinationDir, FileStorageConfiguration configuration) { + Storage storage = + new Storage<>(FolderManager.create(destinationDir, configuration, Clock.getDefault())); + return new FileLogRecordStorage( + new FileSignalStorage<>(storage, SignalSerializer.ofLogs(), SignalDeserializer.ofLogs())); + } + + private FileLogRecordStorage(FileSignalStorage fileSignalStorage) { + this.fileSignalStorage = fileSignalStorage; + } + + @Override + public CompletableFuture write(Collection items) { + return fileSignalStorage.write(items); + } + + @Override + public CompletableFuture clear() { + return fileSignalStorage.clear(); + } + + @Override + public void close() throws IOException { + fileSignalStorage.close(); + } + + @Nonnull + @Override + public Iterator> iterator() { + return fileSignalStorage.iterator(); + } +} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java new file mode 100644 index 000000000..263a0b6e3 --- /dev/null +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java @@ -0,0 +1,59 @@ +package io.opentelemetry.contrib.disk.buffering.storage.impl; + +import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; +import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; +import io.opentelemetry.contrib.disk.buffering.internal.storage.FileSignalStorage; +import io.opentelemetry.contrib.disk.buffering.internal.storage.FolderManager; +import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; +import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; +import io.opentelemetry.contrib.disk.buffering.storage.result.WriteResult; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; + +public final class FileMetricStorage implements SignalStorage.Metric { + private final FileSignalStorage fileSignalStorage; + + public static FileMetricStorage create(File destinationDir) { + return create(destinationDir, FileStorageConfiguration.getDefault()); + } + + public static FileMetricStorage create( + File destinationDir, FileStorageConfiguration configuration) { + Storage storage = + new Storage<>(FolderManager.create(destinationDir, configuration, Clock.getDefault())); + return new FileMetricStorage( + new FileSignalStorage<>( + storage, SignalSerializer.ofMetrics(), SignalDeserializer.ofMetrics())); + } + + private FileMetricStorage(FileSignalStorage fileSignalStorage) { + this.fileSignalStorage = fileSignalStorage; + } + + @Override + public CompletableFuture write(Collection items) { + return fileSignalStorage.write(items); + } + + @Override + public CompletableFuture clear() { + return fileSignalStorage.clear(); + } + + @Override + public void close() throws IOException { + fileSignalStorage.close(); + } + + @Nonnull + @Override + public Iterator> iterator() { + return fileSignalStorage.iterator(); + } +} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java new file mode 100644 index 000000000..ee7c8f71c --- /dev/null +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java @@ -0,0 +1,58 @@ +package io.opentelemetry.contrib.disk.buffering.storage.impl; + +import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; +import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; +import io.opentelemetry.contrib.disk.buffering.internal.storage.FileSignalStorage; +import io.opentelemetry.contrib.disk.buffering.internal.storage.FolderManager; +import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; +import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; +import io.opentelemetry.contrib.disk.buffering.storage.result.WriteResult; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; + +public final class FileSpanStorage implements SignalStorage.Span { + private final FileSignalStorage fileSignalStorage; + + public static FileSpanStorage create(File destinationDir) { + return create(destinationDir, FileStorageConfiguration.getDefault()); + } + + public static FileSpanStorage create( + File destinationDir, FileStorageConfiguration configuration) { + Storage storage = + new Storage<>(FolderManager.create(destinationDir, configuration, Clock.getDefault())); + return new FileSpanStorage( + new FileSignalStorage<>(storage, SignalSerializer.ofSpans(), SignalDeserializer.ofSpans())); + } + + private FileSpanStorage(FileSignalStorage fileSignalStorage) { + this.fileSignalStorage = fileSignalStorage; + } + + @Override + public CompletableFuture write(Collection items) { + return fileSignalStorage.write(items); + } + + @Override + public CompletableFuture clear() { + return fileSignalStorage.clear(); + } + + @Override + public void close() throws IOException { + fileSignalStorage.close(); + } + + @Nonnull + @Override + public Iterator> iterator() { + return fileSignalStorage.iterator(); + } +} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileStorageConfiguration.java similarity index 86% rename from disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java rename to disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileStorageConfiguration.java index 126e4cc6d..350b437f5 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileStorageConfiguration.java @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.disk.buffering.config; +package io.opentelemetry.contrib.disk.buffering.storage.impl; import com.google.auto.value.AutoValue; import java.util.concurrent.TimeUnit; /** Defines how the storage should be managed. */ @AutoValue -public abstract class StorageConfiguration { +public abstract class FileStorageConfiguration { /** The max amount of time a file can receive new data. */ public abstract long getMaxFileAgeForWriteMillis(); @@ -41,12 +41,12 @@ public abstract class StorageConfiguration { */ public abstract int getMaxFolderSize(); - public static StorageConfiguration getDefault() { + public static FileStorageConfiguration getDefault() { return builder().build(); } public static Builder builder() { - return new AutoValue_StorageConfiguration.Builder() + return new AutoValue_FileStorageConfiguration.Builder() .setMaxFileSize(1024 * 1024) // 1MB .setMaxFolderSize(10 * 1024 * 1024) // 10MB .setMaxFileAgeForWriteMillis(TimeUnit.SECONDS.toMillis(30)) @@ -66,10 +66,10 @@ public abstract static class Builder { public abstract Builder setMaxFolderSize(int value); - abstract StorageConfiguration autoBuild(); + abstract FileStorageConfiguration autoBuild(); - public final StorageConfiguration build() { - StorageConfiguration configuration = autoBuild(); + public final FileStorageConfiguration build() { + FileStorageConfiguration configuration = autoBuild(); if (configuration.getMinFileAgeForReadMillis() <= configuration.getMaxFileAgeForWriteMillis()) { throw new IllegalArgumentException( diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java index 49a39e0fa..3b51125b6 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/TestData.java @@ -7,8 +7,8 @@ import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.Severity; -import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; import io.opentelemetry.contrib.disk.buffering.internal.serialization.mapping.logs.models.LogRecordDataImpl; +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileStorageConfiguration; import io.opentelemetry.sdk.logs.data.LogRecordData; public final class TestData { @@ -70,8 +70,8 @@ public final class TestData { public static final int MAX_FILE_SIZE = 2000; public static final int MAX_FOLDER_SIZE = 6000; - public static StorageConfiguration getConfiguration() { - return StorageConfiguration.builder() + public static FileStorageConfiguration getConfiguration() { + return FileStorageConfiguration.builder() .setMaxFileAgeForWriteMillis(MAX_FILE_AGE_FOR_WRITE_MILLIS) .setMinFileAgeForReadMillis(MIN_FILE_AGE_FOR_READ_MILLIS) .setMaxFileAgeForReadMillis(MAX_FILE_AGE_FOR_READ_MILLIS) From 9af7917e5c3cca34d5e388bf862bbbdae07ebfae Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:48:17 +0200 Subject: [PATCH 11/30] Fixing lint warnings --- .../disk/buffering/internal/storage/FileSignalStorage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java index 937a18e07..5da39a300 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java @@ -17,6 +17,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; /** Default storage implementation where items are stored in multiple protobuf files. */ @@ -29,6 +30,7 @@ public final class FileSignalStorage implements SignalStorage { private final Object iteratorLock = new Object(); @GuardedBy("iteratorLock") + @Nullable private Iterator> iterator; public FileSignalStorage( From 1ca3cb2109706190efa8d93754adaf85c8e3de2b Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Sun, 24 Aug 2025 14:06:30 +0200 Subject: [PATCH 12/30] Creating export to disk implementations --- .../exporters/LogRecordToDiskExporter.java | 78 +++++++++++++++ .../exporters/MetricToDiskExporter.java | 98 +++++++++++++++++++ .../exporters/SpanToDiskExporter.java | 2 + .../{ => callback}/ExporterCallback.java | 2 +- .../{ => callback}/NoopExporterCallback.java | 2 +- .../exporters/SignalStorageExporter.java | 5 +- .../exporters/SignalStorageExporterTest.java | 9 +- 7 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java create mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java rename disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/{ => callback}/ExporterCallback.java (93%) rename disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/{ => callback}/NoopExporterCallback.java (89%) rename disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/{ => internal}/exporters/SignalStorageExporter.java (90%) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java new file mode 100644 index 000000000..f0d574163 --- /dev/null +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.disk.buffering.exporters; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.contrib.disk.buffering.SignalType; +import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; +import io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter; +import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import java.time.Duration; +import java.util.Collection; + +/** Exporter that stores logs into disk. */ +public final class LogRecordToDiskExporter implements LogRecordExporter { + private final SignalStorageExporter storageExporter; + private final ExporterCallback callback; + private static final SignalType TYPE = SignalType.LOG; + + private LogRecordToDiskExporter( + SignalStorageExporter storageExporter, ExporterCallback callback) { + this.storageExporter = storageExporter; + this.callback = callback; + } + + public static Builder builder(SignalStorage.LogRecord storage) { + return new Builder(storage); + } + + @Override + public CompletableResultCode export(Collection logs) { + return storageExporter.exportToStorage(logs); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + callback.onShutdown(TYPE); + return CompletableResultCode.ofSuccess(); + } + + public static final class Builder { + private final SignalStorage.LogRecord storage; + private ExporterCallback callback = ExporterCallback.noop(); + private Duration writeTimeout = Duration.ofSeconds(10); + + @CanIgnoreReturnValue + public Builder setExporterCallback(ExporterCallback value) { + callback = value; + return this; + } + + @CanIgnoreReturnValue + public Builder setWriteTimeout(Duration value) { + writeTimeout = value; + return this; + } + + public LogRecordToDiskExporter build() { + SignalStorageExporter storageExporter = + new SignalStorageExporter<>(storage, callback, writeTimeout, TYPE); + return new LogRecordToDiskExporter(storageExporter, callback); + } + + private Builder(SignalStorage.LogRecord storage) { + this.storage = storage; + } + } +} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java new file mode 100644 index 000000000..c9a1830cc --- /dev/null +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.disk.buffering.exporters; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import io.opentelemetry.contrib.disk.buffering.SignalType; +import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; +import io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter; +import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import java.time.Duration; +import java.util.Collection; + +/** Exporter that stores metrics into disk. */ +public final class MetricToDiskExporter implements MetricExporter { + private final SignalStorageExporter storageExporter; + private final AggregationTemporalitySelector aggregationTemporalitySelector; + private final ExporterCallback callback; + private static final SignalType TYPE = SignalType.METRIC; + + private MetricToDiskExporter( + SignalStorageExporter storageExporter, + AggregationTemporalitySelector aggregationTemporalitySelector, + ExporterCallback callback) { + this.storageExporter = storageExporter; + this.aggregationTemporalitySelector = aggregationTemporalitySelector; + this.callback = callback; + } + + public static Builder builder(SignalStorage.Metric storage) { + return new Builder(storage); + } + + @Override + public CompletableResultCode export(Collection metrics) { + return storageExporter.exportToStorage(metrics); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + callback.onShutdown(TYPE); + return CompletableResultCode.ofSuccess(); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return aggregationTemporalitySelector.getAggregationTemporality(instrumentType); + } + + public static final class Builder { + private final SignalStorage.Metric storage; + private AggregationTemporalitySelector aggregationTemporalitySelector = + AggregationTemporalitySelector.alwaysCumulative(); + private ExporterCallback callback = ExporterCallback.noop(); + private Duration writeTimeout = Duration.ofSeconds(10); + + @CanIgnoreReturnValue + public Builder setExporterCallback(ExporterCallback value) { + callback = value; + return this; + } + + @CanIgnoreReturnValue + public Builder setWriteTimeout(Duration value) { + writeTimeout = value; + return this; + } + + @CanIgnoreReturnValue + public Builder setAggregationTemporalitySelector(AggregationTemporalitySelector value) { + aggregationTemporalitySelector = value; + return this; + } + + public MetricToDiskExporter build() { + SignalStorageExporter storageExporter = + new SignalStorageExporter<>(storage, callback, writeTimeout, TYPE); + return new MetricToDiskExporter(storageExporter, aggregationTemporalitySelector, callback); + } + + private Builder(SignalStorage.Metric storage) { + this.storage = storage; + } + } +} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java index 670c7cab7..835e93ebb 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java @@ -7,6 +7,8 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.contrib.disk.buffering.SignalType; +import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; +import io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.data.SpanData; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/ExporterCallback.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java similarity index 93% rename from disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/ExporterCallback.java rename to disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java index f877c7b7d..e893f7a55 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/ExporterCallback.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.disk.buffering.exporters; +package io.opentelemetry.contrib.disk.buffering.exporters.callback; import io.opentelemetry.contrib.disk.buffering.SignalType; import javax.annotation.Nullable; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/NoopExporterCallback.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java similarity index 89% rename from disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/NoopExporterCallback.java rename to disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java index 2dd4f2f70..198bbdf32 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/NoopExporterCallback.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.disk.buffering.exporters; +package io.opentelemetry.contrib.disk.buffering.exporters.callback; import io.opentelemetry.contrib.disk.buffering.SignalType; import javax.annotation.Nullable; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporter.java similarity index 90% rename from disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporter.java rename to disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporter.java index 51d135299..338bcc3dd 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporter.java @@ -3,9 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.disk.buffering.exporters; +package io.opentelemetry.contrib.disk.buffering.internal.exporters; import io.opentelemetry.contrib.disk.buffering.SignalType; +import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.contrib.disk.buffering.storage.result.WriteResult; import io.opentelemetry.sdk.common.CompletableResultCode; @@ -17,7 +18,7 @@ import java.util.concurrent.TimeoutException; /** Internal utility for common export to disk operations across all exporters. */ -final class SignalStorageExporter { +public final class SignalStorageExporter { private final SignalStorage storage; private final ExporterCallback callback; private final Duration writeTimeout; diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java index f09489e47..422c84e36 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java @@ -14,6 +14,7 @@ import static org.mockito.Mockito.when; import io.opentelemetry.contrib.disk.buffering.SignalType; +import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.contrib.disk.buffering.storage.result.WriteResult; import io.opentelemetry.sdk.common.CompletableResultCode; @@ -40,8 +41,8 @@ class SignalStorageExporterTest { void verifyExportToStorage_success() { SignalStorage.Span storage = new TestSpanStorage(); SignalType signalType = SignalType.SPAN; - SignalStorageExporter storageExporter = - new SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); + io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter storageExporter = + new io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); SpanData item1 = mock(); SpanData item2 = mock(); SpanData item3 = mock(); @@ -72,8 +73,8 @@ void verifyExportToStorage_success() { void verifyExportToStorage_failure() { SignalStorage.Span storage = mock(); SignalType signalType = SignalType.SPAN; - SignalStorageExporter storageExporter = - new SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); + io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter storageExporter = + new io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); SpanData item1 = mock(); // Without exception From b5fcf76b41029dbc86566d6d95593fff7aaa323d Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Sun, 24 Aug 2025 15:01:33 +0200 Subject: [PATCH 13/30] Updating integration tests --- .../disk/buffering/IntegrationTest.java | 217 ++++++++---------- .../exporters/SignalStorageExporterTest.java | 10 +- 2 files changed, 95 insertions(+), 132 deletions(-) rename disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/{ => internal}/exporters/SignalStorageExporterTest.java (88%) diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java index b46cba12f..eac4ad528 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java @@ -5,27 +5,26 @@ package io.opentelemetry.contrib.disk.buffering; -import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.lang.Thread.sleep; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import io.opentelemetry.api.logs.Logger; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.contrib.disk.buffering.config.StorageConfiguration; -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporterBuilder; -import io.opentelemetry.contrib.disk.buffering.internal.exporter.FromDiskExporterImpl; -import io.opentelemetry.contrib.disk.buffering.internal.exporter.ToDiskExporter; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; -import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer; -import io.opentelemetry.contrib.disk.buffering.internal.storage.Storage; -import io.opentelemetry.contrib.disk.buffering.internal.utils.SignalTypes; -import io.opentelemetry.sdk.common.Clock; -import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.contrib.disk.buffering.exporters.LogRecordToDiskExporter; +import io.opentelemetry.contrib.disk.buffering.exporters.MetricToDiskExporter; +import io.opentelemetry.contrib.disk.buffering.exporters.SpanToDiskExporter; +import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; +import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileLogRecordStorage; +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileMetricStorage; +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileSpanStorage; +import io.opentelemetry.contrib.disk.buffering.storage.impl.FileStorageConfiguration; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.data.LogRecordData; import io.opentelemetry.sdk.logs.export.LogRecordExporter; @@ -34,159 +33,123 @@ import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; -import io.opentelemetry.sdk.testing.exporter.InMemoryLogRecordExporter; -import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter; -import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.File; import java.io.IOException; -import java.util.Collection; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.function.Supplier; -import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; public class IntegrationTest { - private InMemorySpanExporter memorySpanExporter; private Tracer tracer; - private InMemoryMetricExporter memoryMetricExporter; private SdkMeterProvider meterProvider; private Meter meter; - private InMemoryLogRecordExporter memoryLogRecordExporter; private Logger logger; - private Clock clock; - @TempDir File rootDir; - private static final long INITIAL_TIME_IN_MILLIS = 1000; - private static final long NOW_NANOS = MILLISECONDS.toNanos(INITIAL_TIME_IN_MILLIS); - private StorageConfiguration storageConfig; - private Storage spanStorage; + private SignalStorage.Span spanStorage; + private SignalStorage.LogRecord logStorage; + private SignalStorage.Metric metricStorage; + private SpanToDiskExporter spanToDiskExporter; + private MetricToDiskExporter metricToDiskExporter; + private LogRecordToDiskExporter logToDiskExporter; + private ExporterCallback callback; + @TempDir private File rootDir; + private static final long DELAY_BEFORE_READING_MILLIS = 500; @BeforeEach - void setUp() throws IOException { - clock = mock(); - storageConfig = StorageConfiguration.getDefault(rootDir); - spanStorage = - Storage.builder(SignalTypes.spans) - .setStorageConfiguration(storageConfig) - .setStorageClock(clock) + void setUp() { + callback = mock(); + FileStorageConfiguration storageConfig = + FileStorageConfiguration.builder() + .setMaxFileAgeForWriteMillis(DELAY_BEFORE_READING_MILLIS - 1) + .setMinFileAgeForReadMillis(DELAY_BEFORE_READING_MILLIS) .build(); - when(clock.now()).thenReturn(NOW_NANOS); - // Setting up spans - memorySpanExporter = InMemorySpanExporter.create(); - ToDiskExporter toDiskSpanExporter = - buildToDiskExporter(SignalSerializer.ofSpans(), memorySpanExporter::export); - SpanToDiskExporter spanToDiskExporter = new SpanToDiskExporter(toDiskSpanExporter); + spanStorage = FileSpanStorage.create(new File(rootDir, "spans"), storageConfig); + spanToDiskExporter = + SpanToDiskExporter.builder(spanStorage).setExporterCallback(callback).build(); tracer = createTracerProvider(spanToDiskExporter).get("SpanInstrumentationScope"); // Setting up metrics - memoryMetricExporter = InMemoryMetricExporter.create(); - ToDiskExporter toDiskMetricExporter = - buildToDiskExporter(SignalSerializer.ofMetrics(), memoryMetricExporter::export); - MetricToDiskExporter metricToDiskExporter = - new MetricToDiskExporter(toDiskMetricExporter, memoryMetricExporter); + metricStorage = FileMetricStorage.create(new File(rootDir, "metrics"), storageConfig); + metricToDiskExporter = + MetricToDiskExporter.builder(metricStorage).setExporterCallback(callback).build(); meterProvider = createMeterProvider(metricToDiskExporter); meter = meterProvider.get("MetricInstrumentationScope"); // Setting up logs - memoryLogRecordExporter = InMemoryLogRecordExporter.create(); - ToDiskExporter toDiskLogExporter = - buildToDiskExporter(SignalSerializer.ofLogs(), memoryLogRecordExporter::export); - LogRecordToDiskExporter logToDiskExporter = new LogRecordToDiskExporter(toDiskLogExporter); + logStorage = FileLogRecordStorage.create(new File(rootDir, "logs"), storageConfig); + logToDiskExporter = + LogRecordToDiskExporter.builder(logStorage).setExporterCallback(callback).build(); logger = createLoggerProvider(logToDiskExporter).get("LogInstrumentationScope"); } @AfterEach void tearDown() throws IOException { + // Closing span exporter + spanToDiskExporter.shutdown(); + verify(callback).onShutdown(SignalType.SPAN); + verifyNoMoreInteractions(callback); + + // Closing log exporter + clearInvocations(callback); + logToDiskExporter.shutdown(); + verify(callback).onShutdown(SignalType.LOG); + verifyNoMoreInteractions(callback); + + // Closing metric exporter + clearInvocations(callback); + metricToDiskExporter.shutdown(); + verify(callback).onShutdown(SignalType.METRIC); + verifyNoMoreInteractions(callback); + + // Closing storages spanStorage.close(); - } - - @NotNull - private ToDiskExporter buildToDiskExporter( - SignalSerializer serializer, Function, CompletableResultCode> exporter) { - return ToDiskExporter.builder(spanStorage) - .setSerializer(serializer) - .setExportFunction(exporter) - .build(); - } - - @NotNull - private static FromDiskExporterImpl buildFromDiskExporter( - FromDiskExporterBuilder builder, - Function, CompletableResultCode> exportFunction, - SignalDeserializer deserializer) - throws IOException { - return builder.setExportFunction(exportFunction).setDeserializer(deserializer).build(); + logStorage.close(); + metricStorage.close(); } @Test - void verifySpansIntegration() throws IOException { + void verifyIntegration() throws InterruptedException { + // Creating span Span span = tracer.spanBuilder("Span name").startSpan(); span.end(); - FromDiskExporterImpl fromDiskExporter = - buildFromDiskExporter( - FromDiskExporterImpl.builder(spanStorage), - memorySpanExporter::export, - SignalDeserializer.ofSpans()); - assertExporter(fromDiskExporter, () -> memorySpanExporter.getFinishedSpanItems().size()); - } - - @Test - void verifyMetricsIntegration() throws IOException { - meter.counterBuilder("Counter").build().add(2); + verify(callback).onExportSuccess(SignalType.SPAN); + verifyNoMoreInteractions(callback); + + // Creating log + clearInvocations(callback); + logger.logRecordBuilder().setBody("Log body").emit(); + verify(callback).onExportSuccess(SignalType.LOG); + verifyNoMoreInteractions(callback); + + // Creating metric + clearInvocations(callback); + meter.counterBuilder("counter").build().add(1); meterProvider.forceFlush(); - - FromDiskExporterImpl fromDiskExporter = - buildFromDiskExporter( - FromDiskExporterImpl.builder(spanStorage), - memoryMetricExporter::export, - SignalDeserializer.ofMetrics()); - assertExporter(fromDiskExporter, () -> memoryMetricExporter.getFinishedMetricItems().size()); - } - - @Test - void verifyLogRecordsIntegration() throws IOException { - logger.logRecordBuilder().setBody("I'm a log!").emit(); - - FromDiskExporterImpl fromDiskExporter = - buildFromDiskExporter( - FromDiskExporterImpl.builder(spanStorage), - memoryLogRecordExporter::export, - SignalDeserializer.ofLogs()); - assertExporter( - fromDiskExporter, () -> memoryLogRecordExporter.getFinishedLogRecordItems().size()); - } - - private void assertExporter(FromDiskExporterImpl exporter, Supplier finishedItems) - throws IOException { - // Verify no data has been received in the original exporter until this point. - assertEquals(0, finishedItems.get()); - - // Go to the future when we can read the stored items. - fastForwardTimeByMillis(storageConfig.getMinFileAgeForReadMillis()); - - // Read and send stored data. - assertTrue(exporter.exportStoredBatch(1, TimeUnit.SECONDS)); - - // Now the data must have been delegated to the original exporter. - assertEquals(1, finishedItems.get()); - - // Bonus: Try to read again, no more data should be available. - assertFalse(exporter.exportStoredBatch(1, TimeUnit.SECONDS)); - assertEquals(1, finishedItems.get()); - } - - @SuppressWarnings("DirectInvocationOnMock") - private void fastForwardTimeByMillis(long milliseconds) { - when(clock.now()).thenReturn(NOW_NANOS + MILLISECONDS.toNanos(milliseconds)); + verify(callback).onExportSuccess(SignalType.METRIC); + verifyNoMoreInteractions(callback); + + // Waiting for read time + sleep(DELAY_BEFORE_READING_MILLIS); + + // Read + List storedSpans = new ArrayList<>(); + List storedLogs = new ArrayList<>(); + List storedMetrics = new ArrayList<>(); + spanStorage.forEach(storedSpans::addAll); + logStorage.forEach(storedLogs::addAll); + metricStorage.forEach(storedMetrics::addAll); + + assertEquals(1, storedSpans.size()); + assertEquals(1, storedLogs.size()); + assertEquals(1, storedMetrics.size()); } private static SdkTracerProvider createTracerProvider(SpanExporter exporter) { diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporterTest.java similarity index 88% rename from disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java rename to disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporterTest.java index 422c84e36..0efecb234 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/exporters/SignalStorageExporterTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporterTest.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.contrib.disk.buffering.exporters; +package io.opentelemetry.contrib.disk.buffering.internal.exporters; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyCollection; @@ -41,8 +41,8 @@ class SignalStorageExporterTest { void verifyExportToStorage_success() { SignalStorage.Span storage = new TestSpanStorage(); SignalType signalType = SignalType.SPAN; - io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter storageExporter = - new io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); + SignalStorageExporter storageExporter = + new SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); SpanData item1 = mock(); SpanData item2 = mock(); SpanData item3 = mock(); @@ -73,8 +73,8 @@ void verifyExportToStorage_success() { void verifyExportToStorage_failure() { SignalStorage.Span storage = mock(); SignalType signalType = SignalType.SPAN; - io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter storageExporter = - new io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); + SignalStorageExporter storageExporter = + new SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); SpanData item1 = mock(); // Without exception From 6e35a731c8d5d40c4584702461f22a1db86c0f79 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:45:18 +0200 Subject: [PATCH 14/30] Updating error message --- .../contrib/disk/buffering/internal/storage/FolderManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java index a1a3a5267..605bc56ed 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java @@ -33,7 +33,7 @@ public static FolderManager create( throw new IllegalArgumentException("destinationDir must be a directory"); } else if (!destinationDir.exists()) { if (!destinationDir.mkdirs()) { - throw new IllegalStateException("Could not created dir " + destinationDir); + throw new IllegalStateException("Could not create dir: " + destinationDir); } } return new FolderManager(destinationDir, configuration, clock); From 81533138817703188e0d328811bc57c368470a5c Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:16:34 +0200 Subject: [PATCH 15/30] Updating DESIGN.md --- disk-buffering/DESIGN.md | 69 ++++++++++++------------- disk-buffering/assets/reading-flow.png | Bin 120001 -> 54801 bytes disk-buffering/assets/writing-flow.png | Bin 74418 -> 69466 bytes 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/disk-buffering/DESIGN.md b/disk-buffering/DESIGN.md index 01f6048da..3bd1e3f01 100644 --- a/disk-buffering/DESIGN.md +++ b/disk-buffering/DESIGN.md @@ -1,59 +1,62 @@ # Design Overview -There are three main disk-writing exporters provided by this module: +The core of disk buffering +is [SignalStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/SignalStorage.java). +SignalStorage is an abstraction that defines the bare minimum functionalities needed for +implementations to allow writing and reading signals. -* [LogRecordToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java) -* [MetricToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java) -* [SpanToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java)) +There is a default implementation per signal that writes serialized signal items to protobuf +delimited messages into files, where each file's name represents a timestamp of when it was created, +which will help later to know when it's ready to read, as well as when it's expired. These +implementations are the following: -Each is responsible for writing a specific type of telemetry to disk storage for later -harvest/ingest. +* [FileSpanStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java) +* [FileLogRecordStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java) +* [FileMetricStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java) -For later reading, there are: +Each one has a `create()` method that takes a destination directory (to store data into) and an +optional [FileStorageConfiguration](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileStorageConfiguration.java) +to have a finer control of the storing behavior. -* [LogRecordFromToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordFromDiskExporter.java) -* [MetricFromDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/MetricFromDiskExporter.java) -* [SpanFromDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/SpanFromDiskExporter.java)) +Even +though [SignalStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/SignalStorage.java) +can receive signal items directly to be stored in disk, there are convenience exporter +implementations for each signal that handle the storing process on your behalf. Those are the +following: -Each one of those has a `create()` method that takes a delegate exporter (to send data -to ingest) and the `StorageConfiguration` that tells them where to find buffered data. +* [SpanToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java) +* [LogRecordToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java) +* [MetricToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java) -As explained in the [README](README.md), this has to be triggered manually by the consumer of -this library and does not happen automatically. +Each receive their +respective [SignalStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/SignalStorage.java) +object to delegate signals to as well as an optional callback object to notify its operations. ## Writing overview ![Writing flow](assets/writing-flow.png) -* The writing process happens automatically within its `export(Collection signals)` - method, which is called by the configured signal processor. -* When a set of signals is received, these are delegated over to - a type-specific wrapper of [ToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporter/ToDiskExporter.java) - class which then serializes them using an implementation - of [SignalSerializer](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/serializers/SignalSerializer.java) - and then the serialized data is appended into a File using an instance of - the [Storage](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java) - class. +* Via the convenience toDisk exporters, the writing process happens automatically within their + `export(Collection signals)` method, which is called by the configured signal + processor. +* When a set of signals is received, these are delegated over to a type-specific serializer + and then the serialized data is appended into a file. * The data is written into a file directly, without the use of a buffer, to make sure no data gets lost in case the application ends unexpectedly. -* Each disk exporter stores its signals in its own folder, which is expected to contain files +* Each signal storage stores its signals in its own folder, which is expected to contain files that belong to that type of signal only. * Each file may contain more than a batch of signals if the configuration parameters allow enough limit size for it. * If the configured folder size for the signals has been reached and a new file is needed to be created to keep storing new data, the oldest available file will be removed to make space for the new one. -* The [Storage](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java), - [FolderManager](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java) - and [WritableFile](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFile.java) - files contain more information on the details of the writing process into a file. ## Reading overview ![Reading flow](assets/reading-flow.png) -* The reading process has to be triggered manually by the library consumer as explained in - the [README](README.md). +* The reading process has to be triggered manually by the library consumer via the signal storage + iterator. * A single file is read at a time and updated to remove the data gathered from it after it is successfully exported, until it's emptied. Each file previously created during the writing process has a timestamp in milliseconds, which is used to determine what file to start @@ -62,9 +65,3 @@ this library and does not happen automatically. the time of creating the disk exporter, then it will be ignored, and the next oldest (and unexpired) one will be used instead. * All the stale and empty files will be removed as a new file is created. -* The [Storage](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java), - [FolderManager](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java) - and [ReadableFile](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java) - files contain more information on the details of the file reading process. -* Note that the reader delegates the data to the exporter exactly in the way it has received the - data - it does not try to batch data (but this could be an optimization in the future). diff --git a/disk-buffering/assets/reading-flow.png b/disk-buffering/assets/reading-flow.png index 76b8de43871efa097dd283a0cce5e05eab1885f2..63750e5a366600bb0dc35eb4a0bfc8bfb50207f3 100644 GIT binary patch literal 54801 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Ê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≦rJDsZw;GZMHUUZzi>0p>7L!gz)Z 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 literal 120001 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 diff --git a/disk-buffering/assets/writing-flow.png b/disk-buffering/assets/writing-flow.png index c6144b301c6f77b42e68dc9e8f47495643117b97..b4b21359dc28d0d742da7dd515989c968c678a02 100644 GIT binary patch literal 69466 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 literal 74418 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 LL%vwn$p8NX#&kri From 1d64de8997da86f8bc5afd10825082763a8f62e6 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:31:31 +0200 Subject: [PATCH 16/30] Making callbacks aware of the signal type --- .../contrib/disk/buffering/SignalType.java | 12 ----- .../exporters/LogRecordToDiskExporter.java | 15 +++--- .../exporters/MetricToDiskExporter.java | 14 +++--- .../exporters/SpanToDiskExporter.java | 14 +++--- .../exporters/callback/ExporterCallback.java | 24 ++++------ .../callback/NoopExporterCallback.java | 13 +++--- .../exporters/SignalStorageExporter.java | 13 ++---- .../disk/buffering/IntegrationTest.java | 46 +++++++++---------- .../exporters/SignalStorageExporterTest.java | 30 ++++++------ 9 files changed, 78 insertions(+), 103 deletions(-) delete mode 100644 disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SignalType.java diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SignalType.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SignalType.java deleted file mode 100644 index c66ed940e..000000000 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/SignalType.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.disk.buffering; - -public enum SignalType { - SPAN, - LOG, - METRIC -} diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java index f0d574163..3d6d62bad 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java @@ -6,7 +6,6 @@ package io.opentelemetry.contrib.disk.buffering.exporters; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.contrib.disk.buffering.SignalType; import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; import io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; @@ -19,11 +18,11 @@ /** Exporter that stores logs into disk. */ public final class LogRecordToDiskExporter implements LogRecordExporter { private final SignalStorageExporter storageExporter; - private final ExporterCallback callback; - private static final SignalType TYPE = SignalType.LOG; + private final ExporterCallback callback; private LogRecordToDiskExporter( - SignalStorageExporter storageExporter, ExporterCallback callback) { + SignalStorageExporter storageExporter, + ExporterCallback callback) { this.storageExporter = storageExporter; this.callback = callback; } @@ -44,17 +43,17 @@ public CompletableResultCode flush() { @Override public CompletableResultCode shutdown() { - callback.onShutdown(TYPE); + callback.onShutdown(); return CompletableResultCode.ofSuccess(); } public static final class Builder { private final SignalStorage.LogRecord storage; - private ExporterCallback callback = ExporterCallback.noop(); + private ExporterCallback callback = ExporterCallback.noop(); private Duration writeTimeout = Duration.ofSeconds(10); @CanIgnoreReturnValue - public Builder setExporterCallback(ExporterCallback value) { + public Builder setExporterCallback(ExporterCallback value) { callback = value; return this; } @@ -67,7 +66,7 @@ public Builder setWriteTimeout(Duration value) { public LogRecordToDiskExporter build() { SignalStorageExporter storageExporter = - new SignalStorageExporter<>(storage, callback, writeTimeout, TYPE); + new SignalStorageExporter<>(storage, callback, writeTimeout); return new LogRecordToDiskExporter(storageExporter, callback); } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java index c9a1830cc..27132cc28 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java @@ -6,7 +6,6 @@ package io.opentelemetry.contrib.disk.buffering.exporters; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.contrib.disk.buffering.SignalType; import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; import io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; @@ -23,13 +22,12 @@ public final class MetricToDiskExporter implements MetricExporter { private final SignalStorageExporter storageExporter; private final AggregationTemporalitySelector aggregationTemporalitySelector; - private final ExporterCallback callback; - private static final SignalType TYPE = SignalType.METRIC; + private final ExporterCallback callback; private MetricToDiskExporter( SignalStorageExporter storageExporter, AggregationTemporalitySelector aggregationTemporalitySelector, - ExporterCallback callback) { + ExporterCallback callback) { this.storageExporter = storageExporter; this.aggregationTemporalitySelector = aggregationTemporalitySelector; this.callback = callback; @@ -51,7 +49,7 @@ public CompletableResultCode flush() { @Override public CompletableResultCode shutdown() { - callback.onShutdown(TYPE); + callback.onShutdown(); return CompletableResultCode.ofSuccess(); } @@ -64,11 +62,11 @@ public static final class Builder { private final SignalStorage.Metric storage; private AggregationTemporalitySelector aggregationTemporalitySelector = AggregationTemporalitySelector.alwaysCumulative(); - private ExporterCallback callback = ExporterCallback.noop(); + private ExporterCallback callback = ExporterCallback.noop(); private Duration writeTimeout = Duration.ofSeconds(10); @CanIgnoreReturnValue - public Builder setExporterCallback(ExporterCallback value) { + public Builder setExporterCallback(ExporterCallback value) { callback = value; return this; } @@ -87,7 +85,7 @@ public Builder setAggregationTemporalitySelector(AggregationTemporalitySelector public MetricToDiskExporter build() { SignalStorageExporter storageExporter = - new SignalStorageExporter<>(storage, callback, writeTimeout, TYPE); + new SignalStorageExporter<>(storage, callback, writeTimeout); return new MetricToDiskExporter(storageExporter, aggregationTemporalitySelector, callback); } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java index 835e93ebb..7ad7ec086 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java @@ -6,7 +6,6 @@ package io.opentelemetry.contrib.disk.buffering.exporters; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import io.opentelemetry.contrib.disk.buffering.SignalType; import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; import io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; @@ -19,11 +18,10 @@ /** Exporter that stores spans into disk. */ public final class SpanToDiskExporter implements SpanExporter { private final SignalStorageExporter storageExporter; - private final ExporterCallback callback; - private static final SignalType TYPE = SignalType.SPAN; + private final ExporterCallback callback; private SpanToDiskExporter( - SignalStorageExporter storageExporter, ExporterCallback callback) { + SignalStorageExporter storageExporter, ExporterCallback callback) { this.storageExporter = storageExporter; this.callback = callback; } @@ -44,17 +42,17 @@ public CompletableResultCode flush() { @Override public CompletableResultCode shutdown() { - callback.onShutdown(TYPE); + callback.onShutdown(); return CompletableResultCode.ofSuccess(); } public static final class Builder { private final SignalStorage.Span storage; - private ExporterCallback callback = ExporterCallback.noop(); + private ExporterCallback callback = ExporterCallback.noop(); private Duration writeTimeout = Duration.ofSeconds(10); @CanIgnoreReturnValue - public Builder setExporterCallback(ExporterCallback value) { + public Builder setExporterCallback(ExporterCallback value) { callback = value; return this; } @@ -67,7 +65,7 @@ public Builder setWriteTimeout(Duration value) { public SpanToDiskExporter build() { SignalStorageExporter storageExporter = - new SignalStorageExporter<>(storage, callback, writeTimeout, TYPE); + new SignalStorageExporter<>(storage, callback, writeTimeout); return new SpanToDiskExporter(storageExporter, callback); } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java index e893f7a55..4ed98e99e 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java @@ -5,34 +5,30 @@ package io.opentelemetry.contrib.disk.buffering.exporters.callback; -import io.opentelemetry.contrib.disk.buffering.SignalType; +import java.util.Collection; import javax.annotation.Nullable; /** Notifies about exporter and storage-related operations from within a signal to disk exporter. */ -public interface ExporterCallback { +public interface ExporterCallback { /** * Called when an export to disk operation succeeded. * - * @param type The type of signal associated to the exporter. + * @param items The items successfully stored in disk. */ - void onExportSuccess(SignalType type); + void onExportSuccess(Collection items); /** * Called when an export to disk operation failed. * - * @param type The type of signal associated to the exporter. + * @param items The items that couldn't get stored in disk. * @param error Optional - provides more information of why the operation failed. */ - void onExportError(SignalType type, @Nullable Throwable error); + void onExportError(Collection items, @Nullable Throwable error); - /** - * Called when the exporter is closed. - * - * @param type The type of signal associated to the exporter. - */ - void onShutdown(SignalType type); + /** Called when the exporter is closed. */ + void onShutdown(); - static ExporterCallback noop() { - return NoopExporterCallback.INSTANCE; + static ExporterCallback noop() { + return new NoopExporterCallback<>(); } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java index 198bbdf32..9d89aaedc 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java @@ -5,20 +5,19 @@ package io.opentelemetry.contrib.disk.buffering.exporters.callback; -import io.opentelemetry.contrib.disk.buffering.SignalType; +import java.util.Collection; import javax.annotation.Nullable; -final class NoopExporterCallback implements ExporterCallback { - static final NoopExporterCallback INSTANCE = new NoopExporterCallback(); +final class NoopExporterCallback implements ExporterCallback { - private NoopExporterCallback() {} + NoopExporterCallback() {} @Override - public void onExportSuccess(SignalType type) {} + public void onExportSuccess(Collection items) {} @Override - public void onExportError(SignalType type, @Nullable Throwable error) {} + public void onExportError(Collection items, @Nullable Throwable error) {} @Override - public void onShutdown(SignalType type) {} + public void onShutdown() {} } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporter.java index 338bcc3dd..dc065d1c1 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporter.java @@ -5,7 +5,6 @@ package io.opentelemetry.contrib.disk.buffering.internal.exporters; -import io.opentelemetry.contrib.disk.buffering.SignalType; import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.contrib.disk.buffering.storage.result.WriteResult; @@ -20,16 +19,14 @@ /** Internal utility for common export to disk operations across all exporters. */ public final class SignalStorageExporter { private final SignalStorage storage; - private final ExporterCallback callback; + private final ExporterCallback callback; private final Duration writeTimeout; - private final SignalType type; public SignalStorageExporter( - SignalStorage storage, ExporterCallback callback, Duration writeTimeout, SignalType type) { + SignalStorage storage, ExporterCallback callback, Duration writeTimeout) { this.storage = storage; this.callback = callback; this.writeTimeout = writeTimeout; - this.type = type; } public CompletableResultCode exportToStorage(Collection items) { @@ -37,18 +34,18 @@ public CompletableResultCode exportToStorage(Collection items) { try { WriteResult operation = future.get(writeTimeout.toMillis(), TimeUnit.MILLISECONDS); if (operation.isSuccessful()) { - callback.onExportSuccess(type); + callback.onExportSuccess(items); return CompletableResultCode.ofSuccess(); } Throwable error = operation.getError(); - callback.onExportError(type, error); + callback.onExportError(items, error); if (error != null) { return CompletableResultCode.ofExceptionalFailure(error); } return CompletableResultCode.ofFailure(); } catch (ExecutionException | InterruptedException | TimeoutException e) { - callback.onExportError(type, e); + callback.onExportError(items, e); return CompletableResultCode.ofExceptionalFailure(e); } } diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java index eac4ad528..9f3ecded0 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java @@ -7,8 +7,7 @@ import static java.lang.Thread.sleep; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.mock; +import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -44,8 +43,12 @@ 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; import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) public class IntegrationTest { private Tracer tracer; private SdkMeterProvider meterProvider; @@ -57,13 +60,14 @@ public class IntegrationTest { private SpanToDiskExporter spanToDiskExporter; private MetricToDiskExporter metricToDiskExporter; private LogRecordToDiskExporter logToDiskExporter; - private ExporterCallback callback; + @Mock private ExporterCallback spanCallback; + @Mock private ExporterCallback logCallback; + @Mock private ExporterCallback metricCallback; @TempDir private File rootDir; private static final long DELAY_BEFORE_READING_MILLIS = 500; @BeforeEach void setUp() { - callback = mock(); FileStorageConfiguration storageConfig = FileStorageConfiguration.builder() .setMaxFileAgeForWriteMillis(DELAY_BEFORE_READING_MILLIS - 1) @@ -73,20 +77,20 @@ void setUp() { // Setting up spans spanStorage = FileSpanStorage.create(new File(rootDir, "spans"), storageConfig); spanToDiskExporter = - SpanToDiskExporter.builder(spanStorage).setExporterCallback(callback).build(); + SpanToDiskExporter.builder(spanStorage).setExporterCallback(spanCallback).build(); tracer = createTracerProvider(spanToDiskExporter).get("SpanInstrumentationScope"); // Setting up metrics metricStorage = FileMetricStorage.create(new File(rootDir, "metrics"), storageConfig); metricToDiskExporter = - MetricToDiskExporter.builder(metricStorage).setExporterCallback(callback).build(); + MetricToDiskExporter.builder(metricStorage).setExporterCallback(metricCallback).build(); meterProvider = createMeterProvider(metricToDiskExporter); meter = meterProvider.get("MetricInstrumentationScope"); // Setting up logs logStorage = FileLogRecordStorage.create(new File(rootDir, "logs"), storageConfig); logToDiskExporter = - LogRecordToDiskExporter.builder(logStorage).setExporterCallback(callback).build(); + LogRecordToDiskExporter.builder(logStorage).setExporterCallback(logCallback).build(); logger = createLoggerProvider(logToDiskExporter).get("LogInstrumentationScope"); } @@ -94,20 +98,18 @@ void setUp() { void tearDown() throws IOException { // Closing span exporter spanToDiskExporter.shutdown(); - verify(callback).onShutdown(SignalType.SPAN); - verifyNoMoreInteractions(callback); + verify(spanCallback).onShutdown(); + verifyNoMoreInteractions(spanCallback); // Closing log exporter - clearInvocations(callback); logToDiskExporter.shutdown(); - verify(callback).onShutdown(SignalType.LOG); - verifyNoMoreInteractions(callback); + verify(logCallback).onShutdown(); + verifyNoMoreInteractions(spanCallback); // Closing metric exporter - clearInvocations(callback); metricToDiskExporter.shutdown(); - verify(callback).onShutdown(SignalType.METRIC); - verifyNoMoreInteractions(callback); + verify(metricCallback).onShutdown(); + verifyNoMoreInteractions(spanCallback); // Closing storages spanStorage.close(); @@ -120,21 +122,19 @@ void verifyIntegration() throws InterruptedException { // Creating span Span span = tracer.spanBuilder("Span name").startSpan(); span.end(); - verify(callback).onExportSuccess(SignalType.SPAN); - verifyNoMoreInteractions(callback); + verify(spanCallback).onExportSuccess(anyCollection()); + verifyNoMoreInteractions(spanCallback); // Creating log - clearInvocations(callback); logger.logRecordBuilder().setBody("Log body").emit(); - verify(callback).onExportSuccess(SignalType.LOG); - verifyNoMoreInteractions(callback); + verify(logCallback).onExportSuccess(anyCollection()); + verifyNoMoreInteractions(spanCallback); // Creating metric - clearInvocations(callback); meter.counterBuilder("counter").build().add(1); meterProvider.forceFlush(); - verify(callback).onExportSuccess(SignalType.METRIC); - verifyNoMoreInteractions(callback); + verify(metricCallback).onExportSuccess(anyCollection()); + verifyNoMoreInteractions(spanCallback); // Waiting for read time sleep(DELAY_BEFORE_READING_MILLIS); diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporterTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporterTest.java index 0efecb234..30bfe310a 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporterTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/SignalStorageExporterTest.java @@ -13,7 +13,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import io.opentelemetry.contrib.disk.buffering.SignalType; import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.contrib.disk.buffering.storage.result.WriteResult; @@ -33,32 +32,34 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +@SuppressWarnings("unchecked") @ExtendWith(MockitoExtension.class) class SignalStorageExporterTest { - @Mock private ExporterCallback callback; + @Mock private ExporterCallback callback; @Test void verifyExportToStorage_success() { SignalStorage.Span storage = new TestSpanStorage(); - SignalType signalType = SignalType.SPAN; SignalStorageExporter storageExporter = - new SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); + new SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1)); SpanData item1 = mock(); SpanData item2 = mock(); SpanData item3 = mock(); - CompletableResultCode resultCode = storageExporter.exportToStorage(Arrays.asList(item1, item2)); + List items = Arrays.asList(item1, item2); + CompletableResultCode resultCode = storageExporter.exportToStorage(items); assertThat(resultCode.isSuccess()).isTrue(); - verify(callback).onExportSuccess(signalType); + verify(callback).onExportSuccess(items); verifyNoMoreInteractions(callback); // Adding more items clearInvocations(callback); - resultCode = storageExporter.exportToStorage(Collections.singletonList(item3)); + List items2 = Collections.singletonList(item3); + resultCode = storageExporter.exportToStorage(items2); assertThat(resultCode.isSuccess()).isTrue(); - verify(callback).onExportSuccess(signalType); + verify(callback).onExportSuccess(items2); verifyNoMoreInteractions(callback); // Checking items @@ -72,21 +73,20 @@ void verifyExportToStorage_success() { @Test void verifyExportToStorage_failure() { SignalStorage.Span storage = mock(); - SignalType signalType = SignalType.SPAN; SignalStorageExporter storageExporter = - new SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1), signalType); + new SignalStorageExporter<>(storage, callback, Duration.ofSeconds(1)); SpanData item1 = mock(); // Without exception when(storage.write(anyCollection())) .thenReturn(CompletableFuture.completedFuture(WriteResult.error(null))); - CompletableResultCode resultCode = - storageExporter.exportToStorage(Collections.singletonList(item1)); + List items = Collections.singletonList(item1); + CompletableResultCode resultCode = storageExporter.exportToStorage(items); assertThat(resultCode.isSuccess()).isFalse(); assertThat(resultCode.getFailureThrowable()).isNull(); - verify(callback).onExportError(signalType, null); + verify(callback).onExportError(items, null); verifyNoMoreInteractions(callback); // With exception @@ -95,11 +95,11 @@ void verifyExportToStorage_failure() { when(storage.write(anyCollection())) .thenReturn(CompletableFuture.completedFuture(WriteResult.error(exception))); - resultCode = storageExporter.exportToStorage(Collections.singletonList(item1)); + resultCode = storageExporter.exportToStorage(items); assertThat(resultCode.isSuccess()).isFalse(); assertThat(resultCode.getFailureThrowable()).isEqualTo(exception); - verify(callback).onExportError(signalType, exception); + verify(callback).onExportError(items, exception); verifyNoMoreInteractions(callback); } From b8c0a5916e09234cb6f8f06a8930333d089b9220 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:40:08 +0200 Subject: [PATCH 17/30] Updating README.md --- disk-buffering/README.md | 174 ++++++++++++++++++++++----------------- 1 file changed, 99 insertions(+), 75 deletions(-) diff --git a/disk-buffering/README.md b/disk-buffering/README.md index 72e17144d..f4f12a2da 100644 --- a/disk-buffering/README.md +++ b/disk-buffering/README.md @@ -1,109 +1,133 @@ # Disk buffering -This module provides exporters that store telemetry data in files which can be -sent later on demand. A high level description of how it works is that there are two separate -processes in place, one for writing data in disk, and one for reading/exporting the previously -stored data. +This module provides an abstraction +named [SignalStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/SignalStorage.java), +as well as default implementations for each signal type that allow writing signals to disk and +reading them later. -* Each exporter stores the received data automatically in disk right after it's received from its - processor. -* The reading of the data back from disk and exporting process has to be done manually. At - the moment there's no automatic mechanism to do so. There's more information on how it can be - achieved, under [Reading data](#reading-data). +For a more detailed information on how the whole process works, take a look at +the [DESIGN.md](DESIGN.md) file. -> For a more detailed information on how the whole process works, take a look at -> the [DESIGN.md](DESIGN.md) file. +## Default implementation usage -## Configuration +The default implementations are the following: -The configurable parameters are provided **per exporter**, the available ones are: +* [FileSpanStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java) +* [FileLogRecordStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java) +* [FileMetricStorage](src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java) -* Max file size, defaults to 1MB. -* Max folder size, defaults to 10MB. All files are stored in a single folder per-signal, therefore - if all 3 types of signals are stored, the total amount of space from disk to be taken by default - would be of 30MB. -* Max age for file writing, defaults to 30 seconds. -* Min age for file reading, defaults to 33 seconds. It must be greater that the max age for file - writing. -* Max age for file reading, defaults to 18 hours. After that time passes, the file will be - considered stale and will be removed when new files are created. No more data will be read from a - file past this time. - -## Usage +### Set up -### Storing data +We need to create a signal storage object per signal type to start writing signals to disk. Each +`File*Storage` implementation +has a `create()` function that receives: + +* A File directory to store the signal files. Note that each signal storage object must have a + dedicated directory to work properly. +* (Optional) a configuration object. -In order to use it, you need to wrap your own exporter with a new instance of -the ones provided in here: +The available configuration parameters are the following: -* For a LogRecordExporter, it must be wrapped within - a [LogRecordToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordToDiskExporter.java). -* For a MetricExporter, it must be wrapped within - a [MetricToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/MetricToDiskExporter.java). -* For a SpanExporter, it must be wrapped within - a [SpanToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/SpanToDiskExporter.java). +* Max file size, defaults to 1MB. +* Max folder size, defaults to 10MB. +* Max age for file writing. It sets the time window where a file can get signals appended to it. + Defaults to 30 seconds. +* Min age for file reading. It sets the time to wait before starting to read from a file after + its creation. Defaults to 33 seconds. It must be greater that the max age for file writing. +* Max age for file reading. After that time passes, the file will be considered stale and will be + removed when new files are created. No more data will be read from a file past this time. Defaults + to 18 hours. -Each wrapper will need the following when instantiating them: +```java +// Root dir +File rootDir = new File("/some/root"); -* The exporter to be wrapped. -* A File instance of the root directory where all the data is going to be written. The same root dir - can be used for all the wrappers, since each will create their own folder inside it. -* An instance - of [StorageConfiguration](src/main/java/io/opentelemetry/contrib/disk/buffering/config/StorageConfiguration.java) - with the desired parameters. You can create one with default values by - calling `StorageConfiguration.getDefault()`. +// Setting up span storage +SignalStorage.Span spanStorage = FileSpanStorage.create(new File(rootDir, "spans")); -After wrapping your exporters, you must register the wrapper as the exporter you'll use. It will -take care of always storing the data it receives. +// Setting up metric storage +SignalStorage.Metric metricStorage = FileMetricStorage.create(new File(rootDir, "metrics")); -#### Set up example for spans +// Setting up log storage +SignalStorage.LogRecord logStorage = FileLogRecordStorage.create(new File(rootDir, "logs")); +``` -### Writing data +### Storing data -The data is written in the disk by "ToDisk" exporters, these are exporters that serialize and store the data as received by their processors. If for some reason -the "ToDisk" cannot store data in the disk, they'll delegate the data to their wrapped exporter. +While you could manually call your `SignalStorage.write(items)` function, disk buffering +provides convenience exporters that you can use in your OpenTelemetry's instance, so +that all signals are automatically stored as they are created. -```java -// Creating the SpanExporter of our choice. -SpanExporter mySpanExporter = OtlpGrpcSpanExporter.getDefault(); +* For a span storage, use + a [SpanToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java). +* For a log storage, use + a [LogRecordToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java). +* For a metric storage, use + a [MetricToDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java). -// Wrapping our exporter with its "ToDisk" exporter. -SpanToDiskExporter toDiskExporter = SpanToDiskExporter.create(mySpanExporter, StorageConfiguration.getDefault(new File("/my/signals/cache/dir"))); +Each will wrap a signal storage for its respective signal type, as well as an optional callback +to notify when it succeeds, fails, and gets shutdown. - // Registering the disk exporter within our OpenTelemetry instance. -SdkTracerProvider myTraceProvider = SdkTracerProvider.builder() - .addSpanProcessor(SimpleSpanProcessor.create(toDiskExporter)) +```java +// Setting up span to disk exporter +SpanToDiskExporter spanToDiskExporter = + SpanToDiskExporter.builder(spanStorage).setExporterCallback(spanCallback).build(); +// Setting up metric to disk +MetricToDiskExporter metricToDiskExporter = + MetricToDiskExporter.builder(metricStorage).setExporterCallback(metricCallback).build(); +// Setting up log to disk exporter +LogRecordToDiskExporter logToDiskExporter = + LogRecordToDiskExporter.builder(logStorage).setExporterCallback(logCallback).build(); + +// Using exporters in your OpenTelemetry instance. +OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder() + // Using span to disk exporter + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor.builder(spanToDiskExporter).build()) + .build()) + // Using log to disk exporter + .setLoggerProvider( + SdkLoggerProvider.builder() + .addLogRecordProcessor( + BatchLogRecordProcessor.builder(logToDiskExporter).build()) + .build()) + // Using metric to disk exporter + .setMeterProvider( + SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.create(metricToDiskExporter)) + .build()) .build(); -OpenTelemetrySdk.builder() - .setTracerProvider(myTraceProvider) - .buildAndRegisterGlobal(); - ``` +Now when creating signals using your `OpenTelemetry` instance, those will get stored in disk. + ### Reading data -In order to read data, we need to create "FromDisk" exporters, which read data from the disk, parse it and delegate it -to their wrapped exporters. +In order to read data, we can iterate through our signal storage objects and then forward them to +a network exporter, as shown in the example for spans below. ```java -try { - SpanFromDiskExporter fromDiskExporter = SpanFromDiskExporter.create(memorySpanExporter, storageConfig); - if(fromDiskExporter.exportStoredBatch(1, TimeUnit.SECONDS)) { - // A batch was successfully exported and removed from disk. You can call this method for as long as it keeps returning true. - } else { - // Either there was no data in the disk or the wrapped exporter returned CompletableResultCode.ofFailure(). - } -} catch (IOException e) { - // Something unexpected happened. +// Example of reading an exporting spans from disk +OtlpHttpSpanExporter networkExporter; +Iterator> spanCollections = spanStorage.iterator(); +while(spanCollections.hasNext()){ + networkExporter.export(spanCollections.next()); } ``` +The `File*Storage` iterators delete the previously returned collection when `next()` is called, +assuming that if the next collection is requested is because the previous one was successfully +consumed. + Both the writing and reading processes can run in parallel and they don't overlap because each is supposed to happen in different files. We ensure that reader and writer don't -accidentally meet in the same file by using the configurable parameters. These parameters set non-overlapping time frames for each action to be done on a single file at a time. On top of that, there's a mechanism in -place to avoid overlapping on edge cases where the time frames ended but the resources haven't been -released. For that mechanism to work properly, this tool assumes that both the reading and the -writing actions are executed within the same application process. +accidentally meet in the same file by using the configurable parameters. These parameters set +non-overlapping time frames for each action to be done on a single file at a time. On top of that, +there's a mechanism in place to avoid overlapping on edge cases where the time frames ended but the +resources haven't been released. For that mechanism to work properly, this tool assumes that both +the reading and the writing actions are executed within the same application process. ## Component owners From cfb818e1e3279e22df38ff0c86c21f2cd1bf25a4 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:41:48 +0200 Subject: [PATCH 18/30] Updating README.md --- disk-buffering/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/disk-buffering/README.md b/disk-buffering/README.md index f4f12a2da..9178794ad 100644 --- a/disk-buffering/README.md +++ b/disk-buffering/README.md @@ -19,8 +19,7 @@ The default implementations are the following: ### Set up We need to create a signal storage object per signal type to start writing signals to disk. Each -`File*Storage` implementation -has a `create()` function that receives: +`File*Storage` implementation has a `create()` function that receives: * A File directory to store the signal files. Note that each signal storage object must have a dedicated directory to work properly. From ac23620ba9da879baf2e06af4bfad0b28f5d253c Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:44:54 +0200 Subject: [PATCH 19/30] Spotless --- .../disk/buffering/internal/storage/StorageIterator.java | 5 +++++ .../disk/buffering/storage/impl/FileLogRecordStorage.java | 5 +++++ .../disk/buffering/storage/impl/FileMetricStorage.java | 5 +++++ .../contrib/disk/buffering/storage/impl/FileSpanStorage.java | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java index f9a676da4..871238b03 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.disk.buffering.internal.storage; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java index 2a65deb99..71d5d884b 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.disk.buffering.storage.impl; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java index 263a0b6e3..8f8b41508 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.disk.buffering.storage.impl; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java index ee7c8f71c..b8cfa8996 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.disk.buffering.storage.impl; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; From ab424be41628357cd4dfd2d994afaa05e3d3a595 Mon Sep 17 00:00:00 2001 From: otelbot <197425009+otelbot@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:57:37 +0000 Subject: [PATCH 20/30] ./gradlew spotlessApply --- .../disk/buffering/internal/storage/StorageIterator.java | 5 +++++ .../disk/buffering/storage/impl/FileLogRecordStorage.java | 5 +++++ .../disk/buffering/storage/impl/FileMetricStorage.java | 5 +++++ .../contrib/disk/buffering/storage/impl/FileSpanStorage.java | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java index f9a676da4..871238b03 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/StorageIterator.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.disk.buffering.internal.storage; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java index 2a65deb99..71d5d884b 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileLogRecordStorage.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.disk.buffering.storage.impl; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java index 263a0b6e3..8f8b41508 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileMetricStorage.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.disk.buffering.storage.impl; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java index ee7c8f71c..b8cfa8996 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileSpanStorage.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.contrib.disk.buffering.storage.impl; import io.opentelemetry.contrib.disk.buffering.internal.serialization.deserializers.SignalDeserializer; From 0430c40966384a7a679bd7b748c6168a4acdfb14 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:44:37 +0200 Subject: [PATCH 21/30] Updating internal log levels --- .../internal/storage/FileSignalStorage.java | 2 +- .../disk/buffering/internal/storage/Storage.java | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java index 5da39a300..a81ca9078 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java @@ -48,7 +48,7 @@ public CompletableFuture write(Collection items) { if (storage.write(serializer)) { return CompletableFuture.completedFuture(WriteResult.successful()); } - logger.info("Could not store batch in disk."); + logger.fine("Could not store batch in disk."); return CompletableFuture.completedFuture(WriteResult.error(null)); } catch (IOException e) { logger.log( diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java index 7add97eac..6b2ee05f7 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java @@ -49,7 +49,7 @@ public boolean write(SignalSerializer marshaler) throws IOException { private boolean write(SignalSerializer marshaler, int attemptNumber) throws IOException { if (isClosed.get()) { - logger.info("Refusing to write to storage after being closed."); + logger.fine("Refusing to write to storage after being closed."); return false; } if (attemptNumber > MAX_ATTEMPTS) { @@ -60,7 +60,7 @@ private boolean write(SignalSerializer marshaler, int attemptNumber) throws I if (writableFile == null) { writableFile = folderManager.createWritableFile(); writableFileRef.set(writableFile); - logger.info("Created new writableFile: " + writableFile); + logger.finer("Created new writableFile: " + writableFile); } WritableResult result = writableFile.append(marshaler); if (result != WritableResult.SUCCEEDED) { @@ -98,7 +98,7 @@ public ReadableResult readNext(SignalDeserializer deserializer) throws IOE private ReadableResult doReadNext(SignalDeserializer deserializer, int attemptNumber) throws IOException { if (isClosed.get()) { - logger.info("Refusing to read from storage after being closed."); + logger.fine("Refusing to read from storage after being closed."); return null; } if (attemptNumber > MAX_ATTEMPTS) { @@ -107,16 +107,16 @@ private ReadableResult doReadNext(SignalDeserializer deserializer, int att } ReadableFile readableFile = readableFileRef.get(); if (readableFile == null) { - logger.info("Obtaining a new readableFile from the folderManager."); + logger.finer("Obtaining a new readableFile from the folderManager."); readableFile = folderManager.getReadableFile(); readableFileRef.set(readableFile); if (readableFile == null) { - logger.info("Unable to get or create readable file."); + logger.fine("Unable to get or create readable file."); return null; } } - logger.info("Attempting to read data from " + readableFile); + logger.finer("Attempting to read data from " + readableFile); byte[] result = readableFile.readNext(); if (result != null) { try { @@ -144,7 +144,7 @@ public boolean isClosed() { @Override public void close() throws IOException { - logger.info("Closing disk buffering storage."); + logger.fine("Closing disk buffering storage."); if (isClosed.compareAndSet(false, true)) { folderManager.close(); writableFileRef.set(null); From a5fe36a1f4a57d1198294e6568a6ef99551b9772 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:47:57 +0200 Subject: [PATCH 22/30] Updating tests --- .../buffering/internal/storage/FolderManagerTest.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java index b074c1e34..e0eac3333 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java @@ -58,14 +58,11 @@ void createWritableFile_withTimeMillisAsName() throws IOException { @Test void clearFiles() throws IOException { - when(clock.now()) - .thenReturn(MILLISECONDS.toNanos(1000L)) - .thenReturn(MILLISECONDS.toNanos(2000L)); + when(clock.now()).thenReturn(MILLISECONDS.toNanos(1000L)); - // Creating 2 files - folderManager.createWritableFile(); + // Creating file folderManager.createWritableFile(); - assertThat(rootDir.list()).containsExactlyInAnyOrder("1000", "2000"); + assertThat(rootDir.list()).containsExactly("1000"); // Clear folderManager.clear(); From a71cf306e6d3de992689cd927ef24285dc6506ef Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:26:45 +0200 Subject: [PATCH 23/30] Updates after merging from main --- .../contrib/disk/buffering/exporters/SpanToDiskExporter.java | 1 - .../disk/buffering/storage/impl/FileStorageConfiguration.java | 1 - 2 files changed, 2 deletions(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java index 4cc5c09df..c68b767e7 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java @@ -17,7 +17,6 @@ /** Exporter that stores spans into disk. */ public final class SpanToDiskExporter implements SpanExporter { - private static final SignalType TYPE = SignalType.SPAN; private final SignalStorageExporter storageExporter; private final ExporterCallback callback; diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileStorageConfiguration.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileStorageConfiguration.java index f95c72c70..0a34c12b4 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileStorageConfiguration.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/storage/impl/FileStorageConfiguration.java @@ -9,7 +9,6 @@ import static java.util.concurrent.TimeUnit.SECONDS; import com.google.auto.value.AutoValue; -import java.io.File; /** Defines how the storage should be managed. */ @AutoValue From 7cf31f4eec9155f26ebf4171e701abb980033200 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:29:27 +0200 Subject: [PATCH 24/30] Update after main changes --- .../opentelemetry/contrib/disk/buffering/IntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java index 9f3ecded0..fe907d528 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java @@ -49,7 +49,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class IntegrationTest { +class IntegrationTest { private Tracer tracer; private SdkMeterProvider meterProvider; private Meter meter; From 0d8b6fa0782cd213f67a8eb5b106604106add290 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:33:59 +0200 Subject: [PATCH 25/30] Storing default export timeout in constants --- .../disk/buffering/exporters/LogRecordToDiskExporter.java | 3 ++- .../disk/buffering/exporters/MetricToDiskExporter.java | 3 ++- .../contrib/disk/buffering/exporters/SpanToDiskExporter.java | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java index 3d6d62bad..2fcd0ca5a 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java @@ -19,6 +19,7 @@ public final class LogRecordToDiskExporter implements LogRecordExporter { private final SignalStorageExporter storageExporter; private final ExporterCallback callback; + private static final Duration DEFAULT_EXPORT_TIMEOUT = Duration.ofSeconds(10); private LogRecordToDiskExporter( SignalStorageExporter storageExporter, @@ -50,7 +51,7 @@ public CompletableResultCode shutdown() { public static final class Builder { private final SignalStorage.LogRecord storage; private ExporterCallback callback = ExporterCallback.noop(); - private Duration writeTimeout = Duration.ofSeconds(10); + private Duration writeTimeout = DEFAULT_EXPORT_TIMEOUT; @CanIgnoreReturnValue public Builder setExporterCallback(ExporterCallback value) { diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java index 27132cc28..a20981ad0 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java @@ -23,6 +23,7 @@ public final class MetricToDiskExporter implements MetricExporter { private final SignalStorageExporter storageExporter; private final AggregationTemporalitySelector aggregationTemporalitySelector; private final ExporterCallback callback; + private static final Duration DEFAULT_EXPORT_TIMEOUT = Duration.ofSeconds(10); private MetricToDiskExporter( SignalStorageExporter storageExporter, @@ -63,7 +64,7 @@ public static final class Builder { private AggregationTemporalitySelector aggregationTemporalitySelector = AggregationTemporalitySelector.alwaysCumulative(); private ExporterCallback callback = ExporterCallback.noop(); - private Duration writeTimeout = Duration.ofSeconds(10); + private Duration writeTimeout = DEFAULT_EXPORT_TIMEOUT; @CanIgnoreReturnValue public Builder setExporterCallback(ExporterCallback value) { diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java index c68b767e7..4a68623b2 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java @@ -17,9 +17,9 @@ /** Exporter that stores spans into disk. */ public final class SpanToDiskExporter implements SpanExporter { - private final SignalStorageExporter storageExporter; private final ExporterCallback callback; + private static final Duration DEFAULT_EXPORT_TIMEOUT = Duration.ofSeconds(10); private SpanToDiskExporter( SignalStorageExporter storageExporter, ExporterCallback callback) { @@ -50,7 +50,7 @@ public CompletableResultCode shutdown() { public static final class Builder { private final SignalStorage.Span storage; private ExporterCallback callback = ExporterCallback.noop(); - private Duration writeTimeout = Duration.ofSeconds(10); + private Duration writeTimeout = DEFAULT_EXPORT_TIMEOUT; private Builder(SignalStorage.Span storage) { this.storage = storage; From 4e48340b2f05a55ea38f3a5e2694e8b0ae065e10 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:40:02 +0200 Subject: [PATCH 26/30] Moving default callback to constants --- .../disk/buffering/exporters/LogRecordToDiskExporter.java | 5 ++++- .../disk/buffering/exporters/MetricToDiskExporter.java | 4 +++- .../contrib/disk/buffering/exporters/SpanToDiskExporter.java | 4 +++- .../disk/buffering/exporters/callback/ExporterCallback.java | 4 ---- .../buffering/exporters/callback/NoopExporterCallback.java | 4 +--- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java index 2fcd0ca5a..6ed7ae2b4 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/LogRecordToDiskExporter.java @@ -7,6 +7,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; +import io.opentelemetry.contrib.disk.buffering.exporters.callback.NoopExporterCallback; import io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.sdk.common.CompletableResultCode; @@ -19,6 +20,8 @@ public final class LogRecordToDiskExporter implements LogRecordExporter { private final SignalStorageExporter storageExporter; private final ExporterCallback callback; + private static final ExporterCallback DEFAULT_CALLBACK = + new NoopExporterCallback<>(); private static final Duration DEFAULT_EXPORT_TIMEOUT = Duration.ofSeconds(10); private LogRecordToDiskExporter( @@ -50,7 +53,7 @@ public CompletableResultCode shutdown() { public static final class Builder { private final SignalStorage.LogRecord storage; - private ExporterCallback callback = ExporterCallback.noop(); + private ExporterCallback callback = DEFAULT_CALLBACK; private Duration writeTimeout = DEFAULT_EXPORT_TIMEOUT; @CanIgnoreReturnValue diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java index a20981ad0..fe7a86abf 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/MetricToDiskExporter.java @@ -7,6 +7,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; +import io.opentelemetry.contrib.disk.buffering.exporters.callback.NoopExporterCallback; import io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.sdk.common.CompletableResultCode; @@ -23,6 +24,7 @@ public final class MetricToDiskExporter implements MetricExporter { private final SignalStorageExporter storageExporter; private final AggregationTemporalitySelector aggregationTemporalitySelector; private final ExporterCallback callback; + private static final ExporterCallback DEFAULT_CALLBACK = new NoopExporterCallback<>(); private static final Duration DEFAULT_EXPORT_TIMEOUT = Duration.ofSeconds(10); private MetricToDiskExporter( @@ -63,7 +65,7 @@ public static final class Builder { private final SignalStorage.Metric storage; private AggregationTemporalitySelector aggregationTemporalitySelector = AggregationTemporalitySelector.alwaysCumulative(); - private ExporterCallback callback = ExporterCallback.noop(); + private ExporterCallback callback = DEFAULT_CALLBACK; private Duration writeTimeout = DEFAULT_EXPORT_TIMEOUT; @CanIgnoreReturnValue diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java index 4a68623b2..9558a2767 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/SpanToDiskExporter.java @@ -7,6 +7,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.contrib.disk.buffering.exporters.callback.ExporterCallback; +import io.opentelemetry.contrib.disk.buffering.exporters.callback.NoopExporterCallback; import io.opentelemetry.contrib.disk.buffering.internal.exporters.SignalStorageExporter; import io.opentelemetry.contrib.disk.buffering.storage.SignalStorage; import io.opentelemetry.sdk.common.CompletableResultCode; @@ -19,6 +20,7 @@ public final class SpanToDiskExporter implements SpanExporter { private final SignalStorageExporter storageExporter; private final ExporterCallback callback; + private static final ExporterCallback DEFAULT_CALLBACK = new NoopExporterCallback<>(); private static final Duration DEFAULT_EXPORT_TIMEOUT = Duration.ofSeconds(10); private SpanToDiskExporter( @@ -49,7 +51,7 @@ public CompletableResultCode shutdown() { public static final class Builder { private final SignalStorage.Span storage; - private ExporterCallback callback = ExporterCallback.noop(); + private ExporterCallback callback = DEFAULT_CALLBACK; private Duration writeTimeout = DEFAULT_EXPORT_TIMEOUT; private Builder(SignalStorage.Span storage) { diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java index 4ed98e99e..9c3c816ea 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/ExporterCallback.java @@ -27,8 +27,4 @@ public interface ExporterCallback { /** Called when the exporter is closed. */ void onShutdown(); - - static ExporterCallback noop() { - return new NoopExporterCallback<>(); - } } diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java index 9d89aaedc..6313d1a5b 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/exporters/callback/NoopExporterCallback.java @@ -8,9 +8,7 @@ import java.util.Collection; import javax.annotation.Nullable; -final class NoopExporterCallback implements ExporterCallback { - - NoopExporterCallback() {} +public final class NoopExporterCallback implements ExporterCallback { @Override public void onExportSuccess(Collection items) {} From 61801979a0d81cacd0fc7b1c6be0c48cf2ca578a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar?= <56847527+LikeTheSalad@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:57:59 +0200 Subject: [PATCH 27/30] Update disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java Co-authored-by: jason plumb <75337021+breedx-splk@users.noreply.github.com> --- .../storage/files/reader/DelimitedProtoStreamReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java index 81e31ef88..1351622b2 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java @@ -25,7 +25,7 @@ public byte[] readNext() throws IOException { return null; } byte[] bytes = new byte[itemSize]; - if (inputStream.read(bytes) < 0) { + if (inputStream.read(bytes) <= 0) { return null; } return bytes; From da689c76362474e75020da192a601edd5a7be4a7 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:37:27 +0200 Subject: [PATCH 28/30] Improving inputStream reading based on PR comment --- .../files/reader/DelimitedProtoStreamReader.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java index 1351622b2..d638c118b 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/reader/DelimitedProtoStreamReader.java @@ -25,8 +25,14 @@ public byte[] readNext() throws IOException { return null; } byte[] bytes = new byte[itemSize]; - if (inputStream.read(bytes) <= 0) { - return null; + int offset = 0; + int readCt; + do { + readCt = inputStream.read(bytes, offset, itemSize - offset); + offset += readCt; + } while (readCt != -1 && offset < itemSize); + if (offset != itemSize) { + return null; // unable to read the whole item correctly } return bytes; } From c39e2a1f9ebc504cdad1f85402ae9de73c7f33bc Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Fri, 19 Sep 2025 09:23:53 +0200 Subject: [PATCH 29/30] Adjusting tests after updating from main --- .../disk/buffering/IntegrationTest.java | 8 ++--- .../internal/storage/FolderManagerTest.java | 1 - .../storage/files/ReadableFileTest.java | 29 +++++++++---------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java index fe907d528..d30466430 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/IntegrationTest.java @@ -6,7 +6,7 @@ package io.opentelemetry.contrib.disk.buffering; import static java.lang.Thread.sleep; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -147,9 +147,9 @@ void verifyIntegration() throws InterruptedException { logStorage.forEach(storedLogs::addAll); metricStorage.forEach(storedMetrics::addAll); - assertEquals(1, storedSpans.size()); - assertEquals(1, storedLogs.size()); - assertEquals(1, storedMetrics.size()); + assertThat(storedSpans).hasSize(1); + assertThat(storedLogs).hasSize(1); + assertThat(storedMetrics).hasSize(1); } private static SdkTracerProvider createTracerProvider(SpanExporter exporter) { diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java index e1680466e..044e7be9b 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManagerTest.java @@ -10,7 +10,6 @@ import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.MIN_FILE_AGE_FOR_READ_MILLIS; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java index b409565d2..791d80faa 100644 --- a/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java +++ b/disk-buffering/src/test/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFileTest.java @@ -11,10 +11,7 @@ import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.THIRD_LOG_RECORD; import static io.opentelemetry.contrib.disk.buffering.internal.storage.TestData.getConfiguration; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -72,22 +69,22 @@ private static void addFileContents(File source) throws IOException { @Test void readAndRemoveItems() throws IOException { - assertEquals(FIRST_LOG_RECORD, deserialize(readableFile.readNext())); + assertThat(FIRST_LOG_RECORD).isEqualTo(deserialize(readableFile.readNext())); readableFile.removeTopItem(); List logs = getRemainingDataAndClose(readableFile); - assertEquals(2, logs.size()); - assertEquals(SECOND_LOG_RECORD, logs.get(0)); - assertEquals(THIRD_LOG_RECORD, logs.get(1)); + assertThat(2).isEqualTo(logs.size()); + assertThat(SECOND_LOG_RECORD).isEqualTo(logs.get(0)); + assertThat(THIRD_LOG_RECORD).isEqualTo(logs.get(1)); } @Test void whenReadingLastLine_deleteOriginalFile_and_close() throws IOException { getRemainingDataAndClose(readableFile); - assertFalse(source.exists()); - assertTrue(readableFile.isClosed()); + assertThat(source.exists()).isFalse(); + assertThat(readableFile.isClosed()).isTrue(); } @Test @@ -101,10 +98,10 @@ void whenNoMoreLinesAvailableToRead_deleteOriginalFile_close_and_returnNoContent ReadableFile emptyReadableFile = new ReadableFile(emptyFile, CREATED_TIME_MILLIS, clock, getConfiguration()); - assertNull(emptyReadableFile.readNext()); + assertThat(emptyReadableFile.readNext()).isNull(); - assertTrue(emptyReadableFile.isClosed()); - assertFalse(emptyFile.exists()); + assertThat(emptyReadableFile.isClosed()).isTrue(); + assertThat(emptyFile.exists()).isFalse(); } @Test @@ -112,15 +109,15 @@ void whenReadingAfterTheConfiguredReadingTimeExpired_close() throws IOException when(clock.now()) .thenReturn(MILLISECONDS.toNanos(CREATED_TIME_MILLIS + MAX_FILE_AGE_FOR_READ_MILLIS)); - assertNull(readableFile.readNext()); - assertTrue(readableFile.isClosed()); + assertThat(readableFile.readNext()).isNull(); + assertThat(readableFile.isClosed()).isTrue(); } @Test void whenReadingAfterClosed_returnNull() throws IOException { readableFile.close(); - assertNull(readableFile.readNext()); + assertThat(readableFile.readNext()).isNull(); } private static List getRemainingDataAndClose(ReadableFile readableFile) From f4999acda93add271f2e72be3352d9a482cf8117 Mon Sep 17 00:00:00 2001 From: Cesar Munoz <56847527+LikeTheSalad@users.noreply.github.com> Date: Fri, 19 Sep 2025 09:32:31 +0200 Subject: [PATCH 30/30] Addressing PR comments --- .../disk/buffering/internal/storage/FileSignalStorage.java | 3 ++- .../contrib/disk/buffering/internal/storage/FolderManager.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java index a81ca9078..743dee757 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FileSignalStorage.java @@ -49,7 +49,8 @@ public CompletableFuture write(Collection items) { return CompletableFuture.completedFuture(WriteResult.successful()); } logger.fine("Could not store batch in disk."); - return CompletableFuture.completedFuture(WriteResult.error(null)); + return CompletableFuture.completedFuture( + WriteResult.error(new Exception("Could not store batch in disk for an unknown reason."))); } catch (IOException e) { logger.log( Level.WARNING, diff --git a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java index 605bc56ed..d93751fa1 100644 --- a/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java +++ b/disk-buffering/src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java @@ -31,7 +31,8 @@ public static FolderManager create( File destinationDir, FileStorageConfiguration configuration, Clock clock) { if (destinationDir.isFile()) { throw new IllegalArgumentException("destinationDir must be a directory"); - } else if (!destinationDir.exists()) { + } + if (!destinationDir.exists()) { if (!destinationDir.mkdirs()) { throw new IllegalStateException("Could not create dir: " + destinationDir); }