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