qsu/
lumberjack.rs

1use std::{
2  env, fmt, fs,
3  io::Write,
4  path::{Path, PathBuf},
5  str::FromStr
6};
7
8use time::macros::format_description;
9
10use tracing_subscriber::{fmt::time::UtcTime, EnvFilter};
11
12#[cfg(feature = "clap")]
13use clap::ValueEnum;
14
15use crate::err::Error;
16
17
18#[derive(Default)]
19enum LogOut {
20  #[default]
21  Console,
22
23  #[cfg(windows)]
24  WinEvtLog { svcname: String }
25}
26
27/// Logging and tracing initialization.
28pub struct LumberJack {
29  init: bool,
30  log_out: LogOut,
31  log_level: LogLevel,
32  trace_filter: Option<String>,
33  //log_file: Option<PathBuf>,
34  trace_file: Option<PathBuf>
35}
36
37impl Default for LumberJack {
38  /// Create a default log/trace initialization.
39  ///
40  /// This will set the `log` log level to the value of the `LOG_LEVEL`
41  /// environment variable, or default to `warm` (if either not set or
42  /// invalid).
43  ///
44  /// The `tracing` trace level will use the environment variable
45  /// `TRACE_FILTER` in a similar manner, but defaults to `none`.
46  ///
47  /// If the environment variable `TRACE_FILE` is set the value will be the
48  /// used as the file name to write the trace logs to.
49  fn default() -> Self {
50    let log_level =
51      std::env::var("LOG_LEVEL").map_or(LogLevel::Warn, |level| {
52        level
53          .parse::<LogLevel>()
54          .map_or(LogLevel::Warn, |level| level)
55      });
56
57    let trace_file =
58      std::env::var("TRACE_FILE").map_or(None, |v| Some(PathBuf::from(v)));
59    let trace_filter = env::var("TRACE_FILTER").ok();
60
61    Self {
62      init: true,
63      log_out: LogOut::default(),
64      log_level,
65      trace_filter,
66      //log_file: None,
67      trace_file
68    }
69  }
70}
71
72impl LumberJack {
73  /// Create a [`LumberJack::default()`] object.
74  #[must_use]
75  pub fn new() -> Self {
76    Self::default()
77  }
78
79  /// Do not initialize logging/tracing.
80  ///
81  /// This is useful when running tests.
82  #[must_use]
83  pub fn noinit() -> Self {
84    Self {
85      init: false,
86      ..Default::default()
87    }
88  }
89
90  #[must_use]
91  pub const fn set_init(mut self, flag: bool) -> Self {
92    self.init = flag;
93    self
94  }
95
96  /// Load logging/tracing information from a service Parameters subkey.
97  ///
98  /// # Errors
99  /// `Error::SubSystem`
100  #[cfg(windows)]
101  pub fn from_winsvc(svcname: &str) -> Result<Self, Error> {
102    let params = crate::rt::winsvc::get_service_param(svcname)?;
103    let loglevel = params
104      .get_value::<String, &str>("LogLevel")
105      .unwrap_or_else(|_| String::from("warn"))
106      .parse::<LogLevel>()
107      .unwrap_or(LogLevel::Warn);
108    let tracefilter = params.get_value::<String, &str>("TraceFilter");
109    let tracefile = params.get_value::<String, &str>("TraceFile");
110
111    let mut this = Self::new().log_level(loglevel);
112    this.log_out = LogOut::WinEvtLog {
113      svcname: svcname.to_string()
114    };
115    let this =
116      if let (Ok(tracefilter), Ok(tracefile)) = (tracefilter, tracefile) {
117        this.trace_filter(tracefilter).trace_file(tracefile)
118      } else {
119        this
120      };
121
122    Ok(this)
123  }
124
125  /// Set the `log` logging level.
126  #[must_use]
127  pub const fn log_level(mut self, level: LogLevel) -> Self {
128    self.log_level = level;
129    self
130  }
131
132  /// Set the `tracing` log level.
133  #[must_use]
134  #[allow(clippy::needless_pass_by_value)]
135  pub fn trace_filter(mut self, filter: impl ToString) -> Self {
136    self.trace_filter = Some(filter.to_string());
137    self
138  }
139
140  /// Set a file to which `tracing` log entries are written (rather than to
141  /// write to console).
142  #[must_use]
143  pub fn trace_file<P>(mut self, fname: P) -> Self
144  where
145    P: AsRef<Path>
146  {
147    self.trace_file = Some(fname.as_ref().to_path_buf());
148    self
149  }
150
151  /// Commit requested settings to `log` and `tracing`.
152  ///
153  /// # Errors
154  /// Any initialization error is translated into [`Error::LumberJack`].
155  pub fn init(self) -> Result<(), Error> {
156    if self.init {
157      match self.log_out {
158        LogOut::Console => {
159          init_console_logging()?;
160        }
161        #[cfg(windows)]
162        LogOut::WinEvtLog { svcname } => {
163          eventlog::init(&svcname, log::Level::Trace)?;
164          log::set_max_level(self.log_level.into());
165        }
166      }
167
168      if let Some(fname) = self.trace_file {
169        init_file_tracing(fname, self.trace_filter.as_deref());
170      } else {
171        init_console_tracing(self.trace_filter.as_deref());
172      }
173    }
174    Ok(())
175  }
176}
177
178
179#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
180#[cfg_attr(feature = "clap", derive(ValueEnum))]
181pub enum LogLevel {
182  /// No logging.
183  #[cfg_attr(feature = "clap", clap(name = "off"))]
184  Off,
185
186  /// Log errors.
187  #[cfg_attr(feature = "clap", clap(name = "error"))]
188  Error,
189
190  /// Log warnings and errors.
191  #[cfg_attr(feature = "clap", clap(name = "warn"))]
192  #[default]
193  Warn,
194
195  /// Log info, warnings and errors.
196  #[cfg_attr(feature = "clap", clap(name = "info"))]
197  Info,
198
199  /// Log debug, info, warnings and errors.
200  #[cfg_attr(feature = "clap", clap(name = "debug"))]
201  Debug,
202
203  /// Log trace, debug, info, warninga and errors.
204  #[cfg_attr(feature = "clap", clap(name = "trace"))]
205  Trace
206}
207
208impl FromStr for LogLevel {
209  type Err = String;
210
211  fn from_str(s: &str) -> Result<Self, Self::Err> {
212    match s {
213      "off" => Ok(Self::Off),
214      "error" => Ok(Self::Error),
215      "warn" => Ok(Self::Warn),
216      "info" => Ok(Self::Info),
217      "debug" => Ok(Self::Debug),
218      "trace" => Ok(Self::Trace),
219      _ => Err(format!("Unknown log level '{s}'"))
220    }
221  }
222}
223
224impl fmt::Display for LogLevel {
225  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226    let s = match self {
227      Self::Off => "off",
228      Self::Error => "error",
229      Self::Warn => "warn",
230      Self::Info => "info",
231      Self::Debug => "debug",
232      Self::Trace => "trace"
233    };
234    write!(f, "{s}")
235  }
236}
237
238impl From<LogLevel> for log::LevelFilter {
239  fn from(ll: LogLevel) -> Self {
240    match ll {
241      LogLevel::Off => Self::Off,
242      LogLevel::Error => Self::Error,
243      LogLevel::Warn => Self::Warn,
244      LogLevel::Info => Self::Info,
245      LogLevel::Debug => Self::Debug,
246      LogLevel::Trace => Self::Trace
247    }
248  }
249}
250
251impl From<LogLevel> for Option<tracing::Level> {
252  fn from(ll: LogLevel) -> Self {
253    match ll {
254      LogLevel::Off => None,
255      LogLevel::Error => Some(tracing::Level::ERROR),
256      LogLevel::Warn => Some(tracing::Level::WARN),
257      LogLevel::Info => Some(tracing::Level::INFO),
258      LogLevel::Debug => Some(tracing::Level::DEBUG),
259      LogLevel::Trace => Some(tracing::Level::TRACE)
260    }
261  }
262}
263
264
265/// Initialize logging for output to console.
266pub fn init_console_logging() -> Result<(), Error> {
267  //
268  // Get level from environment variable LOG_LEVEL.
269  //
270  let lf: log::LevelFilter = if let Ok(val) = std::env::var("LOG_LEVEL") {
271    if let Ok(ll) = val.parse::<LogLevel>() {
272      ll.into()
273    } else {
274      return Err(Error::bad_format("Unknown log level specified"));
275    }
276  } else {
277    // Default to "warn" level
278    log::LevelFilter::Warn
279  };
280
281  env_logger::Builder::new()
282    //.parse_env(&std::env::var("LOG_LEVEL").unwrap_or_default())
283    .format(|buf, record| {
284      writeln!(
285        buf,
286        "{} [{}] - {}",
287        chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
288        record.level(),
289        record.args()
290      )
291    })
292    .filter(None, lf)
293    .init();
294
295  Ok(())
296}
297
298
299pub fn init_console_tracing(filter: Option<&str>) {
300  // When running on console, then disable tracing by default
301  let filter = filter.map_or_else(|| EnvFilter::new("none"), EnvFilter::new);
302
303  tracing_subscriber::fmt()
304    .with_env_filter(filter)
305    //.with_timer(timer)
306    .init();
307}
308
309
310/// Optionally set up tracing to a file.
311///
312/// This function will attempt to set up tracing to a file.  There are three
313/// conditions that must be true in order for tracing to a file to be enabled:
314/// - A filename must be specified (either via the `fname` argument or the
315///   `TRACE_FILE` environment variable).
316/// - A trace filter must be specified (either via the `filter` argument or the
317///   `TRACE_FILTER` environment variable).
318/// - The file name must be openable for writing.
319///
320/// Because tracing is an optional feature and intended for development only,
321/// this function will enable tracing if possible, and silently ignore and
322/// errors.
323pub fn init_file_tracing<P>(fname: P, filter: Option<&str>)
324where
325  P: AsRef<Path>
326{
327  //
328  // If both a trace file name and a trace level
329  //
330  let timer = UtcTime::new(format_description!(
331    "[year]-[month]-[day] [hour]:[minute]:[second]"
332  ));
333
334  let Ok(f) = fs::OpenOptions::new().create(true).append(true).open(fname)
335  else {
336    return;
337  };
338
339  //
340  // If TRACING_FILE is set, then default to filter out at warn level.
341  // Disable all tracing if TRACE_FILTER is not set
342  //
343  let filter = filter.map_or_else(|| EnvFilter::new("warn"), EnvFilter::new);
344
345  tracing_subscriber::fmt()
346    .with_env_filter(filter)
347    .with_writer(f)
348    .with_ansi(false)
349    .with_timer(timer)
350    .init();
351
352  //tracing_subscriber::registry().with(filter).init();
353}
354
355// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :