1use std::{
7 env, io,
8 io::{IsTerminal, stdin},
9 sync::LazyLock,
10};
11
12use tracing::{
13 Event, Level, Metadata, Subscriber,
14 field::{Field, Visit},
15};
16use tracing_subscriber::{
17 field::RecordFields,
18 fmt,
19 fmt::{
20 FmtContext, FormatEvent, FormatFields, MakeWriter,
21 format::{Compact, DefaultVisitor, Format, Full, Pretty, Writer},
22 },
23 registry::LookupSpan,
24};
25
26use super::journald::{Entry, Journal, enabled as journald_enabled};
27use crate::{Config, Result, apply, debug, is_equal_to};
28
29static SYSTEMD_MODE: LazyLock<bool> =
30 LazyLock::new(|| env::var("SYSTEMD_EXEC_PID").is_ok() && env::var("JOURNAL_STREAM").is_ok());
31
32static TERMINAL_MODE: LazyLock<bool> = LazyLock::new(|| stdin().is_terminal());
33
34pub struct ConsoleWriter {
39 stdout: io::Stdout,
40 stderr: io::Stderr,
41 _journal_stream: [u64; 2],
42 use_stderr: bool,
43 journal: Option<Journal>,
44}
45
46pub enum Sink<'a> {
51 Console(&'a ConsoleWriter),
56
57 Journal(Entry<'a>),
62}
63
64impl ConsoleWriter {
65 #[must_use]
71 pub fn new(config: &Config) -> Self {
72 let journal_stream = get_journal_stream();
73
74 Self {
75 stdout: io::stdout(),
76 stderr: io::stderr(),
77 _journal_stream: journal_stream.into(),
78 use_stderr: journal_stream.0 != 0 || config.log_to_stderr,
79 journal: Journal::open(config),
80 }
81 }
82}
83
84impl<'a> MakeWriter<'a> for ConsoleWriter {
85 type Writer = Sink<'a>;
86
87 fn make_writer(&'a self) -> Self::Writer { Sink::Console(self) }
88
89 fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
90 self.journal
91 .as_ref()
92 .map_or(Sink::Console(self), |journal| Sink::Journal(journal.entry(meta)))
93 }
94}
95
96impl io::Write for Sink<'_> {
97 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
98 match self {
99 | Self::Console(console) => console.write(buf),
100 | Self::Journal(entry) => entry.write(buf),
101 }
102 }
103
104 fn flush(&mut self) -> io::Result<()> {
105 match self {
106 | Self::Console(console) => console.flush(),
107 | Self::Journal(entry) => entry.flush(),
108 }
109 }
110}
111
112impl io::Write for &'_ ConsoleWriter {
113 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
114 if self.use_stderr {
115 self.stderr.lock().write(buf)
116 } else {
117 self.stdout.lock().write(buf)
118 }
119 }
120
121 fn flush(&mut self) -> io::Result<()> {
122 if self.use_stderr {
123 self.stderr.lock().flush()
124 } else {
125 self.stdout.lock().flush()
126 }
127 }
128}
129
130pub struct ConsoleFormat {
136 pretty: Format<Pretty>,
137 full: Format<Full>,
138 compact: Format<Compact>,
139 compact_mode: bool,
140}
141
142impl ConsoleFormat {
143 #[must_use]
149 pub fn new(config: &Config) -> Self {
150 let ansi = ansi_enabled(config);
151
152 Self {
153 pretty: fmt::format()
154 .pretty()
155 .with_ansi(ansi)
156 .with_thread_names(true)
157 .with_thread_ids(true)
158 .with_target(true)
159 .with_file(true)
160 .with_line_number(true)
161 .with_source_location(true),
162
163 full: Format::<Full>::default()
164 .with_thread_ids(config.log_thread_ids)
165 .with_ansi(ansi),
166
167 compact: fmt::format().compact().with_ansi(ansi),
168
169 compact_mode: config.log_compact,
170 }
171 }
172}
173
174impl<S, N> FormatEvent<S, N> for ConsoleFormat
175where
176 S: Subscriber + for<'a> LookupSpan<'a>,
177 N: for<'a> FormatFields<'a> + 'static,
178{
179 fn format_event(
180 &self,
181 ctx: &FmtContext<'_, S, N>,
182 writer: Writer<'_>,
183 event: &Event<'_>,
184 ) -> Result<(), std::fmt::Error> {
185 let is_debug = debug::logging()
186 && event
187 .fields()
188 .map(|field| field.name())
189 .any(is_equal_to!("_debug"));
190
191 match *event.metadata().level() {
192 | _ if self.compact_mode => self.compact.format_event(ctx, writer, event),
193 | Level::ERROR if !is_debug => self.pretty.format_event(ctx, writer, event),
194 | _ => self.full.format_event(ctx, writer, event),
195 }
196 }
197}
198
199struct ConsoleVisitor<'a> {
200 visitor: DefaultVisitor<'a>,
201}
202
203impl<'writer> FormatFields<'writer> for ConsoleFormat {
204 fn format_fields<R>(&self, writer: Writer<'writer>, fields: R) -> Result<(), std::fmt::Error>
205 where
206 R: RecordFields,
207 {
208 let mut visitor = ConsoleVisitor {
209 visitor: DefaultVisitor::<'_>::new(writer, true),
210 };
211
212 fields.record(&mut visitor);
213
214 Ok(())
215 }
216}
217
218impl Visit for ConsoleVisitor<'_> {
219 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
220 if field.name().starts_with('_') {
221 return;
222 }
223
224 self.visitor.record_debug(field, value);
225 }
226}
227
228#[must_use]
229fn get_journal_stream() -> (u64, u64) {
230 is_systemd_mode()
231 .then(|| env::var("JOURNAL_STREAM").ok())
232 .flatten()
233 .as_deref()
234 .and_then(|s| s.split_once(':'))
235 .map(apply!(2, str::parse))
236 .map(apply!(2, Result::unwrap_or_default))
237 .unwrap_or((0, 0))
238}
239
240#[inline]
246#[must_use]
247pub fn ansi_enabled(config: &Config) -> bool { config.log_colors && !journald_enabled(config) }
248
249#[inline]
254#[must_use]
255pub fn is_systemd_mode() -> bool { *SYSTEMD_MODE }
256
257#[inline]
259#[must_use]
260pub fn is_terminal_mode() -> bool { *TERMINAL_MODE }