Skip to content

Commit e2a8baa

Browse files
docs(exporter-chain): add exporter chain overview with default (#1243)
* docs(exporter-chain): add exporter chain overview with default * docs(exporter-chain): apply PR 1243 feedback
1 parent 8e97eb9 commit e2a8baa

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ Some of the additional features provided include:
7171

7272
Note: Use of these features is not yet well documented.
7373

74+
## Exporter Chain
75+
76+
The SDK performs asynchronous exporter initialization with an in-memory buffering layer and optional
77+
disk buffering for offline scenarios. See [Exporter Chain documentation](./docs/EXPORTER_CHAIN.md)
78+
for details, customization hooks, and ordering semantics.
79+
7480
# Contributing
7581

7682
See [CONTRIBUTING.md](CONTRIBUTING.md).

docs/EXPORTER_CHAIN.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Exporter Delegate Chain (Spans / Logs / Metrics)
2+
3+
Status: draft
4+
5+
This document explains the default exporter delegation chain used by the OpenTelemetry Android RUM SDK and how you can customize or extend it with your own exporter layers.
6+
7+
## Goals
8+
9+
1. Avoid ANRs and StrictMode violations during SDK initialization by deferring expensive exporter setup off the main thread.
10+
2. Ensure early telemetry (signals produced before exporters finish initializing) is not lost.
11+
3. Provide optional durable (disk) buffering for offline / startup scenarios.
12+
4. Allow applications to insert custom exporters (filtering, redaction, fan‑out, encryption, etc.).
13+
14+
## High-Level Flow
15+
16+
When you call `OpenTelemetryRum.builder()...build()`:
17+
18+
1. Lightweight in‑memory buffer exporters are installed immediately so spans, logs, and metrics can be accepted.
19+
2. Actual exporter initialization (logging / OTLP + optional disk storage wiring) runs asynchronously on a background executor.
20+
3. Once ready, buffered signals are flushed to the real exporters and all future exports are delegated directly.
21+
4. If disk buffering is enabled, signals flow through disk first, then are later read back and exported to the network exporter.
22+
23+
## Default Chain (No Custom Exporters, Disk Buffering Enabled)
24+
25+
For each signal type (Span / LogRecord / Metric):
26+
27+
```text
28+
BufferDelegating*Exporter --> *ToDiskExporter --> (Original default exporter)
29+
(in-memory buffer) (writes batch (e.g. LoggingSpanExporter,
30+
files to storage) SystemOutLogRecordExporter,
31+
LoggingMetricExporter)
32+
```text
33+
34+
Later, a periodic scheduler reads batches from disk and replays them on the original exporter via:
35+
36+
```text
37+
SignalFromDiskExporter -> *FromDiskExporter -> Original exporter
38+
```
39+
40+
(Where `*` stands for `Span`, `LogRecord`, or `Metric`.)
41+
42+
## Default Chain (Disk Buffering Disabled)
43+
44+
```text
45+
BufferDelegating*Exporter --> (Original default exporter)
46+
```
47+
48+
No disk layer is inserted and no scheduled reader is enabled.
49+
50+
## Components
51+
52+
### BufferDelegating[Span|Log|Metric]Exporter
53+
54+
Internal in‑memory temporary exporters created immediately. They hold up to 5,000 items (per signal type) produced before the real delegate is attached. If the buffer fills, additional items are dropped with a warning log (`The <type> buffer was filled before export delegate set...`). After `setDelegate(...)` is invoked, the buffered data is exported, optional pending `flush()` / `shutdown()` calls are honored, and the buffer is cleared. From that point on the exporter no longer buffers anything: every new signal is delegated straight through to the next exporter in the chain (*ToDiskExporter if disk buffering is enabled, otherwise the customized/base exporter).
55+
56+
Source examples:
57+
58+
* `core/src/main/java/io/opentelemetry/android/export/BufferDelegatingSpanExporter.kt`
59+
* `core/src/main/java/io/opentelemetry/android/export/BufferDelegatingLogExporter.kt`
60+
* `core/src/main/java/io/opentelemetry/android/export/BufferDelegatingMetricExporter.kt`
61+
62+
### *ToDiskExporter (Optional Layer)
63+
64+
Wrappers provided by `io.opentelemetry.contrib.disk.buffering` that persist batches to disk before forwarding to the underlying ("original") exporter. Inserted only when `DiskBufferingConfig.enabled == true`.
65+
66+
### Original Exporters
67+
68+
The base exporters produced by the builder if you don't customize them:
69+
70+
* Spans: `LoggingSpanExporter`
71+
* Logs: `SystemOutLogRecordExporter`
72+
* Metrics: `LoggingMetricExporter`
73+
74+
In real deployments you usually replace these with OTLP exporters (e.g. OTLP/HTTP) by supplying customizers or by configuring upstream dependencies providing them.
75+
76+
### SignalFromDiskExporter & *FromDiskExporter
77+
78+
A coordinator (`SignalFromDiskExporter`) plus per-signal readers that pull batches from disk (one stored batch per original write call) and export them to the original exporter. A scheduler periodically invokes these to drain the on-device queue.
79+
80+
## Asynchronous Initialization
81+
82+
Exporter initialization (including disk capacity checks and creating `Storage` directories) is executed on `AsyncTask.THREAD_POOL_EXECUTOR` to prevent main-thread stalls. This was introduced after ANRs were observed when performing synchronous disk space checks (see PR #709). The memory buffering ensures telemetry created during this window is retained (up to buffer limits).
83+
84+
## Customizing the Chain
85+
86+
The builder exposes customizer hooks that let you wrap or replace the default exporter before the disk layer is added:
87+
88+
```java
89+
OpenTelemetryRum.builder(application)
90+
.addSpanExporterCustomizer(exp -> new MyFilteringSpanExporter(exp))
91+
.addLogRecordExporterCustomizer(exp -> SpanAttributeRedactingLogExporter.wrap(exp))
92+
.addMetricExporterCustomizer(exp -> myMetricsFanOut(exp))
93+
.build();
94+
```
95+
96+
Customizer semantics (build time vs. runtime order):
97+
98+
* Start with the SDK's default exporter (e.g. `LoggingSpanExporter`) – call this the base exporter.
99+
* Each customizer is invoked in the exact order it was registered. It receives the exporter built so far and returns a (possibly wrapped) exporter. This produces a nested chain. If you register A then B then C, the resulting nesting is: `C(B(A(base)))`.
100+
* If disk buffering is enabled, the fully customized exporter (the outermost custom wrapper in code, i.e. `C(...)` in the example) is then wrapped by `*ToDiskExporter` so data is persisted to disk before reaching any of the custom wrappers.
101+
* Finally a `BufferDelegating*Exporter` wraps the whole thing to capture early telemetry while async initialization finishes. After `setDelegate(...)` it becomes a pass‑through.
102+
* Runtime data flow therefore in the A,B,C example (registered in that order) is:
103+
104+
```text
105+
BufferDelegating*Exporter → *ToDiskExporter (if enabled) → C → B → A → base exporter
106+
```
107+
108+
Notes:
109+
110+
* Registration order (A,B,C) is outside‑in at build time, but runtime export order is the reverse (C,B,A) because of nested wrapping.
111+
* The disk buffering layer is not added via a customizer; it is applied after all customizers are evaluated.
112+
* If disk buffering is disabled the flow simply omits that layer:
113+
114+
```text
115+
BufferDelegating*Exporter → C → B → A → base exporter
116+
```
117+
118+
Summary (concise): Once the exporter is built and all customizers have been applied, that result is wrapped by a disk buffering exporter (if enabled) and then finally wrapped by a `BufferDelegating*Exporter`.
119+
120+
You control the final network/export sink by returning it from the last customizer. For example, to send spans via OTLP HTTP:
121+
122+
```java
123+
builder.addSpanExporterCustomizer(prev -> OtlpHttpSpanExporter.builder()
124+
.setEndpoint("https://collector.example.com/v1/traces")
125+
.build());
126+
```
127+
128+
(You can wrap the OTLP exporter again if you need additional behavior.)
129+
130+
## Flush and Shutdown Behavior
131+
132+
If `flush()` or `shutdown()` is invoked before delegates are attached, the `DelegatingExporter` stores a pending result. Once the real delegate is set:
133+
134+
1. Buffered data is exported
135+
2. A flush is issued if it was pending
136+
3. A shutdown is issued if it was pending
137+
4. Pending futures complete with the real delegate result
138+
139+
## Failure and Backpressure Characteristics
140+
141+
* Buffer Overflow: If more than 5,000 signals of a type are produced before delegate attachment, newer signals beyond capacity are dropped (a warning is logged). Consider reducing startup emission volume or initializing earlier if this occurs.
142+
* Disk Layer: If disk initialization fails, the SDK logs an error and proceeds WITHOUT disk buffering (the chain reverts to memory buffer -> original exporter). The scheduled disk reader is disabled in this case.
143+
* From-Disk Export Failures: Batches that fail to export remain on disk (subject to age / size pruning rules governed by `DiskBufferingConfig`).
144+
* To-Disk Export Failures: If disk buffering is enabled and initialized but a batch cannot be written (e.g., I/O error or size constraint), that batch is immediately forwarded to the underlying exporter (skipping disk for that batch) to avoid data loss. Subsequent batches continue attempting disk writes.
145+
146+
## Configuration References
147+
148+
`DiskBufferingConfig` (see `core/src/main/java/io/opentelemetry/android/features/diskbuffering/DiskBufferingConfig.kt`) controls:
149+
150+
* Enable/disable disk buffering
151+
* Max cache folder size
152+
* Max file size
153+
* File age thresholds for read/write rotation
154+
* Optional directory override
155+
156+
## When To Add Your Own Layer
157+
158+
Common customization patterns:
159+
160+
* Filtering / Sampling: Drop or modify signals before disk/network (privacy, volume control)
161+
* Redaction / PII Scrubbing: Remove sensitive attributes centrally
162+
* Fan-out: Send data to multiple exporters (e.g., internal analytics + OTLP)
163+
* Encryption: Encrypt payloads prior to writing to disk (wrap before `*ToDiskExporter`) or prior to network send (wrap after disk exporter if you want encrypted at rest)
164+
165+
Example filtering span exporter:
166+
167+
```java
168+
class MyFilteringSpanExporter implements SpanExporter {
169+
private final SpanExporter delegate;
170+
MyFilteringSpanExporter(SpanExporter delegate) { this.delegate = delegate; }
171+
@Override public CompletableResultCode export(Collection<SpanData> spans) {
172+
List<SpanData> filtered = spans.stream()
173+
.filter(s -> !"debug".equals(s.getAttributes().get(stringKey("env"))))
174+
.toList();
175+
return delegate.export(filtered);
176+
}
177+
@Override public CompletableResultCode flush() { return delegate.flush(); }
178+
@Override public CompletableResultCode shutdown() { return delegate.shutdown(); }
179+
}
180+
```
181+
182+
Register in builder:
183+
184+
```java
185+
builder.addSpanExporterCustomizer(exp -> new MyFilteringSpanExporter(exp));
186+
```

0 commit comments

Comments
 (0)