lumbermill 0.2.0

Simple structured logging
Documentation
use std::{path::PathBuf, sync::OnceLock};

use crate::{
  file::FileLogger,
  log::{Log, LogFormat, LogLevel},
  stdout::StdoutLogger,
  RollInterval,
};

pub static LOGGER: OnceLock<Logger> = OnceLock::new();

#[derive(Debug)]
pub struct Logger {
  level: LogLevel,
  format: LogFormat,
  stdout: Option<StdoutLogger>,
  file: Option<FileLogger>,
}

impl Logger {
  pub fn new() -> Self {
    Self::default()
  }

  pub fn builder() -> Self {
    Self::default()
  }
}

impl Logger {
  pub fn level(mut self, level: LogLevel) -> Self {
    self.level = level;
    self
  }

  pub fn format(mut self, format: LogFormat) -> Self {
    self.format = format;
    self
  }

  pub fn pretty(self) -> Self {
    self.format(LogFormat::Pretty)
  }

  pub fn pretty_structured(self) -> Self {
    self.format(LogFormat::PrettyStructured)
  }

  pub fn compact(self) -> Self {
    self.format(LogFormat::Compact)
  }

  pub fn json(self) -> Self {
    self.format(LogFormat::Json)
  }

  pub fn stdout(mut self, s: bool) -> Self {
    if s {
      self.stdout = Some(StdoutLogger::new())
    } else {
      self.stdout = None;
    }

    self
  }

  pub fn file<Dir: Into<PathBuf>>(
    mut self,
    directory: Dir,
    roll_interval: RollInterval,
  ) -> Self {
    self.file = Some(FileLogger::new(directory, roll_interval));
    self
  }

  pub fn init(self) {
    LOGGER
      .set(self)
      .expect("Loggers can only be initialized once");
  }

  pub fn log(&self, log: Log) {
    if log.level < self.level {
      return;
    }

    _ = self
      .stdout
      .as_ref()
      .map(|logger| logger.log(&log, &self.format));

    _ = self
      .file
      .as_ref()
      .map(|logger| logger.log(&log, &self.format));
  }
}

impl Default for Logger {
  fn default() -> Self {
    Self {
      level: LogLevel::Info,
      format: LogFormat::Pretty,
      stdout: Some(StdoutLogger::new()),
      file: None,
    }
  }
}

#[cfg(test)]
mod tests {
  use crate::{log::Log, OffsetDateTime, LOGGER};

  #[test]
  fn stdout() {
    LOGGER.get().unwrap().log(Log {
      timestamp: OffsetDateTime::now_utc(),
      level: crate::LogLevel::Trace,
      module: module_path!(),
      file: file!(),
      line: line!(),
      kv: &[
        ("service", format_args!("{}", "toph")),
        ("node", format_args!("{}", "fra")),
        (
          "message",
          format_args!("Accepted TCP connection from 172.45.22.190:62498"),
        ),
      ],
    });

    LOGGER.get().unwrap().log(Log {
      timestamp: OffsetDateTime::now_utc(),
      level: crate::LogLevel::Debug,
      module: module_path!(),
      file: file!(),
      line: line!(),
      kv: &[
        ("addr", format_args!("{}", "https://postgres.org")),
        ("message", format_args!("Established connection to DB")),
      ],
    });

    LOGGER.get().unwrap().log(Log {
      timestamp: OffsetDateTime::now_utc(),
      level: crate::LogLevel::Info,
      module: module_path!(),
      file: file!(),
      line: line!(),
      kv: &[
        ("addr", format_args!("{}", "0.0.0.0")),
        ("message", format_args!("Listening on :7096")),
      ],
    });

    LOGGER.get().unwrap().log(Log {
      timestamp: OffsetDateTime::now_utc(),
      level: crate::LogLevel::Warn,
      module: module_path!(),
      file: file!(),
      line: line!(),
      kv: &[
        ("count", format_args!("{}", "9001")),
        ("message", format_args!("Too many items in queue")),
      ],
    });

    LOGGER.get().unwrap().log(Log {
      timestamp: OffsetDateTime::now_utc(),
      level: crate::LogLevel::Error,
      module: module_path!(),
      file: file!(),
      line: line!(),
      kv: &[
        ("reason", format_args!("{}", "No connectivity")),
        ("message", format_args!("Database connection dropped")),
      ],
    });

    LOGGER.get().unwrap().log(Log {
      timestamp: OffsetDateTime::now_utc(),
      level: crate::LogLevel::Fatal,
      module: module_path!(),
      file: file!(),
      line: line!(),
      kv: &[
        ("usage", format_args!("{}", "128MB")),
        ("message", format_args!("Out of memory")),
      ],
    });
  }
}