merfolk_frontend_logger 0.1.0

A `Frontend` for merfolk using the log crate.
Documentation
use merfolk::{
  interfaces::{Backend, Frontend},
  Call, Reply,
};

use wildmatch::WildMatch;

use std::{marker::PhantomData, sync::Arc};

use anyhow::Result;
use thiserror::Error;

use log::{Level, Metadata, Record};

#[derive(Debug, Error)]
pub enum Error {
  #[error("backend error: {0}")]
  FromBackend(#[from] anyhow::Error),
  #[error("could not set logger: {0}")]
  SetLogger(#[from] log::SetLoggerError),
  #[error("no sink provided in init()")]
  NoSink,
  #[error("error parsing nog level {level}")]
  LevelParseError { level: String },
}

struct LoggerInstance {
  level: Level,
  ignore_targets: Arc<Option<Vec<&'static str>>>,
  allow_targets: Arc<Option<Vec<&'static str>>>,

  #[allow(clippy::type_complexity)]
  call: Arc<dyn Fn(&Record) + 'static + Send + Sync>,
}

#[derive(derive_builder::Builder)]
#[builder(pattern = "owned")]
pub struct Logger<'a, B: Backend> {
  #[builder(setter(into, strip_option), default = "None")]
  level: Option<Level>,

  #[builder(setter(name = "sink_setter", strip_option), private, default = "None")]
  sink: Option<Box<dyn Fn(Level, String)>>,

  #[builder(setter(name = "ignore_targets_setter", strip_option), private, default = "Arc::new(None)")]
  ignore_targets: Arc<Option<Vec<&'static str>>>,

  #[builder(setter(name = "allow_targets_setter", strip_option), private, default = "Arc::new(None)")]
  allow_targets: Arc<Option<Vec<&'static str>>>,

  #[builder(private, default = "PhantomData")]
  __phantom: std::marker::PhantomData<&'a B>,
}

impl<'a, B: Backend> LoggerBuilder<'a, B> {
  pub fn sink<S: 'static + Fn(Level, String)>(self, value: S) -> Self {
    self.sink_setter(Box::new(value))
  }

  pub fn ignore_targets(self, mut value: Vec<&'static str>) -> Self {
    value.push("merfolk");

    self.ignore_targets_setter(Arc::new(Some(value)))
  }

  pub fn allow_targets(self, value: Vec<&'static str>) -> Self {
    self.allow_targets_setter(Arc::new(Some(value)))
  }
}

impl<'a, B: Backend> Logger<'a, B> {
  pub fn builder() -> LoggerBuilder<'a, B> {
    LoggerBuilder::default()
  }
}

unsafe impl<'a, T: Backend> Send for Logger<'a, T> {}

impl log::Log for LoggerInstance {
  fn enabled(&self, metadata: &Metadata) -> bool {
    metadata.level() <= self.level
      && !metadata.target().is_empty()
      && if let Some(ignore_targets) = self.ignore_targets.as_ref() {
        !ignore_targets.iter().any(|t| WildMatch::new(t).is_match(&metadata.target()))
      } else {
        true
      }
      && if let Some(allow_targets) = self.allow_targets.as_ref() {
        allow_targets.iter().any(|t| WildMatch::new(t).is_match(&metadata.target()))
      } else {
        true
      }
  }

  fn log(&self, record: &Record) {
    if self.enabled(record.metadata()) {
      self.call.as_ref()(record);
    }
  }

  fn flush(&self) {}
}

impl<'a, B: Backend> Frontend for Logger<'a, B> {
  type Backend = B;

  fn register<T>(&mut self, caller: T) -> Result<()>
  where
    T: Fn(Call<B::Intermediate>) -> Result<Reply<B::Intermediate>> + 'static + Send + Sync,
  {
    if let Some(level) = self.level {
      log::set_boxed_logger(Box::new(LoggerInstance {
        level,
        ignore_targets: self.ignore_targets.clone(),
        allow_targets: self.allow_targets.clone(),
        call: Arc::new(move |record: &Record| {
          let args = match B::serialize(&record.args().to_string()) {
            Ok(ser) => ser,
            Err(err) => B::serialize(&err.to_string()).unwrap(),
          };

          caller(Call {
            procedure: record.level().to_string(),
            payload: args,
          })
          .ok();
        }),
      }))
      .map_err(Error::SetLogger)?;
      log::set_max_level(level.to_level_filter());
    }
    Ok(())
  }

  fn receive(&self, call: Call<B::Intermediate>) -> Result<Reply<B::Intermediate>> {
    self.sink.as_ref().ok_or(Error::NoSink)?(
      match call.procedure.as_str() {
        "ERROR" => Level::Error,
        "WARN" => Level::Warn,
        "INFO" => Level::Info,
        "DEBUG" => Level::Debug,
        "TRACE" => Level::Trace,
        err => return Err(Error::LevelParseError { level: err.to_string() }.into()),
      },
      B::deserialize(&call.payload)?,
    );

    Ok(Reply { payload: B::serialize(&())? })
  }
}