spdlog_opentelemetry/
lib.rs

1//! Sends logs to [OpenTelemetry], based on [spdlog-rs].
2//!
3//! [spdlog-rs] is a fast, highly configurable Rust logging crate. This crate
4//! provides a sink that emits the collected logs to [OpenTelemetry]-compatible
5//! distributed logging systems for processing and visualization.
6//!
7//! ## Examples
8//!
9//! See directory [./examples].
10//!
11//! [OpenTelemetry]: https://opentelemetry.io/
12//! [spdlog-rs]: https://crates.io/crates/spdlog-rs
13//! [./examples]: https://github.com/SpriteOvO/spdlog-opentelemetry/tree/main/examples
14
15#![warn(missing_docs)]
16
17mod error;
18
19use std::{convert::Infallible, result::Result as StdResult, sync::Arc};
20
21pub use error::{Error, Result};
22use opentelemetry::{
23    Key as OtelKey,
24    logs::{
25        AnyValue as OtelAnyValue, LogRecord as _, Logger as OtelLogger,
26        LoggerProvider as OtelLoggerProvider, Severity as OtelSeverity,
27    },
28};
29use spdlog::{
30    ErrorHandler, Record, StringBuf,
31    formatter::{Formatter, FormatterContext, FullFormatter},
32    prelude::*,
33    sink::{GetSinkProp, Sink, SinkProp},
34};
35use value_bag::{ValueBag, visit::Visit as ValueBagVisit};
36
37#[doc(hidden)]
38pub mod __private {
39    pub struct NotSet;
40}
41use __private::NotSet;
42
43/// #
44///
45/// # Note
46///
47/// The generics here are designed to check for required fields at compile time,
48/// users should not specify them manually and/or depend on them. If the generic
49/// concrete types or the number of generic types are changed in the future, it
50/// may not be considered as a breaking change.
51pub struct OpenTelemetryBuilder<ArgL> {
52    prop: SinkProp,
53    logger: ArgL,
54}
55
56impl<ArgL> OpenTelemetryBuilder<ArgL> {
57    /// Specifies a provider.
58    ///
59    /// The provided provider will be used to create a
60    /// [`opentelemetry::logs::Logger`] for subsequent use.
61    ///
62    /// This parameter is **required**.
63    pub fn provider<P>(self, provider: &P) -> OpenTelemetryBuilder<P::Logger>
64    where
65        P: OtelLoggerProvider,
66    {
67        // Using empty scope name for now.
68        // https://github.com/open-telemetry/semantic-conventions/issues/1550
69        let logger = provider.logger("");
70        OpenTelemetryBuilder {
71            prop: self.prop,
72            logger,
73        }
74    }
75
76    // Prop
77    //
78
79    /// Specifies a log level filter.
80    ///
81    /// This parameter is **optional**, and defaults to [`LevelFilter::All`].
82    #[must_use]
83    pub fn level_filter(self, level_filter: LevelFilter) -> Self {
84        self.prop.set_level_filter(level_filter);
85        self
86    }
87
88    /// Specifies a formatter.
89    ///
90    /// This parameter is **optional**, and defaults to [`FullFormatter`]
91    /// `(!time !level !source_location !eol)`.
92    #[must_use]
93    pub fn formatter<F>(self, formatter: F) -> Self
94    where
95        F: Formatter + 'static,
96    {
97        self.prop.set_formatter(formatter);
98        self
99    }
100
101    /// Specifies an error handler.
102    ///
103    /// This parameter is **optional**, and defaults to
104    /// [`ErrorHandler::default()`].
105    #[must_use]
106    pub fn error_handler<F>(self, handler: F) -> Self
107    where
108        F: Into<ErrorHandler>,
109    {
110        self.prop.set_error_handler(handler);
111        self
112    }
113}
114
115impl OpenTelemetryBuilder<NotSet> {
116    #[doc(hidden)]
117    #[deprecated(note = "\n\n\
118        builder compile-time error:\n\
119        - missing required parameter `provider`\n\n\
120    ")]
121    pub fn build(self, _: Infallible) {}
122}
123
124impl<ArgL> OpenTelemetryBuilder<ArgL>
125where
126    ArgL: OtelLogger,
127{
128    /// Builds a `OpenTelemetrySink`.
129    pub fn build(self) -> Result<OpenTelemetrySink<ArgL>> {
130        Ok(OpenTelemetrySink {
131            prop: self.prop,
132            logger: self.logger,
133        })
134    }
135
136    /// Builds a `Arc<OpenTelemetrySink>`.
137    pub fn build_arc(self) -> Result<Arc<OpenTelemetrySink<ArgL>>> {
138        Self::build(self).map(Arc::new)
139    }
140}
141
142//
143
144/// A sink with a OpenTelemetry provider as the target.
145///
146/// It takes a [`SdkLoggerProvider`] from the upstream [`opentelemetry_sdk`]
147/// crate, the relevant settings should be configured there.
148///
149/// Note that when configuring [`SdkLoggerProvider`], you can use [the batch
150/// exporter] to gain better throughput via its built-in asynchronous
151/// implementation. If you choose to do so, combining `OpenTelemetrySink` with
152/// [`AsyncPoolSink`] is no longer necessary.
153///
154/// [`SdkLoggerProvider`]: https://docs.rs/opentelemetry_sdk/0.31.0/opentelemetry_sdk/logs/struct.SdkLoggerProvider.html
155/// [`opentelemetry_sdk`]: https://docs.rs/opentelemetry_sdk/latest/opentelemetry_sdk/
156/// [the batch exporter]: https://docs.rs/opentelemetry_sdk/0.31.0/opentelemetry_sdk/logs/struct.LoggerProviderBuilder.html#method.with_batch_exporter
157/// [`AsyncPoolSink`]: https://docs.rs/spdlog-rs/0.5.2/spdlog/sink/struct.AsyncPoolSink.html
158pub struct OpenTelemetrySink<L> {
159    prop: SinkProp,
160    logger: L,
161}
162
163impl OpenTelemetrySink<()> {
164    /// Gets a builder of `TelegramSink` with default parameters:
165    ///
166    /// | Parameter         | Default Value                                            |
167    /// |-------------------|----------------------------------------------------------|
168    /// | [level_filter]    | `All`                                                    |
169    /// | [formatter]       | [`FullFormatter`] `(!time !level !source_location !eol)` |
170    /// | [error_handler]   | [`ErrorHandler::default()`]                              |
171    /// |                   |                                                          |
172    /// | [provider]        | *must be specified*                                      |
173    ///
174    /// [level_filter]: OpenTelemetryBuilder::level_filter
175    /// [formatter]: OpenTelemetryBuilder::formatter
176    /// [error_handler]: OpenTelemetryBuilder::error_handler
177    /// [provider]: OpenTelemetryBuilder::provider
178    #[must_use]
179    pub fn builder() -> OpenTelemetryBuilder<NotSet> {
180        let prop = SinkProp::default();
181        prop.set_formatter(
182            FullFormatter::builder()
183                .time(false)
184                .level(false)
185                .source_location(false)
186                .eol(false)
187                .build(),
188        );
189        OpenTelemetryBuilder {
190            prop,
191            logger: NotSet,
192        }
193    }
194}
195
196impl<L> OpenTelemetrySink<L> {
197    const LEVELS_MAPPING: LevelsMapping = LevelsMapping::new();
198}
199
200impl<L> GetSinkProp for OpenTelemetrySink<L> {
201    fn prop(&self) -> &SinkProp {
202        &self.prop
203    }
204}
205
206impl<L> Sink for OpenTelemetrySink<L>
207where
208    L: OtelLogger + Send + Sync,
209{
210    // Unstable "spec_unstable_logs_enabled"
211    // https://github.com/open-telemetry/opentelemetry-rust/issues/3020
212    //
213    // fn should_log(&self, level: Level) -> bool {}
214
215    fn log(&self, record: &Record) -> spdlog::Result<()> {
216        let mut string_buf = StringBuf::new();
217        let mut ctx = FormatterContext::new();
218        self.prop
219            .formatter()
220            .format(record, &mut string_buf, &mut ctx)?;
221
222        let mut otel_record = self.logger.create_log_record();
223        if let Some(logger_name) = record.logger_name() {
224            otel_record.set_target(logger_name.to_owned());
225        }
226        otel_record.set_timestamp(record.time());
227        otel_record.set_severity_text(record.level().as_str());
228        otel_record.set_severity_number(Self::LEVELS_MAPPING.level(record.level()));
229        otel_record.set_body(OtelAnyValue::from(string_buf));
230        if let Some(srcloc) = record.source_location() {
231            // https://opentelemetry.io/docs/specs/semconv/registry/attributes/code/#code-attributes
232            otel_record.add_attribute("code.line.number", srcloc.line());
233            otel_record.add_attribute("code.column.number", srcloc.column());
234            otel_record.add_attribute("code.file.path", srcloc.file());
235            otel_record.add_attribute("code.function.name", srcloc.module_path());
236        }
237        otel_record.add_attributes(convert_attrs(record)?);
238
239        self.logger.emit(otel_record);
240        Ok(())
241    }
242
243    fn flush(&self) -> spdlog::Result<()> {
244        Ok(())
245    }
246}
247
248//
249
250struct LevelsMapping([OtelSeverity; Level::count()]);
251
252impl LevelsMapping {
253    #[must_use]
254    const fn new() -> Self {
255        Self([
256            OtelSeverity::Fatal, // spdlog::Critical
257            OtelSeverity::Error, // spdlog::Error
258            OtelSeverity::Warn,  // spdlog::Warn
259            OtelSeverity::Info,  // spdlog::Info
260            OtelSeverity::Debug, // spdlog::Debug
261            OtelSeverity::Trace, // spdlog::Trace
262        ])
263    }
264
265    #[must_use]
266    fn level(&self, level: Level) -> OtelSeverity {
267        self.0[level as usize]
268    }
269}
270
271fn convert_attrs(record: &Record) -> spdlog::Result<Vec<(OtelKey, OtelAnyValue)>> {
272    record
273        .key_values()
274        .iter()
275        .map(|(k, v)| convert_value(v).map(|v| (OtelKey::from(k.as_str().to_owned()), v)))
276        .collect::<spdlog::Result<_>>()
277}
278
279fn convert_value(value: ValueBag) -> spdlog::Result<OtelAnyValue> {
280    struct Converter(Option<OtelAnyValue>);
281
282    impl<'a> ValueBagVisit<'a> for Converter {
283        fn visit_any(&mut self, value: ValueBag) -> StdResult<(), value_bag::Error> {
284            self.0 = Some(OtelAnyValue::from(value.to_string()));
285            Ok(())
286        }
287
288        fn visit_i64(&mut self, value: i64) -> StdResult<(), value_bag::Error> {
289            self.0 = Some(OtelAnyValue::Int(value));
290            Ok(())
291        }
292
293        fn visit_u64(&mut self, value: u64) -> StdResult<(), value_bag::Error> {
294            if let Ok(value) = value.try_into() {
295                self.visit_i64(value)
296            } else {
297                self.visit_any(value.into())
298            }
299        }
300
301        fn visit_i128(&mut self, value: i128) -> StdResult<(), value_bag::Error> {
302            if let Ok(value) = value.try_into() {
303                self.visit_i64(value)
304            } else {
305                self.visit_any(value.into())
306            }
307        }
308
309        fn visit_u128(&mut self, value: u128) -> StdResult<(), value_bag::Error> {
310            if let Ok(value) = value.try_into() {
311                self.visit_i64(value)
312            } else {
313                self.visit_any(value.into())
314            }
315        }
316
317        fn visit_f64(&mut self, value: f64) -> StdResult<(), value_bag::Error> {
318            self.0 = Some(OtelAnyValue::Double(value));
319            Ok(())
320        }
321
322        fn visit_bool(&mut self, value: bool) -> StdResult<(), value_bag::Error> {
323            self.0 = Some(OtelAnyValue::Boolean(value));
324            Ok(())
325        }
326    }
327
328    let mut cvt = Converter(None);
329    value
330        .visit(&mut cvt)
331        .map_err(|err| spdlog::Error::Downstream(Box::new(Error::ValueBag(err))))?;
332    Ok(cvt.0.unwrap())
333}