cli/
log.rs

1/*---------------------------------------------------------------------------------------------
2 *  Copyright (c) Microsoft Corporation. All rights reserved.
3 *  Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5
6use chrono::Local;
7use opentelemetry::{
8	sdk::trace::{Tracer, TracerProvider},
9	trace::{SpanBuilder, Tracer as TraitTracer, TracerProvider as TracerProviderTrait},
10};
11use serde::{Deserialize, Serialize};
12use std::fmt;
13use std::{
14	io::Write,
15	sync::atomic::{AtomicU32, Ordering},
16};
17use std::{path::Path, sync::Arc};
18
19use crate::constants::COLORS_ENABLED;
20
21static INSTANCE_COUNTER: AtomicU32 = AtomicU32::new(0);
22
23// Gets a next incrementing number that can be used in logs
24pub fn next_counter() -> u32 {
25	INSTANCE_COUNTER.fetch_add(1, Ordering::SeqCst)
26}
27
28// Log level
29#[derive(
30	clap::ValueEnum, PartialEq, Eq, PartialOrd, Clone, Copy, Debug, Serialize, Deserialize, Default,
31)]
32pub enum Level {
33	Trace = 0,
34	Debug,
35	#[default]
36	Info,
37	Warn,
38	Error,
39	Critical,
40	Off,
41}
42
43impl fmt::Display for Level {
44	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
45		match self {
46			Level::Critical => write!(f, "critical"),
47			Level::Debug => write!(f, "debug"),
48			Level::Error => write!(f, "error"),
49			Level::Info => write!(f, "info"),
50			Level::Off => write!(f, "off"),
51			Level::Trace => write!(f, "trace"),
52			Level::Warn => write!(f, "warn"),
53		}
54	}
55}
56
57impl Level {
58	pub fn name(&self) -> Option<&str> {
59		match self {
60			Level::Trace => Some("trace"),
61			Level::Debug => Some("debug"),
62			Level::Info => Some("info"),
63			Level::Warn => Some("warn"),
64			Level::Error => Some("error"),
65			Level::Critical => Some("critical"),
66			Level::Off => None,
67		}
68	}
69
70	pub fn color_code(&self) -> Option<&str> {
71		if !*COLORS_ENABLED {
72			return None;
73		}
74
75		match self {
76			Level::Trace => None,
77			Level::Debug => Some("\x1b[36m"),
78			Level::Info => Some("\x1b[35m"),
79			Level::Warn => Some("\x1b[33m"),
80			Level::Error => Some("\x1b[31m"),
81			Level::Critical => Some("\x1b[31m"),
82			Level::Off => None,
83		}
84	}
85
86	pub fn to_u8(self) -> u8 {
87		self as u8
88	}
89}
90
91pub fn new_tunnel_prefix() -> String {
92	format!("[tunnel.{}]", next_counter())
93}
94
95pub fn new_code_server_prefix() -> String {
96	format!("[codeserver.{}]", next_counter())
97}
98
99pub fn new_rpc_prefix() -> String {
100	format!("[rpc.{}]", next_counter())
101}
102
103// Base logger implementation
104#[derive(Clone)]
105pub struct Logger {
106	tracer: Arc<Tracer>,
107	sink: Vec<Box<dyn LogSink>>,
108	prefix: Option<String>,
109}
110
111// Copy trick from https://stackoverflow.com/a/30353928
112pub trait LogSinkClone {
113	fn clone_box(&self) -> Box<dyn LogSink>;
114}
115
116impl<T> LogSinkClone for T
117where
118	T: 'static + LogSink + Clone,
119{
120	fn clone_box(&self) -> Box<dyn LogSink> {
121		Box::new(self.clone())
122	}
123}
124
125pub trait LogSink: LogSinkClone + Sync + Send {
126	fn write_log(&self, level: Level, prefix: &str, message: &str);
127	fn write_result(&self, message: &str);
128}
129
130impl Clone for Box<dyn LogSink> {
131	fn clone(&self) -> Box<dyn LogSink> {
132		self.clone_box()
133	}
134}
135
136/// The basic log sink that writes output to stdout, with colors when relevant.
137#[derive(Clone)]
138pub struct StdioLogSink {
139	level: Level,
140}
141
142impl LogSink for StdioLogSink {
143	fn write_log(&self, level: Level, prefix: &str, message: &str) {
144		if level < self.level {
145			return;
146		}
147
148		emit(level, prefix, message);
149	}
150
151	fn write_result(&self, message: &str) {
152		println!("{}", message);
153	}
154}
155
156#[derive(Clone)]
157pub struct FileLogSink {
158	level: Level,
159	file: Arc<std::sync::Mutex<std::fs::File>>,
160}
161
162const FILE_LOG_SIZE_LIMIT: u64 = 1024 * 1024 * 10; // 10MB
163
164impl FileLogSink {
165	pub fn new(level: Level, path: &Path) -> std::io::Result<Self> {
166		// Truncate the service log occasionally to avoid growing infinitely
167		if matches!(path.metadata(), Ok(m) if m.len() > FILE_LOG_SIZE_LIMIT) {
168			// ignore errors, can happen if another process is writing right now
169			let _ = std::fs::remove_file(path);
170		}
171
172		let file = std::fs::OpenOptions::new()
173			.append(true)
174			.create(true)
175			.open(path)?;
176
177		Ok(Self {
178			level,
179			file: Arc::new(std::sync::Mutex::new(file)),
180		})
181	}
182}
183
184impl LogSink for FileLogSink {
185	fn write_log(&self, level: Level, prefix: &str, message: &str) {
186		if level < self.level {
187			return;
188		}
189
190		let line = format(level, prefix, message, false);
191
192		// ignore any errors, not much we can do if logging fails...
193		self.file.lock().unwrap().write_all(line.as_bytes()).ok();
194	}
195
196	fn write_result(&self, _message: &str) {}
197}
198
199impl Logger {
200	pub fn test() -> Self {
201		Self {
202			tracer: Arc::new(TracerProvider::builder().build().tracer("codeclitest")),
203			sink: vec![],
204			prefix: None,
205		}
206	}
207
208	pub fn new(tracer: Tracer, level: Level) -> Self {
209		Self {
210			tracer: Arc::new(tracer),
211			sink: vec![Box::new(StdioLogSink { level })],
212			prefix: None,
213		}
214	}
215
216	pub fn span(&self, name: &str) -> SpanBuilder {
217		self.tracer.span_builder(format!("serverlauncher/{}", name))
218	}
219
220	pub fn tracer(&self) -> &Tracer {
221		&self.tracer
222	}
223
224	pub fn emit(&self, level: Level, message: &str) {
225		let prefix = self.prefix.as_deref().unwrap_or("");
226		for sink in &self.sink {
227			sink.write_log(level, prefix, message);
228		}
229	}
230
231	pub fn result(&self, message: impl AsRef<str>) {
232		for sink in &self.sink {
233			sink.write_result(message.as_ref());
234		}
235	}
236
237	pub fn prefixed(&self, prefix: &str) -> Logger {
238		Logger {
239			prefix: Some(match &self.prefix {
240				Some(p) => format!("{}{} ", p, prefix),
241				None => format!("{} ", prefix),
242			}),
243			..self.clone()
244		}
245	}
246
247	/// Creates a new logger with the additional log sink added.
248	pub fn tee<T>(&self, sink: T) -> Logger
249	where
250		T: LogSink + 'static,
251	{
252		let mut new_sinks = self.sink.clone();
253		new_sinks.push(Box::new(sink));
254
255		Logger {
256			sink: new_sinks,
257			..self.clone()
258		}
259	}
260
261	/// Creates a new logger with the sink replace with the given sink.
262	pub fn with_sink<T>(&self, sink: T) -> Logger
263	where
264		T: LogSink + 'static,
265	{
266		Logger {
267			sink: vec![Box::new(sink)],
268			..self.clone()
269		}
270	}
271
272	pub fn get_download_logger<'a>(&'a self, prefix: &'static str) -> DownloadLogger<'a> {
273		DownloadLogger {
274			prefix,
275			logger: self,
276		}
277	}
278}
279
280pub struct DownloadLogger<'a> {
281	prefix: &'static str,
282	logger: &'a Logger,
283}
284
285impl<'a> crate::util::io::ReportCopyProgress for DownloadLogger<'a> {
286	fn report_progress(&mut self, bytes_so_far: u64, total_bytes: u64) {
287		if total_bytes > 0 {
288			self.logger.emit(
289				Level::Trace,
290				&format!(
291					"{} {}/{} ({:.0}%)",
292					self.prefix,
293					bytes_so_far,
294					total_bytes,
295					(bytes_so_far as f64 / total_bytes as f64) * 100.0,
296				),
297			);
298		} else {
299			self.logger.emit(
300				Level::Trace,
301				&format!("{} {}/{}", self.prefix, bytes_so_far, total_bytes,),
302			);
303		}
304	}
305}
306
307fn format(level: Level, prefix: &str, message: &str, use_colors: bool) -> String {
308	let current = Local::now();
309	let timestamp = current.format("%Y-%m-%d %H:%M:%S").to_string();
310
311	let name = level.name().unwrap();
312
313	if use_colors {
314		if let Some(c) = level.color_code() {
315			return format!(
316				"\x1b[2m[{}]\x1b[0m {}{}\x1b[0m {}{}\n",
317				timestamp, c, name, prefix, message
318			);
319		}
320	}
321
322	format!("[{}] {} {}{}\n", timestamp, name, prefix, message)
323}
324
325pub fn emit(level: Level, prefix: &str, message: &str) {
326	let line = format(level, prefix, message, *COLORS_ENABLED);
327	if level == Level::Trace && *COLORS_ENABLED {
328		print!("\x1b[2m{}\x1b[0m", line);
329	} else {
330		print!("{}", line);
331	}
332}
333
334/// Installs the logger instance as the global logger for the 'log' service.
335/// Replaces any existing registered logger. Note that the logger will be leaked/
336pub fn install_global_logger(log: Logger) {
337	log::set_logger(Box::leak(Box::new(RustyLogger(log))))
338		.map(|()| log::set_max_level(log::LevelFilter::Debug))
339		.expect("expected to make logger");
340}
341
342/// Logger that uses the common rust "log" crate and directs back to one of
343/// our managed loggers.
344struct RustyLogger(Logger);
345
346impl log::Log for RustyLogger {
347	fn enabled(&self, metadata: &log::Metadata) -> bool {
348		metadata.level() <= log::Level::Debug
349	}
350
351	fn log(&self, record: &log::Record) {
352		if !self.enabled(record.metadata()) {
353			return;
354		}
355
356		// exclude noisy log modules:
357		let src = match record.module_path() {
358			Some("russh::cipher" | "russh::negotiation" | "russh::kex::dh") => return,
359			Some(s) => s,
360			None => "<unknown>",
361		};
362
363		self.0.emit(
364			match record.level() {
365				log::Level::Debug => Level::Debug,
366				log::Level::Error => Level::Error,
367				log::Level::Info => Level::Info,
368				log::Level::Trace => Level::Trace,
369				log::Level::Warn => Level::Warn,
370			},
371			&format!("[{}] {}", src, record.args()),
372		);
373	}
374
375	fn flush(&self) {}
376}
377
378#[macro_export]
379macro_rules! error {
380    ($logger:expr, $str:expr) => {
381        $logger.emit(log::Level::Error, $str)
382     };
383     ($logger:expr, $($fmt:expr),+) => {
384        $logger.emit(log::Level::Error, &format!($($fmt),+))
385     };
386 }
387
388#[macro_export]
389macro_rules! trace {
390     ($logger:expr, $str:expr) => {
391         $logger.emit(log::Level::Trace, $str)
392     };
393     ($logger:expr, $($fmt:expr),+) => {
394         $logger.emit(log::Level::Trace, &format!($($fmt),+))
395     };
396 }
397
398#[macro_export]
399macro_rules! debug {
400     ($logger:expr, $str:expr) => {
401         $logger.emit(log::Level::Debug, $str)
402     };
403     ($logger:expr, $($fmt:expr),+) => {
404         $logger.emit(log::Level::Debug, &format!($($fmt),+))
405     };
406 }
407
408#[macro_export]
409macro_rules! info {
410     ($logger:expr, $str:expr) => {
411         $logger.emit(log::Level::Info, $str)
412     };
413     ($logger:expr, $($fmt:expr),+) => {
414         $logger.emit(log::Level::Info, &format!($($fmt),+))
415     };
416 }
417
418#[macro_export]
419macro_rules! warning {
420     ($logger:expr, $str:expr) => {
421         $logger.emit(log::Level::Warn, $str)
422     };
423     ($logger:expr, $($fmt:expr),+) => {
424         $logger.emit(log::Level::Warn, &format!($($fmt),+))
425     };
426 }
427
428#[macro_export]
429macro_rules! span {
430	($logger:expr, $span:expr, $func:expr) => {{
431		use opentelemetry::trace::TraceContextExt;
432
433		let span = $span.start($logger.tracer());
434		let cx = opentelemetry::Context::current_with_span(span);
435		let guard = cx.clone().attach();
436		let t = $func;
437
438		if let Err(e) = &t {
439			cx.span().record_error(e);
440		}
441
442		std::mem::drop(guard);
443
444		t
445	}};
446}
447
448#[macro_export]
449macro_rules! spanf {
450	($logger:expr, $span:expr, $func:expr) => {{
451		use opentelemetry::trace::{FutureExt, TraceContextExt};
452
453		let span = $span.start($logger.tracer());
454		let cx = opentelemetry::Context::current_with_span(span);
455		let t = $func.with_context(cx.clone()).await;
456
457		if let Err(e) = &t {
458			cx.span().record_error(e);
459		}
460
461		cx.span().end();
462
463		t
464	}};
465}