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