mas_context/
fmt.rs

1// Copyright 2025 New Vector Ltd.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4// Please see LICENSE in the repository root for full details.
5
6use console::{Color, Style};
7use opentelemetry::{
8    TraceId,
9    trace::{SamplingDecision, TraceContextExt},
10};
11use tracing::{Level, Subscriber};
12use tracing_opentelemetry::OtelData;
13use tracing_subscriber::{
14    fmt::{
15        FormatEvent, FormatFields,
16        format::{DefaultFields, Writer},
17        time::{FormatTime, SystemTime},
18    },
19    registry::LookupSpan,
20};
21
22use crate::LogContext;
23
24/// An event formatter usable by the [`tracing-subscriber`] crate, which
25/// includes the log context and the OTEL trace ID.
26#[derive(Debug, Default)]
27pub struct EventFormatter;
28
29struct FmtLevel<'a> {
30    level: &'a Level,
31    ansi: bool,
32}
33
34impl<'a> FmtLevel<'a> {
35    pub(crate) fn new(level: &'a Level, ansi: bool) -> Self {
36        Self { level, ansi }
37    }
38}
39
40const TRACE_STR: &str = "TRACE";
41const DEBUG_STR: &str = "DEBUG";
42const INFO_STR: &str = " INFO";
43const WARN_STR: &str = " WARN";
44const ERROR_STR: &str = "ERROR";
45
46const TRACE_STYLE: Style = Style::new().fg(Color::Magenta);
47const DEBUG_STYLE: Style = Style::new().fg(Color::Blue);
48const INFO_STYLE: Style = Style::new().fg(Color::Green);
49const WARN_STYLE: Style = Style::new().fg(Color::Yellow);
50const ERROR_STYLE: Style = Style::new().fg(Color::Red);
51
52impl std::fmt::Display for FmtLevel<'_> {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        let msg = match *self.level {
55            Level::TRACE => TRACE_STYLE.force_styling(self.ansi).apply_to(TRACE_STR),
56            Level::DEBUG => DEBUG_STYLE.force_styling(self.ansi).apply_to(DEBUG_STR),
57            Level::INFO => INFO_STYLE.force_styling(self.ansi).apply_to(INFO_STR),
58            Level::WARN => WARN_STYLE.force_styling(self.ansi).apply_to(WARN_STR),
59            Level::ERROR => ERROR_STYLE.force_styling(self.ansi).apply_to(ERROR_STR),
60        };
61        write!(f, "{msg}")
62    }
63}
64
65struct TargetFmt<'a> {
66    target: &'a str,
67    line: Option<u32>,
68}
69
70impl<'a> TargetFmt<'a> {
71    pub(crate) fn new(metadata: &tracing::Metadata<'a>) -> Self {
72        Self {
73            target: metadata.target(),
74            line: metadata.line(),
75        }
76    }
77}
78
79impl std::fmt::Display for TargetFmt<'_> {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(f, "{}", self.target)?;
82        if let Some(line) = self.line {
83            write!(f, ":{line}")?;
84        }
85        Ok(())
86    }
87}
88
89impl<S, N> FormatEvent<S, N> for EventFormatter
90where
91    S: Subscriber + for<'a> LookupSpan<'a>,
92    N: for<'writer> FormatFields<'writer> + 'static,
93{
94    fn format_event(
95        &self,
96        ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>,
97        mut writer: Writer<'_>,
98        event: &tracing::Event<'_>,
99    ) -> std::fmt::Result {
100        let ansi = writer.has_ansi_escapes();
101        let metadata = event.metadata();
102
103        SystemTime.format_time(&mut writer)?;
104
105        let level = FmtLevel::new(metadata.level(), ansi);
106        write!(&mut writer, " {level} ")?;
107
108        // If there is no explicit 'name' set in the event macro, it will have the
109        // 'event {filename}:{line}' value. In this case, we want to display the target:
110        // the module from where it was emitted. In other cases, we want to
111        // display the explit name of the event we have set.
112        let style = Style::new().dim().force_styling(ansi);
113        if metadata.name().starts_with("event ") {
114            write!(&mut writer, "{} ", style.apply_to(TargetFmt::new(metadata)))?;
115        } else {
116            write!(&mut writer, "{} ", style.apply_to(metadata.name()))?;
117        }
118
119        LogContext::maybe_with(|log_context| {
120            let log_context = Style::new()
121                .bold()
122                .force_styling(ansi)
123                .apply_to(log_context);
124            write!(&mut writer, "{log_context} - ")
125        })
126        .transpose()?;
127
128        let field_fromatter = DefaultFields::new();
129        field_fromatter.format_fields(writer.by_ref(), event)?;
130
131        // If we have a OTEL span, we can add the trace ID to the end of the log line
132        if let Some(span) = ctx.lookup_current() {
133            if let Some(otel) = span.extensions().get::<OtelData>() {
134                let parent_cx_span = otel.parent_cx.span();
135                let sc = parent_cx_span.span_context();
136
137                // Check if the span is sampled, first from the span builder,
138                // then from the parent context if nothing is set there
139                if otel
140                    .builder
141                    .sampling_result
142                    .as_ref()
143                    .map_or(sc.is_sampled(), |r| {
144                        r.decision == SamplingDecision::RecordAndSample
145                    })
146                {
147                    // If it is the root span, the trace ID will be in the span builder. Else, it
148                    // will be in the parent OTEL context
149                    let trace_id = otel.builder.trace_id.unwrap_or(sc.trace_id());
150                    if trace_id != TraceId::INVALID {
151                        let label = Style::new()
152                            .italic()
153                            .force_styling(ansi)
154                            .apply_to("trace.id");
155                        write!(&mut writer, " {label}={trace_id}")?;
156                    }
157                }
158            }
159        }
160
161        writeln!(&mut writer)
162    }
163}