1use std::collections::BTreeMap;
2use std::fs::File;
3use std::io::Write;
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant};
6
7use crate::progress::color::ColorMode;
8use crate::progress::log::{ProgressEvent, StatusLevel, normalize_multiline_message};
9use crate::progress::redaction::{ProgressRedactor, redact_sensitive_text};
10
11pub enum LogTarget {
12 Terminal,
13 File(File),
14 Batch {
15 prefix: String,
16 file: File,
17 started_at: Option<Instant>,
18 last_event_at: Option<Instant>,
19 outcome: Option<BatchOutcome>,
20 },
21 Routed {
22 fallback: Logger,
23 hosts: BTreeMap<String, Logger>,
24 },
25 Silent,
26}
27
28impl LogTarget {
29 pub fn log_event(&mut self, level: StatusLevel, message: &str) {
30 self.write_event(level, message, message);
31 }
32
33 pub fn log_status(&mut self, message: &str) {
34 self.write_event(StatusLevel::Info, message, message);
35 }
36
37 pub fn log_debug(&mut self, message: &str) {
38 self.write_event(StatusLevel::Debug, message, message);
39 }
40
41 fn write_event(&mut self, level: StatusLevel, message: &str, file_message: &str) {
42 let message = normalize_multiline_message(message);
43 let message = redact_sensitive_text(&message);
44 let file_message = normalize_multiline_message(file_message);
45 let file_message = redact_sensitive_text(&file_message);
46 let message_lines: Vec<&str> =
47 if message.is_empty() { vec![""] } else { message.lines().collect() };
48 let file_lines: Vec<&str> =
49 if file_message.is_empty() { vec![""] } else { file_message.lines().collect() };
50
51 match self {
52 LogTarget::Terminal => {
53 for line in message_lines {
54 let event = ProgressEvent::new(level, None, line);
55 event.log(ColorMode::Auto);
56 }
57 }
58 LogTarget::Batch { prefix, file, started_at, last_event_at, outcome } => {
59 let now = Instant::now();
60 let started = *started_at.get_or_insert(now);
61 let elapsed = now.saturating_duration_since(started);
62 *last_event_at = Some(now);
63 match level {
64 StatusLevel::Success => *outcome = Some(BatchOutcome::Succeeded),
65 StatusLevel::Failure | StatusLevel::Error => *outcome = Some(BatchOutcome::Failed),
66 _ => {}
67 }
68 let terminal_message = message.strip_prefix(&format!("{prefix}: ")).unwrap_or(&message);
69 let terminal_message = timed_batch_message(level, terminal_message, elapsed);
70 for line in terminal_message.lines() {
71 let event = ProgressEvent::new(level, Some(prefix), line);
72 event.log(ColorMode::Auto);
73 }
74 for line in file_lines {
75 let line = timed_batch_message(level, line, elapsed);
76 let _ = writeln!(file, "[{}] {}", level.label(), line);
77 }
78 }
79 LogTarget::File(file) => {
80 for line in file_lines {
81 let _ = writeln!(file, "[{}] {}", level.label(), line);
82 }
83 }
84 LogTarget::Routed { .. } => {}
85 LogTarget::Silent => {}
86 }
87 }
88}
89
90fn timed_batch_message(level: StatusLevel, message: &str, elapsed: Duration) -> String {
91 let elapsed = crate::progress::log::format_elapsed(elapsed);
92 match level {
93 StatusLevel::Success => format!("{message} in {elapsed}"),
94 StatusLevel::Failure | StatusLevel::Error => format!("{message} after {elapsed}"),
95 _ => message.to_string(),
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum BatchOutcome {
101 Succeeded,
102 Failed,
103}
104
105#[derive(Clone)]
106pub struct Logger {
107 pub target: Arc<Mutex<LogTarget>>,
108 redactor: Arc<Mutex<ProgressRedactor>>,
109}
110
111impl Logger {
112 pub fn new(target: Arc<Mutex<LogTarget>>) -> Self {
113 Self { target, redactor: Arc::new(Mutex::new(ProgressRedactor::default())) }
114 }
115
116 pub fn with_sensitive_values<'a>(&self, values: impl IntoIterator<Item = &'a [u8]>) -> Self {
117 Self {
118 target: self.target.clone(),
119 redactor: Arc::new(Mutex::new(ProgressRedactor::with_sensitive_values(values))),
120 }
121 }
122
123 pub fn sanitize(&self, message: &str) -> String {
124 self
125 .redactor
126 .lock()
127 .map(|mut redactor| redactor.redact(message))
128 .unwrap_or_else(|_| redact_sensitive_text(message))
129 }
130
131 pub fn lock(&self) -> std::sync::LockResult<std::sync::MutexGuard<'_, LogTarget>> {
132 self.target.lock()
133 }
134
135 pub fn silent() -> Self {
136 Self::new(Arc::new(Mutex::new(LogTarget::Silent)))
137 }
138
139 pub fn terminal() -> Self {
140 Self::new(Arc::new(Mutex::new(LogTarget::Terminal)))
141 }
142
143 pub fn file(file: File) -> Self {
144 Self::new(Arc::new(Mutex::new(LogTarget::File(file))))
145 }
146
147 pub fn batch(prefix: String, file: File) -> Self {
148 Self::new(Arc::new(Mutex::new(LogTarget::Batch {
149 prefix,
150 file,
151 started_at: None,
152 last_event_at: None,
153 outcome: None,
154 })))
155 }
156
157 pub fn routed(fallback: Logger, hosts: BTreeMap<String, Logger>) -> Self {
158 Self::new(Arc::new(Mutex::new(LogTarget::Routed { fallback, hosts })))
159 }
160
161 pub fn for_host(&self, hostname: &str) -> Self {
162 self
163 .target
164 .lock()
165 .ok()
166 .and_then(|target| match &*target {
167 LogTarget::Routed { fallback, hosts } => {
168 Some(hosts.get(hostname).unwrap_or(fallback).clone())
169 }
170 _ => None,
171 })
172 .unwrap_or_else(|| self.clone())
173 }
174
175 fn routed_fallback(&self) -> Option<Logger> {
176 self.target.lock().ok().and_then(|target| match &*target {
177 LogTarget::Routed { fallback, .. } => Some(fallback.clone()),
178 _ => None,
179 })
180 }
181
182 pub fn event(&self, level: StatusLevel, message: &str) {
183 if level == StatusLevel::Debug && !crate::config::get_runtime_options().debug {
184 return;
185 }
186 let message = self.sanitize(message);
187 if let Some(fallback) = self.routed_fallback() {
188 fallback.event(level, &message);
189 return;
190 }
191 if let Ok(mut lock) = self.target.lock() {
192 lock.log_event(level, &message);
193 }
194 }
195
196 pub fn detail(&self, message: &str) {
200 let message = self.sanitize(message);
201 if let Some(fallback) = self.routed_fallback() {
202 fallback.detail(&message);
203 return;
204 }
205 if let Ok(mut target) = self.target.lock() {
206 match &mut *target {
207 LogTarget::Terminal => target.log_event(StatusLevel::Info, &message),
208 LogTarget::Batch { file, .. } | LogTarget::File(file) => {
209 for line in normalize_multiline_message(&message).lines() {
210 let _ = writeln!(file, "{}", line);
211 }
212 }
213 LogTarget::Routed { .. } | LogTarget::Silent => {}
214 }
215 }
216 }
217
218 pub fn elapsed(&self) -> Option<Duration> {
222 self.target.lock().ok().and_then(|target| match &*target {
223 LogTarget::Batch { started_at: Some(started), last_event_at: Some(last), .. } => {
224 Some(last.saturating_duration_since(*started))
225 }
226 _ => None,
227 })
228 }
229
230 pub fn batch_outcome(&self) -> Option<BatchOutcome> {
231 self.target.lock().ok().and_then(|target| match &*target {
232 LogTarget::Batch { outcome, .. } => *outcome,
233 _ => None,
234 })
235 }
236
237 pub fn output_line(&self, line: &str, stderr: bool) {
238 let line = self.sanitize(line);
239 if let Some(fallback) = self.routed_fallback() {
240 fallback.output_line(&line, stderr);
241 return;
242 }
243 if let Ok(mut target) = self.target.lock() {
244 match &mut *target {
245 LogTarget::Terminal => {
246 if stderr {
247 eprintln!("{}", crate::progress::stream::format_output_line(&line));
248 } else {
249 println!("{}", crate::progress::stream::format_output_line(&line));
250 }
251 }
252 LogTarget::Batch { file, .. } | LogTarget::File(file) => {
253 let _ = writeln!(file, "{}", line);
254 }
255 LogTarget::Routed { .. } | LogTarget::Silent => {}
256 }
257 }
258 }
259
260 pub fn info(&self, message: &str) {
261 self.event(StatusLevel::Info, message);
262 }
263
264 pub fn success(&self, message: &str) {
265 self.event(StatusLevel::Success, message);
266 }
267
268 pub fn warn(&self, message: &str) {
269 self.event(StatusLevel::Warning, message);
270 }
271
272 pub fn error(&self, message: &str) {
273 self.event(StatusLevel::Error, message);
274 }
275
276 pub fn failure(&self, message: &str) {
277 self.event(StatusLevel::Failure, message);
278 }
279
280 pub fn debug(&self, message: &str) {
281 self.event(StatusLevel::Debug, message);
282 }
283}
284
285#[macro_export]
286macro_rules! info {
287 ($logger:expr, $fmt:literal) => { $logger.info(&format!($fmt)) };
288 ($logger:expr, $msg:expr) => { $logger.info($msg) };
289 ($logger:expr, $fmt:literal, $($arg:tt)*) => { $logger.info(&format!($fmt, $($arg)*)) };
290}
291
292#[macro_export]
293macro_rules! success {
294 ($logger:expr, $fmt:literal) => { $logger.success(&format!($fmt)) };
295 ($logger:expr, $msg:expr) => { $logger.success($msg) };
296 ($logger:expr, $fmt:literal, $($arg:tt)*) => { $logger.success(&format!($fmt, $($arg)*)) };
297}
298
299#[macro_export]
300macro_rules! warn {
301 ($logger:expr, $fmt:literal) => { $logger.warn(&format!($fmt)) };
302 ($logger:expr, $msg:expr) => { $logger.warn($msg) };
303 ($logger:expr, $fmt:literal, $($arg:tt)*) => { $logger.warn(&format!($fmt, $($arg)*)) };
304}
305
306#[macro_export]
307macro_rules! error {
308 ($logger:expr, $fmt:literal) => { $logger.error(&format!($fmt)) };
309 ($logger:expr, $msg:expr) => { $logger.error($msg) };
310 ($logger:expr, $fmt:literal, $($arg:tt)*) => { $logger.error(&format!($fmt, $($arg)*)) };
311}
312
313#[macro_export]
314macro_rules! failure {
315 ($logger:expr, $fmt:literal) => { $logger.failure(&format!($fmt)) };
316 ($logger:expr, $msg:expr) => { $logger.failure($msg) };
317 ($logger:expr, $fmt:literal, $($arg:tt)*) => { $logger.failure(&format!($fmt, $($arg)*)) };
318}
319
320#[macro_export]
321macro_rules! debug {
322 ($logger:expr, $fmt:literal) => { $logger.debug(&format!($fmt)) };
323 ($logger:expr, $msg:expr) => { $logger.debug($msg) };
324 ($logger:expr, $fmt:literal, $($arg:tt)*) => { $logger.debug(&format!($fmt, $($arg)*)) };
325}
326
327#[cfg(test)]
328#[path = "target_tests.rs"]
329mod tests;