Skip to content

feat: add opentelemetry-otlp-file crate implementing the OTLP File Exporter - #3763

Open
garysassano wants to merge 13 commits into
open-telemetry:mainfrom
garysassano:feat/otlp-file-exporter
Open

garysassano wants to merge 13 commits into
open-telemetry:mainfrom
garysassano:feat/otlp-file-exporter

Conversation

@garysassano

@garysassano garysassano commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Closes #2602
Design discussion issue (if applicable) #2602

Changes

Adds a new crate, opentelemetry-otlp-file, that implements the OTLP File Exporter specification (Development status) for traces, metrics and logs.

  • Format: SpanExporter, MetricExporter and LogExporter write each export as one line of compact OTLP JSON: a TracesData, MetricsData or LogsData object followed by \n.
  • Destinations: output defaults to stdout, as the spec requires. with_file(path) appends to a file, creating it if needed, and opens it in build() so a bad path fails early. with_writer(w) accepts any std::io::Write + Send + 'static.
  • Writes: each line is serialized first, then written and flushed under the writer's lock (and the stdout lock on stdout), so it is complete when export returns and does not interleave with other exports or println! output. If a write fails partway through a line, the export returns an error and the rest of the line is written before the next one, so later lines stay valid. Empty batches write nothing.
  • Shutdown: flushes and drops the writer; later exports return AlreadyShutdown.
  • Configuration: MetricExporter reads OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE with the same precedence as opentelemetry-otlp (builder, then environment, then cumulative).
  • Encoding: reuses the opentelemetry-proto with-serde types and transforms, the same path as http-json. No new dependencies are added to the workspace.
  • Docs: the README covers destinations, configuration, the Collector receiver's max_log_size limit, blocking writes with the async-runtime processors, and running on AWS Lambda, including the trade-offs between flushing strategies.

Consistency with other SDKs

  • Python (opentelemetry-exporter-otlp-json-file): writes to a path, a stream or stdout, and flushes after every export. This crate does the same.
  • Java (OtlpStdout*Exporter): writes to any OutputStream and never closes stdout. with_writer and the stdout handling match this.
  • C++: also offers file rotation. It is not included here because the spec does not ask for it.

Why a separate crate

The exporter needs none of the transport configuration in opentelemetry-otlp (endpoint, headers, timeout, retry, TLS), and opentelemetry-otlp is being stabilized while this spec is still Development, so a separate crate keeps it out of that API. Python and Java also ship it as a separate package. In review, @lalitb agreed with keeping it as a separate crate in this repo.

Not covered

Testing

  • Unit tests per signal: encoding, a round trip through opentelemetry-proto deserialization, empty batches, shutdown, file create/append/open errors and concurrent writes.
  • Lints: Clippy passes with -D warnings for each feature on its own and with all features, and the workspace lib tests pass.
  • Collector interop: output covering every attribute type, events, links, error status, all metric kinds including exponential histograms (cumulative and delta) and structured log bodies was read by the otlp_json_file receiver in Collector contrib 0.161.0. Diffing what its file exporter wrote back against the input showed no differences for any signal.
  • AWS Lambda (provided.al2023, arm64) with all three exporters on stdout and force_flush at the end of each invocation: every line arrived in CloudWatch Logs as a single log event with both the Text and JSON log formats, and the extracted lines passed the same Collector check. Lines longer than the roughly 256 KiB CloudWatch event limit are split across events, which the README points out.
  • Collector line limit: with Collector contrib 0.162.0, a 2.3 MiB traces line was dropped without an error under the receiver's default 1 MiB max_log_size and received in full with max_log_size: 4MiB, which the README now documents.
  • Async runtimes: with the async-runtime batch span processor on Tokio and a writer that blocks for 3 s, a 500 ms export timeout did not fire, which the README now documents.
  • MSRV and public API: the crate builds with Rust 1.75, and cargo check-external-types (nightly-2025-05-04) passes with the new allowed-external-types.toml, which I added to the CI matrix.

Notes

This was developed with AI assistance (Claude); I reviewed and tested the changes.

Merge requirement checklist

  • CONTRIBUTING guidelines followed
  • Unit tests added/updated (if applicable)
  • Appropriate CHANGELOG.md files updated for non-trivial, user-facing changes
  • Changes in public API reviewed (if applicable)

…porter

SpanExporter, MetricExporter and LogExporter write each export as one
line of OTLP JSON (TracesData, MetricsData or LogsData) to stdout, to a
file opened for appending, or to any std::io::Write implementation, as
described by the OpenTelemetry Protocol File Exporter specification.
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-10-17 01:45 UTC

Respond to 4 review items (e.g. link a commit, explain why not, ask a follow-up):

  • Inline threads: 1, 2, 3, 4
Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Should this be with reviewers? Comment /dashboard route:reviewers to route it to them.
  • Anything wrong — including the routing? Report it with what you expected; it helps us improve the dashboard.

@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.57593% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.9%. Comparing base (8d81bb7) to head (e8bf3b7).

