diff options
Diffstat (limited to 'crates/otlp-file-exporter')
| -rw-r--r-- | crates/otlp-file-exporter/Cargo.toml | 24 | ||||
| -rw-r--r-- | crates/otlp-file-exporter/README.md | 12 | ||||
| -rw-r--r-- | crates/otlp-file-exporter/src/lib.rs | 130 |
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(); + } +} |
