tracing_dlt/
lib.rs

1/*
2 * Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS)
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 */
13
14//! Tracing integration for COVESA DLT (Diagnostic Log and Trace)
15//!
16//! [`tracing`](https://docs.rs/tracing) subscriber layer that forwards tracing events to the
17//! DLT daemon, enabling standard `tracing` macros (`info!`, `debug!`, `error!`, etc.) to
18//! send logs to DLT for centralized diagnostics.
19//!
20//! # Quick Start
21//!
22//! ```no_run
23//! use tracing_dlt::{DltLayer, DltId};
24//! use tracing::{info, span, Level};
25//! use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
26//!
27//! # #[tokio::main]
28//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
29//! // Register DLT layer
30//! let app_id = DltId::new(b"MBTI")?;
31//! tracing_subscriber::registry()
32//!     .with(DltLayer::new(&app_id, "My Beautiful Trace Ingestor")?)
33//!     .init();
34//!
35//! // Basic logging (uses default "DFLT" context)
36//! info!("Application started");
37//! // DLT Output: MBTI DFLT log info V 1 [lib: Application started]
38//! //             ^^^^ ^^^^ ^^^ ^^^^ ^ ^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39//! //             |    |    |   |    | |  Message (crate + msg)
40//! //             |    |    |   |    | Number of arguments
41//! //             |    |    |   |    Verbose flag
42//! //             |    |    |   Log level
43//! //             |    |    Log type
44//! //             |    Context ID (default)
45//! //             Application ID
46//!
47//! // Custom DLT context per span
48//! let span = span!(Level::INFO, "network", dlt_context = "NET");
49//! let _guard = span.enter();
50//! info!(bytes = 1024, "Data received");
51//! // DLT Output: MBTI NET log info V 2 [network: lib: Data received bytes = 1024]
52//! //                  ^^^               ^^^^^^^^       ^^^^^^^^^^^^        ^^^^
53//! //             |    Custom context    Span name      Message             Structured field
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! # Core Features
59//!
60//! - **Per-span contexts** - Use `dlt_context` field to route logs to specific DLT contexts
61//! - **Structured logging** - Span fields automatically included in messages with native types
62//! - **Layer composition** - Combine with other tracing layers (fmt, file, etc.)
63//! - **Thread-safe** - Full `Send + Sync` support
64//!
65//! # DLT Context Management
66//!
67//! Events outside spans use the default "DFLT" context. Spans can specify their own
68//! context with the `dlt_context` field (auto-creates and caches contexts):
69//!
70//! ```no_run
71//! # use tracing_dlt::{DltLayer, DltId};
72//! # use tracing::{info, span, Level};
73//! # use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
74//! # #[tokio::main]
75//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
76//! # let app_id = DltId::new(b"ADAS")?;
77//! # tracing_subscriber::registry().with(DltLayer::new(&app_id, "ADAS")?)  .init();
78//! // Nested contexts
79//! let outer = span!(Level::INFO, "can_bus", dlt_context = "CAN");
80//! let _g1 = outer.enter();
81//! info!(msg_id = 0x123, "CAN frame received");
82//!
83//! let inner = span!(Level::DEBUG, "decode", dlt_context = "CTRL");
84//! let _g2 = inner.enter();
85//! info!("Decoded steering command");
86//! # Ok(())
87//! # }
88//! ```
89//!
90//! # See Also
91//!
92//! - [`dlt_sys`] - Low-level Rust bindings for DLT
93//! - [`tracing`](https://docs.rs/tracing) - Application-level tracing framework
94//! - [COVESA DLT](https://github.com/COVESA/dlt-daemon) - The DLT daemon
95
96use std::{
97    collections::HashMap,
98    fmt,
99    fmt::Write,
100    sync::{Arc, RwLock},
101};
102
103use dlt_rs::DltContextHandle;
104// Re-export types for users of this library
105pub use dlt_rs::{DltApplication, DltError, DltId, DltLogLevel, DltSysError};
106use indexmap::IndexMap;
107use tracing_core::{Event, Subscriber, span};
108use tracing_subscriber::{Layer, filter::LevelFilter, layer::Context, registry::LookupSpan};
109
110/// Field name for custom DLT context ID in spans
111const DLT_CONTEXT_FIELD: &str = "dlt_context";
112
113/// COVESA DLT layer for tracing
114///
115/// Integrates `tracing` with DLT (Diagnostic Log and Trace). Spans can specify their
116/// own DLT context via the `dlt_context` field; events outside spans use "DFLT" context.
117///
118/// See the [crate-level documentation](index.html) for usage examples.
119pub struct DltLayer {
120    pub app: Arc<DltApplication>,
121    /// Default context for events outside of spans
122    default_context: Arc<DltContextHandle>,
123    /// Cache of DLT contexts by span name
124    context_cache: Arc<RwLock<HashMap<String, Arc<DltContextHandle>>>>,
125}
126impl DltLayer {
127    /// Create a new DLT layer
128    ///
129    /// Registers the application with DLT and creates a default "DFLT" context.
130    /// Span contexts are created on-demand using the `dlt_context` field.
131    ///
132    /// # Errors
133    /// Returns error if DLT registration fails or strings contain null bytes.
134    ///
135    /// # Panics
136    /// If called outside a Tokio runtime context.
137    pub fn new(app_id: &DltId, app_description: &str) -> Result<Self, DltError> {
138        // Register application with DLT
139        let app = Arc::new(DltApplication::register(app_id, app_description)?);
140        // Create default context for events outside spans
141        let default_context =
142            Arc::new(app.create_context(&DltId::new(b"DFLT")?, "Default context")?);
143
144        Self::register_context_level_changed(&default_context)?;
145
146        Ok(DltLayer {
147            app,
148            default_context,
149            context_cache: Arc::new(RwLock::new(HashMap::new())),
150        })
151    }
152
153    fn register_context_level_changed(context: &DltContextHandle) -> Result<(), DltError> {
154        let mut receiver = context.register_log_level_changed_listener()?;
155        // Spawn a background task to handle log level updates for all contexts
156        tokio::spawn(async move {
157            while let Ok(_event) = receiver.recv().await {
158                // Log level is already updated internally in the DltContextHandle
159                // We just need to rebuild the callsite interest
160                // cache so that the new level takes effect
161                tracing_core::callsite::rebuild_interest_cache();
162            }
163        });
164        Ok(())
165    }
166
167    /// Get or create a DLT context for a span
168    ///
169    /// Checks for a `dlt_context` field in the span. If present, uses it as the context ID.
170    /// Otherwise, returns the default context.
171    /// Caches contexts to avoid recreating them for the same context ID.
172    fn get_or_create_context_for_span<S>(
173        &self,
174        span: &tracing_subscriber::registry::SpanRef<'_, S>,
175    ) -> Result<Arc<DltContextHandle>, DltError>
176    where
177        S: Subscriber + for<'a> LookupSpan<'a>,
178    {
179        let span_name = span.name();
180
181        // Check if span has a custom "dlt_context" field
182        let dlt_context_id = {
183            let extensions = span.extensions();
184            extensions
185                .get::<IndexMap<String, FieldValue>>()
186                .and_then(|fields| {
187                    fields.get(DLT_CONTEXT_FIELD).and_then(|value| match value {
188                        FieldValue::Str(s) => Some(s.clone()),
189                        _ => None,
190                    })
191                })
192        };
193
194        // If no custom dlt_context field, use default context
195        let Some(custom_id) = dlt_context_id else {
196            return Ok(Arc::clone(&self.default_context));
197        };
198
199        // Check cache for custom context
200        {
201            let cache = self.context_cache.read().map_err(|_| DltError::BadLock)?;
202            if let Some(context) = cache.get(&custom_id) {
203                return Ok(Arc::clone(context));
204            }
205        }
206
207        // Create new context with custom ID
208        let ctx_id = DltId::from_str_clamped(&custom_id)?;
209        let context = Arc::new(self.app.create_context(&ctx_id, span_name)?);
210
211        let mut cache = self.context_cache.write().map_err(|_| DltError::BadLock)?;
212        cache.insert(custom_id, Arc::clone(&context));
213        Self::register_context_level_changed(&context)?;
214
215        Ok(context)
216    }
217
218    #[cfg(feature = "dlt_layer_internal_logging")]
219    fn log_dlt_error(metadata: &tracing_core::Metadata, level: tracing::Level, e: DltSysError) {
220        eprintln!("DLT error occurred: {e:?}");
221        tracing::warn!(
222            target: "dlt_layer_internal",
223            error = ?e,
224            event_target = metadata.target(),
225            event_level = ?level,
226            "DLT error occurred"
227        );
228    }
229
230    #[cfg(not(feature = "dlt_layer_internal_logging"))]
231    fn log_dlt_error(_metadata: &tracing_core::Metadata, _level: tracing::Level, _e: DltSysError) {
232        // Silent if internal logging is disabled
233    }
234
235    fn max_dlt_level(&self) -> DltLogLevel {
236        if let Ok(cache) = self.context_cache.read() {
237            cache
238                .values()
239                .map(|ctx| ctx.log_level())
240                .chain(std::iter::once(self.default_context.log_level()))
241                .max_by_key(|&level| level as i32)
242                .unwrap_or(DltLogLevel::Default)
243        } else {
244            DltLogLevel::Default
245        }
246    }
247}
248
249impl<S> Layer<S> for DltLayer
250where
251    S: Subscriber + for<'a> LookupSpan<'a>,
252{
253    fn enabled(&self, metadata: &tracing::Metadata<'_>, _ctx: Context<'_, S>) -> bool {
254        // Prevent our own internal error messages from being logged to DLT
255        metadata.target() != "dlt_layer_internal"
256    }
257
258    fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: Context<'_, S>) {
259        // Store span fields for later context building
260        if let Some(span) = ctx.span(id) {
261            let mut visitor = FieldVisitor::new();
262            attrs.record(&mut visitor);
263
264            let mut extensions = span.extensions_mut();
265            extensions.insert(visitor.fields);
266        }
267    }
268
269    fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
270        // Update span fields
271        if let Some(span) = ctx.span(span) {
272            let mut visitor = FieldVisitor::new();
273            values.record(&mut visitor);
274
275            let mut extensions = span.extensions_mut();
276            if let Some(fields) = extensions.get_mut::<IndexMap<String, FieldValue>>() {
277                fields.extend(visitor.fields);
278            } else {
279                extensions.insert(visitor.fields);
280            }
281        }
282    }
283
284    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
285        let metadata = event.metadata();
286        let level = metadata.level();
287
288        // Determine which DLT context to use based on the current span
289        let dlt_context = ctx
290            .event_scope(event)
291            .and_then(|scope| scope.from_root().last())
292            .map_or(Arc::clone(&self.default_context), |span| {
293                self.get_or_create_context_for_span(&span)
294                    .unwrap_or_else(|_| Arc::clone(&self.default_context))
295            });
296
297        let dlt_level = map_level_to_dlt(*level);
298        let context_log_level = dlt_context.log_level();
299        if (dlt_level as i32) > (context_log_level as i32) {
300            return; // Skip logging if level is above DLT threshold for this context
301        }
302
303        // Start building the DLT message
304        let mut log_writer = match dlt_context.log_write_start(dlt_level) {
305            Ok(log_writer) => log_writer,
306            Err(e) => {
307                Self::log_dlt_error(metadata, *level, e);
308                return;
309            }
310        };
311
312        if let Some(scope) = ctx.event_scope(event) {
313            let mut span_context = String::new();
314            for span in scope.from_root() {
315                if !span_context.is_empty() {
316                    span_context.push(':');
317                }
318                span_context.push_str(span.name());
319
320                let extensions = span.extensions();
321                if let Some(fields) = extensions.get::<IndexMap<String, FieldValue>>() {
322                    // Filter out dlt_context field from display
323                    let display_fields: Vec<_> = fields
324                        .iter()
325                        .filter(|(name, _)| *name != DLT_CONTEXT_FIELD)
326                        .collect();
327
328                    if !display_fields.is_empty() {
329                        span_context.push('{');
330                        for (i, (k, v)) in display_fields.iter().enumerate() {
331                            if i > 0 {
332                                span_context.push_str(", ");
333                            }
334                            let _ = write!(span_context, "{k}={v}");
335                        }
336                        span_context.push('}');
337                    }
338                }
339            }
340
341            if !span_context.is_empty() {
342                span_context.push(':');
343                let _ = log_writer.write_string(&span_context);
344            }
345        }
346
347        // Add event fields with native types
348        let mut visitor = FieldVisitor::new();
349        event.record(&mut visitor);
350
351        // Extract the message field if present, so we can
352        // only take the value without the "message=" prefix
353        // this reduces the clutter in dlt and gives more space for relevant info
354        let mut fields = visitor.fields;
355        let message_field = fields.shift_remove("message");
356        let target = metadata.target();
357        if let Some(msg) = message_field {
358            let formatted = if target.is_empty() {
359                msg.to_string()
360            } else {
361                format!("{target}: {msg}")
362            };
363            let _ = log_writer.write_string(&formatted);
364        } else if !target.is_empty() {
365            let _ = log_writer.write_string(target);
366        }
367
368        // Write all other fields normally, the dlt_context field is already filtered out
369        if let Err(e) = write_fields(&mut log_writer, fields) {
370            Self::log_dlt_error(metadata, *level, e);
371            return;
372        }
373
374        // Finish and send the log message
375        if let Err(e) = log_writer.finish() {
376            Self::log_dlt_error(metadata, *level, e);
377        }
378    }
379}
380
381// Implement Filter trait to provide dynamic max level hint based on DLT configuration
382impl<S> tracing_subscriber::layer::Filter<S> for DltLayer {
383    /// Determines if a span or event with the given metadata is enabled.
384    ///
385    /// This implementation checks if the event/span level is within the current
386    /// DLT maximum log level across all contexts. It also filters out internal
387    /// DLT layer errors to prevent recursion.
388    ///
389    /// Since we don't know which context will be used at this point, we use the
390    /// most permissive level across all contexts.
391    fn enabled(&self, meta: &tracing_core::Metadata<'_>, _cx: &Context<'_, S>) -> bool {
392        // Prevent our own internal error messages from being logged to DLT
393        if meta.target() == "dlt_layer_internal" {
394            return false;
395        }
396
397        // Check if this log level is enabled by any DLT context
398        let dlt_level = map_level_to_dlt(*meta.level());
399
400        // Find the most permissive (highest) log level across all contexts
401        let max_level = self.max_dlt_level();
402
403        // Compare log levels - enable if event level is less than or equal to max allowed
404        (dlt_level as i32) <= (max_level as i32)
405    }
406
407    /// Returns the current maximum log level from DLT.
408    ///
409    /// This hint allows the tracing infrastructure to skip callsites that are
410    /// more verbose than the current DLT log level, improving performance.
411    ///
412    /// When the DLT log level changes (via DLT daemon or configuration), the
413    /// background task calls `rebuild_interest_cache()` to ensure this new hint
414    /// takes effect immediately.
415    ///
416    /// This returns the most permissive (verbose) level across all contexts, since
417    /// we can't know which context will be used at callsite registration time.
418    fn max_level_hint(&self) -> Option<LevelFilter> {
419        let max_level = self.max_dlt_level();
420
421        Some(map_dlt_to_level_filter(max_level))
422    }
423}
424
425/// Map tracing log levels to DLT log levels
426fn map_level_to_dlt(level: tracing::Level) -> DltLogLevel {
427    match level {
428        tracing::Level::ERROR => DltLogLevel::Error,
429        tracing::Level::WARN => DltLogLevel::Warn,
430        tracing::Level::INFO => DltLogLevel::Info,
431        tracing::Level::DEBUG => DltLogLevel::Debug,
432        tracing::Level::TRACE => DltLogLevel::Verbose,
433    }
434}
435
436/// Map DLT log level to tracing `LevelFilter`
437fn map_dlt_to_level_filter(dlt_level: DltLogLevel) -> LevelFilter {
438    match dlt_level {
439        DltLogLevel::Off | DltLogLevel::Default => LevelFilter::OFF,
440        DltLogLevel::Fatal | DltLogLevel::Error => LevelFilter::ERROR,
441        DltLogLevel::Warn => LevelFilter::WARN,
442        DltLogLevel::Info => LevelFilter::INFO,
443        DltLogLevel::Debug => LevelFilter::DEBUG,
444        DltLogLevel::Verbose => LevelFilter::TRACE,
445    }
446}
447
448/// Helper function to write fields to DLT with proper error propagation
449fn write_fields(
450    log_writer: &mut dlt_rs::DltLogWriter,
451    fields: IndexMap<String, FieldValue>,
452) -> Result<(), DltSysError> {
453    for (field_name, field_value) in fields {
454        // Write field name
455        log_writer.write_string(&field_name)?;
456        log_writer.write_string("=")?;
457
458        // Write field value with native type
459        match field_value {
460            FieldValue::I64(v) => {
461                log_writer.write_int64(v)?;
462            }
463            FieldValue::U64(v) => {
464                log_writer.write_uint64(v)?;
465            }
466            FieldValue::I128(v) => {
467                // DLT doesn't support i128, convert to string
468                log_writer.write_string(&v.to_string())?;
469            }
470            FieldValue::U128(v) => {
471                // DLT doesn't support u128, convert to string
472                log_writer.write_string(&v.to_string())?;
473            }
474            FieldValue::F64(v) => {
475                log_writer.write_float64(v)?;
476            }
477            FieldValue::Bool(v) => {
478                log_writer.write_bool(v)?;
479            }
480            FieldValue::Str(s) | FieldValue::Debug(s) => {
481                log_writer.write_string(&s)?;
482            }
483        }
484    }
485    Ok(())
486}
487
488/// Typed field value that preserves the original data type
489#[derive(Debug, Clone)]
490enum FieldValue {
491    Str(String),
492    I64(i64),
493    U64(u64),
494    I128(i128),
495    U128(u128),
496    F64(f64),
497    Bool(bool),
498    Debug(String),
499}
500
501impl fmt::Display for FieldValue {
502    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503        match self {
504            FieldValue::I64(v) => write!(f, "{v}"),
505            FieldValue::U64(v) => write!(f, "{v}"),
506            FieldValue::I128(v) => write!(f, "{v}"),
507            FieldValue::U128(v) => write!(f, "{v}"),
508            FieldValue::F64(v) => write!(f, "{v}"),
509            FieldValue::Bool(v) => write!(f, "{v}"),
510            FieldValue::Str(s) => write!(f, "\"{s}\""),
511            FieldValue::Debug(s) => write!(f, "{s}"),
512        }
513    }
514}
515
516/// Helper visitor for extracting span/event fields with type preservation
517struct FieldVisitor {
518    fields: IndexMap<String, FieldValue>,
519}
520
521impl FieldVisitor {
522    fn new() -> Self {
523        FieldVisitor {
524            fields: IndexMap::new(),
525        }
526    }
527}
528
529impl tracing::field::Visit for FieldVisitor {
530    fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
531        self.fields
532            .insert(field.name().to_string(), FieldValue::F64(value));
533    }
534
535    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
536        self.fields
537            .insert(field.name().to_string(), FieldValue::I64(value));
538    }
539
540    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
541        self.fields
542            .insert(field.name().to_string(), FieldValue::U64(value));
543    }
544
545    fn record_i128(&mut self, field: &tracing::field::Field, value: i128) {
546        self.fields
547            .insert(field.name().to_string(), FieldValue::I128(value));
548    }
549
550    fn record_u128(&mut self, field: &tracing::field::Field, value: u128) {
551        self.fields
552            .insert(field.name().to_string(), FieldValue::U128(value));
553    }
554
555    fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
556        self.fields
557            .insert(field.name().to_string(), FieldValue::Bool(value));
558    }
559
560    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
561        self.fields
562            .insert(field.name().to_string(), FieldValue::Str(value.to_string()));
563    }
564
565    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn fmt::Debug) {
566        self.fields.insert(
567            field.name().to_string(),
568            FieldValue::Debug(format!("{value:?}")),
569        );
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
576
577    use super::*;
578
579    #[tokio::test]
580    async fn test_dlt_appender() {
581        // This works even without a running DLT daemon because
582        // the DLT C library buffers messages internally.
583        let app_id = DltId::new(b"APP").unwrap();
584
585        tracing_subscriber::registry()
586            .with(DltLayer::new(&app_id, "test").expect("Failed to create DLT layer"))
587            .init();
588
589        let outer_span = tracing::info_span!("outer", level = 0);
590        let _outer_entered = outer_span.enter();
591
592        let inner_span = tracing::error_span!("inner", level = 1);
593        let _inner_entered = inner_span.enter();
594
595        tracing::info!(a_bool = true, answer = 42, message = "first example");
596    }
597}