Files with missing lines Patch % Lines
opentelemetry-otlp-file/src/metrics.rs 88.4% 23 Missing ⚠️
opentelemetry-otlp-file/src/output.rs 94.0% 22 Missing ⚠️
opentelemetry-otlp-file/src/logs.rs 87.4% 16 Missing ⚠️
opentelemetry-otlp-file/src/trace.rs 97.4% 5 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main   #3763     +/-   ##
=======================================
+ Coverage   84.1%   84.9%   +0.8%     
=======================================
  Files        123     127      +4     
  Lines      30280   31169    +889     
=======================================
+ Hits       25483   26486   +1003     
+ Misses      4797    4683    -114     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@garysassano garysassano changed the title feat: add opentelemetry-otlp-file crate implementing the OTLP File Exporter feat: add opentelemetry-otlp-file crate implementing the OTLP File Exporter Sep 29, 2026
SystemTime has 100 ns resolution on Windows, so a start time ending in
...321 ns came back as ...300 and failed the encoding assertion there.
@garysassano
garysassano force-pushed the feat/otlp-file-exporter branch from 2166952 to e430738 Compare September 29, 2026 20:04
@garysassano
garysassano marked this pull request as ready for review September 29, 2026 20:30
@garysassano
garysassano requested a review from a team as a code owner September 29, 2026 20:30

@scottgerring scottgerring left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @garysassano thanks for the PR! -i've put some initial feedback inline but it's generally looking good! I reckon the biggest thing to agree on to move forward is where this belongs - contrib, here, or as an experimental feature on the OTLP crate. I'm kind of leaning towards the latter to avoid duplication.

Have tagged the rest of the maintainers to express opinions so we can unblock you

@@ -0,0 +1,53 @@
[package]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should host this in contrib or as an unstable feature in the OTLP crate (which will help with re-use), in particular because the spec is still in development.

Reviewing it inline anyhow as I don't think it should affect the actual contents much, but it would be good to align on this - @open-telemetry/rust-maintainers thoughts?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to move it wherever you prefer. If it goes into opentelemetry-otlp, I'd suggest a separate file module behind its own feature flag (e.g. opentelemetry_otlp::file::SpanExporter) rather than a third option next to with_tonic()/with_http() on the existing builders, since it shares none of the endpoint, headers, timeout or TLS configuration. That would also let it reuse the env-var helpers you mention in the other comment. The move itself is mostly mechanical, since the encoding already goes through the same opentelemetry-proto with-serde types as http-json, so I'll wait for the decision before changing anything.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d keep this as a separate crate in this repo, unless other maintainers have concerns. The file exporter has a distinct configuration and dependency surface from HTTP/gRPC, and it already reuses the proto transforms. The small amount of duplicated environment-variable handling doesn’t seem sufficient reason to put it in opentelemetry-otlp or move that policy into the SDK. From my side, @garysassano can proceed with the current placement.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lalitb, I'll keep it as a separate crate here then. The Development status is called out at the top of the README and in the crate docs. I'll leave this thread open in case @scottgerring wants to weigh in, since he leaned towards the OTLP crate.

Comment thread opentelemetry-otlp-file/README.md Outdated
Comment thread opentelemetry-otlp-file/Cargo.toml Outdated
Comment thread opentelemetry-otlp-file/src/error.rs Outdated
Comment thread opentelemetry-otlp-file/src/metrics.rs
Comment thread opentelemetry-otlp-file/src/output.rs Outdated
Comment thread opentelemetry-otlp-file/README.md Outdated
Comment thread opentelemetry-otlp-file/README.md Outdated
@@ -0,0 +1,53 @@
[package]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d keep this as a separate crate in this repo, unless other maintainers have concerns. The file exporter has a distinct configuration and dependency surface from HTTP/gRPC, and it already reuses the proto transforms. The small amount of duplicated environment-variable handling doesn’t seem sufficient reason to put it in opentelemetry-otlp or move that policy into the SDK. From my side, @garysassano can proceed with the current placement.

@opentelemetry-pr-dashboard

This comment has been minimized.

@garysassano

Copy link
Copy Markdown
Member Author

/dashboard route:reviewers

@opentelemetry-pr-dashboard

Copy link
Copy Markdown

@garysassano, this pull request was routed to reviewers. The handoff remains active across pushes until newer actionable human feedback arrives. Top-level feedback through this request will not return; unresolved review threads remain open.

/// exporter. The writer is dropped even if the flush fails.
pub(crate) fn shutdown(&self) -> OTelSdkResult {
let mut state = self.lock()?.take().ok_or(OTelSdkError::AlreadyShutdown)?;
state.flush().map_err(|err| self.io_error(err))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worth also calling fsync when the destination is a file so that the caller can be more certain that the data is actually written durably to disk.


/// The destination configured on an exporter builder.
#[derive(Default)]
pub(crate) enum Output {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a more descriptive name here other than just Output, e.g. OutputStream?

@@ -0,0 +1,667 @@
use std::fmt;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might just be me, but I feel like a lot of the logic in this file can be greatly simplified and that some of these helper enums and structs can be consolidated.

}

#[cfg(test)]
mod tests {

@herin049 herin049 Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include some true end-to-end tests that involve writing to a real file (same for the trace/log exporters)?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: OTLP File Exporter

4 participants