Skip to content

Commit 3ce9506

Browse files
authored
export logs over logs stream (#73)
1 parent 48376d1 commit 3ce9506

File tree

10 files changed

+1019
-435
lines changed

10 files changed

+1019
-435
lines changed

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ tonic = { version = "0.13", optional = true }
1919

2020
rand = "0.9.0"
2121

22-
opentelemetry = { version = "0.30", default-features = false, features = ["trace"] }
23-
opentelemetry_sdk = { version = "0.30", default-features = false, features = ["trace", "experimental_metrics_custom_reader"] }
24-
opentelemetry-otlp = { version = "0.30", default-features = false, features = ["trace", "metrics"] }
22+
opentelemetry = { version = "0.30", default-features = false, features = ["trace", "logs"] }
23+
opentelemetry_sdk = { version = "0.30", default-features = false, features = ["trace", "experimental_metrics_custom_reader", "logs"] }
24+
opentelemetry-otlp = { version = "0.30", default-features = false, features = ["trace", "metrics", "logs"] }
2525
futures-util = "0.3"
2626

2727
tracing = "0.1"

src/bridges/tracing.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,7 +1783,7 @@ mod tests {
17831783
let output = std::str::from_utf8(&output).unwrap();
17841784
let output = remap_timestamps_in_console_output(output);
17851785

1786-
assert_snapshot!(output, @r#"
1786+
assert_snapshot!(output, @r"
17871787
1970-01-01T00:00:00.000000Z INFO logfire::bridges::tracing::tests root event
17881788
1970-01-01T00:00:00.000001Z INFO logfire::bridges::tracing::tests root event with value field_value=1
17891789
1970-01-01T00:00:00.000002Z INFO logfire::bridges::tracing::tests root span
@@ -1792,6 +1792,6 @@ mod tests {
17921792
1970-01-01T00:00:00.000005Z DEBUG logfire::bridges::tracing::tests debug span with explicit parent
17931793
1970-01-01T00:00:00.000006Z INFO logfire::bridges::tracing::tests hello world log
17941794
1970-01-01T00:00:00.000007Z INFO logfire::bridges::tracing::tests hello world log with value field_value=1
1795-
"#);
1795+
");
17961796
}
17971797
}

src/config.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::{
99
};
1010

1111
use opentelemetry_sdk::{
12+
logs::LogProcessor,
1213
metrics::reader::MetricReader,
1314
trace::{IdGenerator, SpanProcessor},
1415
};
@@ -193,15 +194,15 @@ pub struct AdvancedOptions {
193194
pub(crate) id_generator: Option<BoxedIdGenerator>,
194195
/// Resource to override default resource detection.
195196
pub(crate) resource: Option<opentelemetry_sdk::Resource>,
197+
198+
// Configuration for OpenTelemetry logging. This is experimental and may be removed.
199+
pub(crate) log_record_processors: Vec<BoxedLogProcessor>,
196200
//
197201
//
198202
// TODO: arguments below supported by Python
199203

200204
// /// Generator for nanosecond start and end timestamps of spans.
201205
// pub ns_timestamp_generator: Option,
202-
203-
// /// Configuration for OpenTelemetry logging. This is experimental and may be removed.
204-
// pub log_record_processors: Vec<Box<dyn LogRecordProcessor>>,
205206
}
206207

207208
impl AdvancedOptions {
@@ -228,6 +229,17 @@ impl AdvancedOptions {
228229
self.resource = Some(resource);
229230
self
230231
}
232+
233+
/// Add a log processor to the list of log processors.
234+
#[must_use]
235+
pub fn with_log_processor<T: LogProcessor + Send + Sync + 'static>(
236+
mut self,
237+
processor: T,
238+
) -> Self {
239+
self.log_record_processors
240+
.push(BoxedLogProcessor::new(Box::new(processor)));
241+
self
242+
}
231243
}
232244

233245
struct RegionData {
@@ -388,6 +400,37 @@ impl MetricReader for BoxedMetricReader {
388400
}
389401
}
390402

403+
/// Boxed log processor for dynamic dispatch
404+
#[derive(Debug)]
405+
pub(crate) struct BoxedLogProcessor {
406+
inner: Box<dyn LogProcessor + Send + Sync>,
407+
}
408+
409+
impl BoxedLogProcessor {
410+
/// Create a new boxed log processor.
411+
pub fn new(processor: Box<dyn LogProcessor + Send + Sync>) -> Self {
412+
Self { inner: processor }
413+
}
414+
}
415+
416+
impl LogProcessor for BoxedLogProcessor {
417+
fn emit(
418+
&self,
419+
log_record: &mut opentelemetry_sdk::logs::SdkLogRecord,
420+
instrumentation_scope: &opentelemetry::InstrumentationScope,
421+
) {
422+
self.inner.emit(log_record, instrumentation_scope);
423+
}
424+
425+
fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
426+
self.inner.force_flush()
427+
}
428+
429+
fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
430+
self.inner.shutdown()
431+
}
432+
}
433+
391434
#[cfg(test)]
392435
mod tests {
393436
use crate::config::SendToLogfire;

src/exporters.rs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
//! Helper functions to configure mo
22
use std::collections::HashMap;
33

4-
use opentelemetry_otlp::{MetricExporter, Protocol};
5-
use opentelemetry_sdk::{metrics::exporter::PushMetricExporter, trace::SpanExporter};
4+
use opentelemetry_otlp::{LogExporter, MetricExporter, Protocol};
5+
use opentelemetry_sdk::{
6+
logs::LogExporter as LogExporterTrait, metrics::exporter::PushMetricExporter,
7+
trace::SpanExporter,
8+
};
69

710
use crate::{
811
ConfigureError, get_optional_env,
@@ -170,6 +173,76 @@ pub fn metric_exporter(
170173
}
171174
}
172175

176+
/// Build a [`LogExporter`] for passing to log processors.
177+
///
178+
/// This uses `OTEL_EXPORTER_OTLP_PROTOCOL` and `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` environment
179+
/// variables to determine the protocol to use (or otherwise defaults to [`Protocol::HttpBinary`]).
180+
///
181+
/// # Errors
182+
///
183+
/// Returns an error if the protocol specified by the env var is not supported or if the required feature is not enabled for
184+
/// the given protocol.
185+
///
186+
/// Returns an error if the endpoint is not a valid URI.
187+
///
188+
/// Returns an error if any headers are not valid HTTP headers.
189+
pub fn log_exporter(
190+
endpoint: &str,
191+
headers: Option<HashMap<String, String>>,
192+
) -> Result<impl LogExporterTrait + use<>, ConfigureError> {
193+
let (source, protocol) = protocol_from_env("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL")?;
194+
195+
let builder = LogExporter::builder();
196+
197+
// FIXME: it would be nice to let `opentelemetry-rust` handle this; ideally we could detect if
198+
// OTEL_EXPORTER_OTLP_PROTOCOL or OTEL_EXPORTER_OTLP_LOGS_PROTOCOL is set and let the SDK
199+
// make a builder. (If unset, we could supply our preferred exporter.)
200+
//
201+
// But at the moment otel-rust ignores these env vars; see
202+
// https://github.com/open-telemetry/opentelemetry-rust/issues/1983
203+
match protocol {
204+
Protocol::Grpc => {
205+
feature_required!("export-grpc", source, {
206+
use opentelemetry_otlp::WithTonicConfig;
207+
Ok(builder
208+
.with_tonic()
209+
.with_channel(
210+
tonic::transport::Channel::builder(
211+
endpoint.try_into().map_err(|e: http::uri::InvalidUri| {
212+
ConfigureError::Other(e.into())
213+
})?,
214+
)
215+
.connect_lazy(),
216+
)
217+
.with_metadata(build_metadata_from_headers(headers.as_ref())?)
218+
.build()?)
219+
})
220+
}
221+
Protocol::HttpBinary => {
222+
feature_required!("export-http-protobuf", source, {
223+
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
224+
Ok(builder
225+
.with_http()
226+
.with_protocol(Protocol::HttpBinary)
227+
.with_headers(headers.unwrap_or_default())
228+
.with_endpoint(format!("{endpoint}/v1/logs"))
229+
.build()?)
230+
})
231+
}
232+
Protocol::HttpJson => {
233+
feature_required!("export-http-json", source, {
234+
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
235+
Ok(builder
236+
.with_http()
237+
.with_protocol(Protocol::HttpBinary)
238+
.with_headers(headers.unwrap_or_default())
239+
.with_endpoint(format!("{endpoint}/v1/logs"))
240+
.build()?)
241+
})
242+
}
243+
}
244+
}
245+
173246
#[cfg(feature = "export-grpc")]
174247
fn build_metadata_from_headers(
175248
headers: Option<&HashMap<String, String>>,

0 commit comments

Comments
 (0)