aboutsummaryrefslogtreecommitdiff
path: root/crates/otlp-file-exporter
diff options
context:
space:
mode:
Diffstat (limited to 'crates/otlp-file-exporter')
-rw-r--r--crates/otlp-file-exporter/Cargo.toml24
-rw-r--r--crates/otlp-file-exporter/README.md12
-rw-r--r--crates/otlp-file-exporter/src/lib.rs130
3 files changed, 166 insertions, 0 deletions
diff --git a/crates/otlp-file-exporter/Cargo.toml b/crates/otlp-file-exporter/Cargo.toml
new file mode 100644
index 000000000..14ef4a46e
--- /dev/null
+++ b/crates/otlp-file-exporter/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "otlp-file-exporter"
+version = "0.1.0"
+authors = ["The Tor Project, Inc.", "Wesley Aptekar-Cassels <[email protected]>"]
+edition = "2024"
+rust-version = "1.91"
+license = "MIT OR Apache-2.0"
+homepage = "https://gitlab.torproject.org/tpo/core/arti/-/wikis/home"
+description = "OpenTelemetry SpanExporter for writing to an OTLP file."
+keywords = ["opentelemetry", "otlp", "tor", "arti"]
+categories = ["development-tools::profiling", "development-tools::debugging"]
+repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
+
+[dependencies]
+futures = "0.3.14"
+opentelemetry-proto = "0.32.0"
+opentelemetry_sdk = "0.32.1"
+serde_json = "1.0.50"
+
+[features]
+full = []
+
+[package.metadata.docs.rs]
+all-features = true
diff --git a/crates/otlp-file-exporter/README.md b/crates/otlp-file-exporter/README.md
new file mode 100644
index 000000000..7e31609e8
--- /dev/null
+++ b/crates/otlp-file-exporter/README.md
@@ -0,0 +1,12 @@
+# otlp-file-exporter
+
+This crate implements an OpenTelemetry exporter for writing [OTLP] JSON files to disk.
+
+This at some point will be [implemented upstream],
+at which point this crate will be deprecated,
+but for now it's here.
+
+License: MIT OR Apache-2.0
+
+[OTLP]: https://opentelemetry.io/docs/specs/otel/protocol/file-exporter/
+[implemented upstream]: https://github.com/open-telemetry/opentelemetry-rust/issues/2602
diff --git a/crates/otlp-file-exporter/src/lib.rs b/crates/otlp-file-exporter/src/lib.rs
new file mode 100644
index 000000000..7ed386be9
--- /dev/null
+++ b/crates/otlp-file-exporter/src/lib.rs
@@ -0,0 +1,130 @@
+// @@ begin lint list maintained by maint/add_warning @@
+#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
+#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
+#![warn(missing_docs)]
+#![warn(noop_method_call)]
+#![warn(unreachable_pub)]
+#![warn(clippy::all)]
+#![deny(clippy::await_holding_lock)]
+#![deny(clippy::cargo_common_metadata)]
+#![deny(clippy::cast_lossless)]
+#![deny(clippy::checked_conversions)]
+#![allow(clippy::cognitive_complexity)] // See arti#2556
+#![deny(clippy::debug_assert_with_mut_call)]
+#![deny(clippy::exhaustive_enums)]
+#![deny(clippy::exhaustive_structs)]
+#![deny(clippy::expl_impl_clone_on_copy)]
+#![deny(clippy::fallible_impl_from)]
+#![deny(clippy::implicit_clone)]
+#![deny(clippy::large_stack_arrays)]
+#![warn(clippy::manual_ok_or)]
+#![deny(clippy::missing_docs_in_private_items)]
+#![warn(clippy::needless_borrow)]
+#![warn(clippy::needless_pass_by_value)]
+#![warn(clippy::option_option)]
+#![deny(clippy::print_stderr)]
+#![deny(clippy::print_stdout)]
+#![warn(clippy::rc_buffer)]
+#![deny(clippy::ref_option_ref)]
+#![warn(clippy::semicolon_if_nothing_returned)]
+#![warn(clippy::trait_duplication_in_bounds)]
+#![deny(clippy::unchecked_time_subtraction)]
+#![deny(clippy::unnecessary_wraps)]
+#![warn(clippy::unseparated_literal_suffix)]
+#![deny(clippy::unwrap_used)]
+#![deny(clippy::mod_module_files)]
+#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
+#![allow(clippy::uninlined_format_args)]
+#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
+#![allow(clippy::result_large_err)] // temporary workaround for arti#587
+#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
+#![allow(clippy::needless_lifetimes)] // See arti#1765
+#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
+#![allow(clippy::collapsible_if)] // See arti#2342
+#![deny(clippy::unused_async)]
+#![deny(clippy::string_slice)] // See arti#2571
+//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
+
+//! Tracing exporter to write spans to a file in the OTLP JSON format.
+
+// TODO: If https://github.com/open-telemetry/opentelemetry-rust/issues/2602 gets fixed, we can
+// replace this entire file with whatever upstream has for doing this.
+
+use opentelemetry_proto::transform::common::tonic::ResourceAttributesWithSchema;
+use opentelemetry_proto::transform::trace::tonic::group_spans_by_resource_and_scope;
+use opentelemetry_sdk::{
+ Resource,
+ error::{OTelSdkError, OTelSdkResult},
+ trace::SpanExporter,
+};
+use std::{
+ fmt::Debug,
+ io::{LineWriter, Write},
+ sync::{Arc, Mutex},
+};
+
+/// Tracing exporter to write OTLP JSON to a file (or anything else that implements [`LineWriter`].
+#[derive(Debug)]
+pub struct FileExporter<W: Write + Send + Debug> {
+ /// The [`LineWriter`] to write to.
+ writer: Arc<Mutex<LineWriter<W>>>,
+ /// The [`Resource`] to associate spans with.
+ resource: Resource,
+}
+
+impl<W: Write + Send + Debug> FileExporter<W> {
+ /// Create a new [`FileExporter`]
+ pub fn new(writer: W, resource: Resource) -> Self {
+ Self {
+ writer: Arc::new(Mutex::new(LineWriter::new(writer))),
+ resource,
+ }
+ }
+}
+
+// Note that OpenTelemetry can only represent events as children of spans, so this exporter only
+// works on spans. If you want a event to be exported, you need to make sure it exists within some
+// span.
+impl<W: Write + Send + Debug> SpanExporter for FileExporter<W> {
+ fn export(
+ &self,
+ batch: Vec<opentelemetry_sdk::trace::SpanData>,
+ ) -> impl futures::Future<
+ Output = std::result::Result<(), opentelemetry_sdk::error::OTelSdkError>,
+ > + std::marker::Send {
+ let resource = ResourceAttributesWithSchema::from(&self.resource);
+ let data = group_spans_by_resource_and_scope(batch, &resource);
+ let mut writer = self.writer.lock().expect("Lock poisoned");
+ Box::pin(std::future::ready('write: {
+ // See https://opentelemetry.io/docs/specs/otel/protocol/file-exporter/ for format
+
+ if let Err(err) = serde_json::to_writer(
+ writer.get_mut(),
+ &serde_json::json!({"resourceSpans": data}),
+ ) {
+ break 'write Err(OTelSdkError::InternalFailure(err.to_string()));
+ }
+
+ if let Err(err) = writer.write(b"\n") {
+ break 'write Err(OTelSdkError::InternalFailure(err.to_string()));
+ }
+
+ Ok(())
+ }))
+ }
+
+ fn force_flush(&self) -> OTelSdkResult {
+ let mut writer = self
+ .writer
+ .lock()
+ .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?;
+
+ writer
+ .flush()
+ .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))
+ }
+
+ fn set_resource(&mut self, res: &opentelemetry_sdk::Resource) {
+ self.resource = res.clone();
+ }
+}