Skip to main content

mas_context/
fmt.rs

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