Skip to content

Commit 2b8888d

Browse files
LikeTheSaladzeitlingerbreedx-splkjack-berg
authored
Signal disk buffering (#913)
Co-authored-by: Gregor Zeitlinger <[email protected]> Co-authored-by: jason plumb <[email protected]> Co-authored-by: jack-berg <[email protected]>
1 parent 3623f98 commit 2b8888d

File tree

68 files changed

+6661
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+6661
-0
lines changed

.github/component_owners.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ components:
2222
consistent-sampling:
2323
- oertl
2424
- PeterF778
25+
disk-buffering:
26+
- LikeTheSalad
27+
- zeitlinger
2528
samplers:
2629
- iNikem
2730
- trask

disk-buffering/CONTRIBUTING.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Contributor Guide
2+
3+
Each one of the three exporters provided by this
4+
tool ([LogRecordDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordDiskExporter.java), [MetricDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/MetricDiskExporter.java)
5+
and [SpanDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/SpanDiskExporter.java))
6+
is responsible of performing 2 actions, `write` and `read/delegate`, the `write` one happens
7+
automatically as a set of signals are provided from the processor, while the `read/delegate` one has
8+
to be triggered manually by the consumer of this library as explained in the [README](README.md).
9+
10+
## Writing overview
11+
12+
![Writing flow](assets/writing-flow.png)
13+
14+
* The writing process happens automatically within its `export(Collection<SignalData> signals)`
15+
method, which is called by the configured signal processor.
16+
* When a set of signals is received, these are delegated over to
17+
the [DiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/exporters/DiskExporter.java)
18+
class which then serializes them using an implementation
19+
of [SignalSerializer](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/serialization/serializers/SignalSerializer.java)
20+
and then the serialized data is appended into a File using an instance of
21+
the [Storage](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java)
22+
class.
23+
* The data is written into a file directly, without the use of a buffer, to make sure no data gets
24+
lost in case the application ends unexpectedly.
25+
* Each disk exporter stores its signals in its own folder, which is expected to contain files
26+
that belong to that type of signal only.
27+
* Each file may contain more than a batch of signals if the configuration parameters allow enough
28+
limit size for it.
29+
* If the configured folder size for the signals has been reached and a new file is needed to be
30+
created to keep storing new data, the oldest available file will be removed to make space for the
31+
new one.
32+
* The [Storage](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java),
33+
[FolderManager](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java)
34+
and [WritableFile](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/WritableFile.java)
35+
files contain more information on the details of the writing process into a file.
36+
37+
## Reading overview
38+
39+
![Reading flow](assets/reading-flow.png)
40+
41+
* The reading process has to be triggered manually by the library consumer as explained in
42+
the [README](README.md).
43+
* A single file is read at a time and updated to remove the data gathered from it after it is
44+
successfully exported, until it's emptied. Each file previously created during the
45+
writing process has a timestamp in milliseconds, which is used to determine what file to start
46+
reading from, which will be the oldest one available.
47+
* If the oldest file available is stale, which is determined based on the configuration provided at
48+
the time of creating the disk exporter, then it will be ignored, and the next oldest (and
49+
unexpired) one will be used instead.
50+
* All the stale and empty files will be removed as a new file is created.
51+
* The [Storage](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/Storage.java),
52+
[FolderManager](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/FolderManager.java)
53+
and [ReadableFile](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/storage/files/ReadableFile.java)
54+
files contain more information on the details of the file reading process.
55+
* Note that the reader delegates the data to the exporter exactly in the way it has received the
56+
data - it does not try to batch data (but this could be an optimization in the future).

disk-buffering/README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Disk buffering
2+
3+
This module provides signal exporter wrappers that intercept and store signals in files which can be
4+
sent later on demand. A high level description of how it works is that there are two separate
5+
processes in place, one for writing data in disk, and one for reading/exporting the previously
6+
stored data.
7+
8+
* Each exporter stores the received data automatically in disk right after it's received from its
9+
processor.
10+
* The reading of the data back from disk and exporting process has to be done manually. At
11+
the moment there's no automatic mechanism to do so. There's more information on it can be
12+
achieved, under [Reading data](#reading-data).
13+
14+
> For a more detailed information on how the whole process works, take a look at
15+
> the [CONTRIBUTING](CONTRIBUTING.md) file.
16+
17+
## Configuration
18+
19+
The configurable parameters are provided **per exporter**, the available ones are:
20+
21+
* Max file size, defaults to 1MB.
22+
* Max folder size, defaults to 10MB. All files are stored in a single folder per-signal, therefore
23+
if all 3 types of signals are stored, the total amount of space from disk to be taken by default
24+
would be of 30MB.
25+
* Max age for file writing, defaults to 30 seconds.
26+
* Min age for file reading, defaults to 33 seconds. It must be greater that the max age for file
27+
writing.
28+
* Max age for file reading, defaults to 18 hours. After that time passes, the file will be
29+
considered stale and will be removed when new files are created. No more data will be read from a
30+
file past this time.
31+
* An instance
32+
of [TemporaryFileProvider](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/files/TemporaryFileProvider.java),
33+
defaults to calling `File.createTempFile`. This provider will be used when reading from the disk
34+
in order create a temporary file from which each line (batch of signals) will be read and
35+
sequentially get removed from the original cache file right after the data has been successfully
36+
exported.
37+
38+
## Usage
39+
40+
### Storing data
41+
42+
In order to use it, you need to wrap your own exporter with a new instance of
43+
the ones provided in here:
44+
45+
* For a LogRecordExporter, it must be wrapped within
46+
a [LogRecordDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/LogRecordDiskExporter.java).
47+
* For a MetricExporter, it must be wrapped within
48+
a [MetricDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/MetricDiskExporter.java).
49+
* For a SpanExporter, it must be wrapped within
50+
a [SpanDiskExporter](src/main/java/io/opentelemetry/contrib/disk/buffering/SpanDiskExporter.java).
51+
52+
Each wrapper will need the following when instantiating them:
53+
54+
* The exporter to be wrapped.
55+
* A File instance of the root directory where all the data is going to be written. The same root dir
56+
can be used for all the wrappers, since each will create their own folder inside it.
57+
* An instance
58+
of [StorageConfiguration](src/main/java/io/opentelemetry/contrib/disk/buffering/internal/StorageConfiguration.java)
59+
with the desired parameters. You can create one with default values by
60+
calling `StorageConfiguration.getDefault()`.
61+
62+
After wrapping your exporters, you must register the wrapper as the exporter you'll use. It will
63+
take care of always storing the data it receives.
64+
65+
#### Set up example for spans
66+
67+
```java
68+
// Creating the SpanExporter of our choice.
69+
SpanExporter mySpanExporter = OtlpGrpcSpanExporter.getDefault();
70+
71+
// Wrapping our exporter with its disk exporter.
72+
SpanDiskExporter diskExporter = SpanDiskExporter.create(mySpanExporter, new File("/my/signals/cache/dir"), StorageConfiguration.getDefault());
73+
74+
// Registering the disk exporter within our OpenTelemetry instance.
75+
SdkTracerProvider myTraceProvider = SdkTracerProvider.builder()
76+
.addSpanProcessor(SimpleSpanProcessor.create(diskExporter))
77+
.build();
78+
OpenTelemetrySdk.builder()
79+
.setTracerProvider(myTraceProvider)
80+
.buildAndRegisterGlobal();
81+
82+
```
83+
84+
### Reading data
85+
86+
Each of the exporter wrappers can read from the disk and send the retrieved data over to their
87+
wrapped exporter by calling this method from them:
88+
89+
```java
90+
try {
91+
if(diskExporter.exportStoredBatch(1, TimeUnit.SECONDS)) {
92+
// A batch was successfully exported and removed from disk. You can call this method for as long as it keeps returning true.
93+
} else {
94+
// Either there was no data in the disk or the wrapped exporter returned CompletableResultCode.ofFailure().
95+
}
96+
} catch (IOException e) {
97+
// Something unexpected happened.
98+
}
99+
```
100+
101+
Both the writing and reading processes can run in parallel and they don't overlap
102+
because each is supposed to happen in different files. We ensure that reader and writer don't
103+
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
104+
place to avoid overlapping on edge cases where the time frames ended but the resources haven't been
105+
released. For that mechanism to work properly, this tool assumes that both the reading and the
106+
writing actions are executed within the same application process.
107+
108+
## Component owners
109+
110+
- [Cesar Munoz](https://github.com/LikeTheSalad), Elastic
111+
- [Gregor Zeitlinger](https://github.com/zeitlinger), Grafana
112+
113+
Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).
117 KB
Loading
72.7 KB
Loading

disk-buffering/build.gradle.kts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import ru.vyarus.gradle.plugin.animalsniffer.AnimalSniffer
2+
3+
plugins {
4+
id("otel.java-conventions")
5+
id("otel.publish-conventions")
6+
id("me.champeau.jmh") version "0.7.1"
7+
id("ru.vyarus.animalsniffer") version "1.7.1"
8+
}
9+
10+
description = "Exporter implementations that store signals on disk"
11+
otelJava.moduleName.set("io.opentelemetry.contrib.exporters.disk")
12+
13+
java {
14+
sourceCompatibility = JavaVersion.VERSION_1_8
15+
targetCompatibility = JavaVersion.VERSION_1_8
16+
}
17+
18+
val autovalueVersion = "1.10.1"
19+
dependencies {
20+
api("io.opentelemetry:opentelemetry-sdk")
21+
implementation("io.opentelemetry:opentelemetry-exporter-otlp-common")
22+
implementation("io.opentelemetry.proto:opentelemetry-proto:0.20.0-alpha")
23+
compileOnly("com.google.auto.value:auto-value-annotations:$autovalueVersion")
24+
annotationProcessor("com.google.auto.value:auto-value:$autovalueVersion")
25+
signature("com.toasttab.android:gummy-bears-api-24:0.5.1@signature")
26+
testImplementation("org.mockito:mockito-inline:4.11.0")
27+
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
28+
}
29+
30+
animalsniffer {
31+
sourceSets = listOf(java.sourceSets.main.get())
32+
}
33+
34+
// Always having declared output makes this task properly participate in tasks up-to-date checks
35+
tasks.withType<AnimalSniffer> {
36+
reports.text.required.set(true)
37+
}
38+
39+
// Attaching animalsniffer check to the compilation process.
40+
tasks.named("classes").configure {
41+
finalizedBy("animalsnifferMain")
42+
}
43+
44+
jmh {
45+
warmupIterations.set(0)
46+
fork.set(2)
47+
iterations.set(5)
48+
timeOnIteration.set("5s")
49+
timeUnit.set("ms")
50+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.disk.buffering.internal.files.utils;
7+
8+
import io.opentelemetry.contrib.disk.buffering.internal.storage.files.utils.FileTransferUtil;
9+
import java.io.File;
10+
import java.io.FileInputStream;
11+
import java.io.IOException;
12+
import java.nio.file.Files;
13+
import java.nio.file.StandardOpenOption;
14+
import org.openjdk.jmh.annotations.Benchmark;
15+
import org.openjdk.jmh.annotations.BenchmarkMode;
16+
import org.openjdk.jmh.annotations.Mode;
17+
import org.openjdk.jmh.annotations.Scope;
18+
import org.openjdk.jmh.annotations.Setup;
19+
import org.openjdk.jmh.annotations.State;
20+
import org.openjdk.jmh.annotations.TearDown;
21+
22+
public class FileTransferUtilBenchmark {
23+
24+
@Benchmark
25+
@BenchmarkMode(Mode.AverageTime)
26+
public void fileTransfer(FileTransferState state) throws IOException {
27+
state.fileTransferUtil.transferBytes(state.offset, state.amountOfBytesToTransfer);
28+
}
29+
30+
@State(Scope.Benchmark)
31+
public static class FileTransferState {
32+
public FileTransferUtil fileTransferUtil;
33+
public int offset;
34+
public int amountOfBytesToTransfer;
35+
private File inputFile;
36+
private File outputFile;
37+
38+
@Setup
39+
public void setUp() throws IOException {
40+
outputFile = File.createTempFile("output", ".txt");
41+
inputFile = File.createTempFile("input", ".txt");
42+
int totalDataSize = 1024 * 1024; // 1MB
43+
byte[] data = new byte[totalDataSize];
44+
Files.write(inputFile.toPath(), data, StandardOpenOption.CREATE);
45+
fileTransferUtil = new FileTransferUtil(new FileInputStream(inputFile), outputFile);
46+
offset = 512;
47+
amountOfBytesToTransfer = totalDataSize - offset;
48+
}
49+
50+
@TearDown
51+
public void tearDown() throws IOException {
52+
fileTransferUtil.close();
53+
inputFile.delete();
54+
outputFile.delete();
55+
}
56+
}
57+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.disk.buffering;
7+
8+
import io.opentelemetry.contrib.disk.buffering.internal.StorageConfiguration;
9+
import io.opentelemetry.contrib.disk.buffering.internal.exporters.DiskExporter;
10+
import io.opentelemetry.contrib.disk.buffering.internal.serialization.serializers.SignalSerializer;
11+
import io.opentelemetry.contrib.disk.buffering.internal.storage.utils.StorageClock;
12+
import io.opentelemetry.sdk.common.CompletableResultCode;
13+
import io.opentelemetry.sdk.logs.LogRecordProcessor;
14+
import io.opentelemetry.sdk.logs.data.LogRecordData;
15+
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
16+
import java.io.File;
17+
import java.io.IOException;
18+
import java.util.Collection;
19+
import java.util.concurrent.TimeUnit;
20+
21+
/**
22+
* This is a {@link LogRecordExporter} wrapper that takes care of intercepting all the signals sent
23+
* out to be exported, tries to store them in the disk in order to export them later.
24+
*
25+
* <p>In order to use it, you need to wrap your own {@link LogRecordExporter} with a new instance of
26+
* this one, which will be the one you need to register in your {@link LogRecordProcessor}.
27+
*/
28+
public final class LogRecordDiskExporter implements LogRecordExporter, StoredBatchExporter {
29+
private final LogRecordExporter wrapped;
30+
private final DiskExporter<LogRecordData> diskExporter;
31+
32+
/**
33+
* Creates a new instance of {@link LogRecordDiskExporter}.
34+
*
35+
* @param wrapped - The exporter where the data retrieved from the disk will be delegated to.
36+
* @param rootDir - The directory to create this signal's cache dir where all the data will be
37+
* written into.
38+
* @param configuration - How you want to manage the storage process.
39+
* @throws IOException If no dir can be created in rootDir.
40+
*/
41+
public static LogRecordDiskExporter create(
42+
LogRecordExporter wrapped, File rootDir, StorageConfiguration configuration)
43+
throws IOException {
44+
return create(wrapped, rootDir, configuration, StorageClock.getInstance());
45+
}
46+
47+
// This is used for testing purposes.
48+
static LogRecordDiskExporter create(
49+
LogRecordExporter wrapped,
50+
File rootDir,
51+
StorageConfiguration configuration,
52+
StorageClock clock)
53+
throws IOException {
54+
return new LogRecordDiskExporter(wrapped, rootDir, configuration, clock);
55+
}
56+
57+
private LogRecordDiskExporter(
58+
LogRecordExporter wrapped,
59+
File rootDir,
60+
StorageConfiguration configuration,
61+
StorageClock clock)
62+
throws IOException {
63+
this.wrapped = wrapped;
64+
diskExporter =
65+
new DiskExporter<>(
66+
rootDir, configuration, "logs", SignalSerializer.ofLogs(), wrapped::export, clock);
67+
}
68+
69+
@Override
70+
public CompletableResultCode export(Collection<LogRecordData> logs) {
71+
return diskExporter.onExport(logs);
72+
}
73+
74+
@Override
75+
public CompletableResultCode flush() {
76+
return CompletableResultCode.ofSuccess();
77+
}
78+
79+
@Override
80+
public CompletableResultCode shutdown() {
81+
try {
82+
diskExporter.onShutDown();
83+
} catch (IOException e) {
84+
return CompletableResultCode.ofFailure();
85+
} finally {
86+
wrapped.shutdown();
87+
}
88+
return CompletableResultCode.ofSuccess();
89+
}
90+
91+
@Override
92+
public boolean exportStoredBatch(long timeout, TimeUnit unit) throws IOException {
93+
return diskExporter.exportStoredBatch(timeout, unit);
94+
}
95+
}

0 commit comments

Comments
 (0)