mirror of https://github.com/breard-r/acmed.git
Rodolphe Bréard
3 weeks ago
Failed to extract signature
13 changed files with 352 additions and 112 deletions
-
11CHANGELOG.md
-
13Cargo.lock
-
2Cargo.toml
-
2config/01_log_stderr.toml
-
2config/02_log_syslog.toml
-
9man/en/acmed.8
-
48man/en/acmed.toml.5
-
54src/cli.rs
-
33src/config.rs
-
171src/config/log.rs
-
85src/log.rs
-
20src/main.rs
-
14tests/config/simple/simple.toml
@ -0,0 +1,2 @@ |
|||||
|
[[logging_facility]] |
||||
|
output = "stderr" |
@ -0,0 +1,2 @@ |
|||||
|
[[logging_facility]] |
||||
|
output = "syslog" |
@ -0,0 +1,171 @@ |
|||||
|
use anyhow::Result;
|
||||
|
use serde::{de, Deserialize, Deserializer};
|
||||
|
use std::path::PathBuf;
|
||||
|
|
||||
|
#[derive(Debug, Deserialize)]
|
||||
|
#[serde(deny_unknown_fields)]
|
||||
|
pub struct LoggingFacility {
|
||||
|
pub output: Facility,
|
||||
|
#[serde(default)]
|
||||
|
pub format: LogFormat,
|
||||
|
#[serde(default)]
|
||||
|
pub(in crate::config) level: Level,
|
||||
|
pub(in crate::config) ansi: Option<bool>,
|
||||
|
}
|
||||
|
|
||||
|
impl LoggingFacility {
|
||||
|
pub fn is_ansi(&self) -> bool {
|
||||
|
self.ansi.unwrap_or_else(|| self.output.default_ansi())
|
||||
|
}
|
||||
|
|
||||
|
pub fn get_level(&self) -> tracing::Level {
|
||||
|
self.level.clone().into()
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||
|
#[serde(remote = "Self")]
|
||||
|
pub enum Facility {
|
||||
|
File(PathBuf),
|
||||
|
StdErr,
|
||||
|
StdOut,
|
||||
|
SysLog,
|
||||
|
}
|
||||
|
|
||||
|
impl Facility {
|
||||
|
fn default_ansi(&self) -> bool {
|
||||
|
match self {
|
||||
|
Self::File(_) | Self::SysLog => false,
|
||||
|
Self::StdErr | Self::StdOut => true,
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
impl<'de> Deserialize<'de> for Facility {
|
||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
|
where
|
||||
|
D: Deserializer<'de>,
|
||||
|
{
|
||||
|
let unchecked = PathBuf::deserialize(deserializer)?;
|
||||
|
if unchecked.components().count() == 0 {
|
||||
|
return Err(de::Error::custom(
|
||||
|
"the logging facility output must not be empty",
|
||||
|
));
|
||||
|
}
|
||||
|
if unchecked == PathBuf::from("stderr") {
|
||||
|
return Ok(Facility::StdErr);
|
||||
|
}
|
||||
|
if unchecked == PathBuf::from("stdout") {
|
||||
|
return Ok(Facility::StdOut);
|
||||
|
}
|
||||
|
if unchecked == PathBuf::from("syslog") {
|
||||
|
return Ok(Facility::SysLog);
|
||||
|
}
|
||||
|
Ok(Facility::File(unchecked))
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
|
pub enum LogFormat {
|
||||
|
Compact,
|
||||
|
#[default]
|
||||
|
Full,
|
||||
|
Json,
|
||||
|
Pretty,
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
|
pub(in crate::config) enum Level {
|
||||
|
Error,
|
||||
|
Warn,
|
||||
|
#[default]
|
||||
|
Info,
|
||||
|
Debug,
|
||||
|
Trace,
|
||||
|
}
|
||||
|
|
||||
|
impl From<Level> for tracing::Level {
|
||||
|
fn from(lvl: Level) -> Self {
|
||||
|
match lvl {
|
||||
|
Level::Error => tracing::Level::ERROR,
|
||||
|
Level::Warn => tracing::Level::WARN,
|
||||
|
Level::Info => tracing::Level::INFO,
|
||||
|
Level::Debug => tracing::Level::DEBUG,
|
||||
|
Level::Trace => tracing::Level::TRACE,
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
mod tests {
|
||||
|
use super::*;
|
||||
|
use crate::config::load_str;
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty_logging_facility() {
|
||||
|
let res = load_str::<LoggingFacility>("");
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn empty_output() {
|
||||
|
let cfg = r#"output = """#;
|
||||
|
let res = load_str::<LoggingFacility>(cfg);
|
||||
|
assert!(res.is_err());
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn logging_facility_minimal() {
|
||||
|
let cfg = r#"output = "test.log""#;
|
||||
|
let c = load_str::<LoggingFacility>(cfg).unwrap();
|
||||
|
assert_eq!(c.output, Facility::File(PathBuf::from("test.log")));
|
||||
|
assert_eq!(c.format, LogFormat::Full);
|
||||
|
assert_eq!(c.level, Level::Info);
|
||||
|
assert_eq!(c.is_ansi(), false);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn logging_facility_stderr() {
|
||||
|
let cfg = r#"output = "stderr""#;
|
||||
|
let c = load_str::<LoggingFacility>(cfg).unwrap();
|
||||
|
assert_eq!(c.output, Facility::StdErr);
|
||||
|
assert_eq!(c.format, LogFormat::Full);
|
||||
|
assert_eq!(c.level, Level::Info);
|
||||
|
assert_eq!(c.is_ansi(), true);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn logging_facility_stdout() {
|
||||
|
let cfg = r#"output = "stdout""#;
|
||||
|
let c = load_str::<LoggingFacility>(cfg).unwrap();
|
||||
|
assert_eq!(c.output, Facility::StdOut);
|
||||
|
assert_eq!(c.format, LogFormat::Full);
|
||||
|
assert_eq!(c.level, Level::Info);
|
||||
|
assert_eq!(c.is_ansi(), true);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn logging_facility_syslog() {
|
||||
|
let cfg = r#"output = "syslog""#;
|
||||
|
let c = load_str::<LoggingFacility>(cfg).unwrap();
|
||||
|
assert_eq!(c.output, Facility::SysLog);
|
||||
|
assert_eq!(c.format, LogFormat::Full);
|
||||
|
assert_eq!(c.level, Level::Info);
|
||||
|
assert_eq!(c.is_ansi(), false);
|
||||
|
}
|
||||
|
|
||||
|
#[test]
|
||||
|
fn logging_facility_full() {
|
||||
|
let cfg = r#"
|
||||
|
output = "test.log"
|
||||
|
format = "json"
|
||||
|
level = "warn"
|
||||
|
ansi = true
|
||||
|
"#;
|
||||
|
let c = load_str::<LoggingFacility>(cfg).unwrap();
|
||||
|
assert_eq!(c.output, Facility::File(PathBuf::from("test.log")));
|
||||
|
assert_eq!(c.format, LogFormat::Json);
|
||||
|
assert_eq!(c.level, Level::Warn);
|
||||
|
assert_eq!(c.is_ansi(), true);
|
||||
|
}
|
||||
|
}
|
@ -1,39 +1,60 @@ |
|||||
use clap::ValueEnum;
|
|
||||
use tracing_subscriber::FmtSubscriber;
|
|
||||
|
use crate::config::{AcmedConfig, Facility, LogFormat};
|
||||
|
use anyhow::{Context, Result};
|
||||
|
use std::fs::File;
|
||||
|
use tracing_subscriber::prelude::*;
|
||||
|
use tracing_subscriber::{filter, Registry};
|
||||
|
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
|
||||
pub enum Level {
|
|
||||
Error,
|
|
||||
Warn,
|
|
||||
Info,
|
|
||||
Debug,
|
|
||||
Trace,
|
|
||||
|
macro_rules! add_output {
|
||||
|
($vec: ident, $facility: ident, $writer: expr) => {{
|
||||
|
let layer = tracing_subscriber::fmt::layer()
|
||||
|
.with_ansi($facility.is_ansi())
|
||||
|
.with_writer($writer);
|
||||
|
match $facility.format {
|
||||
|
LogFormat::Compact => push_output!($vec, $facility, layer.compact()),
|
||||
|
LogFormat::Full => push_output!($vec, $facility, layer),
|
||||
|
LogFormat::Json => push_output!($vec, $facility, layer.json()),
|
||||
|
LogFormat::Pretty => push_output!($vec, $facility, layer.pretty()),
|
||||
|
};
|
||||
|
}};
|
||||
}
|
}
|
||||
|
|
||||
impl Level {
|
|
||||
fn tracing(&self) -> tracing::Level {
|
|
||||
match self {
|
|
||||
Self::Error => tracing::Level::ERROR,
|
|
||||
Self::Warn => tracing::Level::WARN,
|
|
||||
Self::Info => tracing::Level::INFO,
|
|
||||
Self::Debug => tracing::Level::DEBUG,
|
|
||||
Self::Trace => tracing::Level::TRACE,
|
|
||||
}
|
|
||||
}
|
|
||||
|
macro_rules! push_output {
|
||||
|
($vec: ident, $facility: ident, $layer: expr) => {{
|
||||
|
let level = $facility.get_level();
|
||||
|
let layer = $layer.with_filter(filter::filter_fn(move |metadata| {
|
||||
|
metadata.target().starts_with("acmed") && *metadata.level() <= level
|
||||
|
}));
|
||||
|
$vec.push(layer.boxed());
|
||||
|
}};
|
||||
}
|
}
|
||||
|
|
||||
pub fn init(level: Level, is_syslog: bool) {
|
|
||||
if is_syslog {
|
|
||||
let identity = std::ffi::CStr::from_bytes_with_nul(crate::APP_IDENTITY).unwrap();
|
|
||||
let (options, facility) = Default::default();
|
|
||||
let syslog = syslog_tracing::Syslog::new(identity, options, facility)
|
|
||||
.expect("building syslog subscriber failed");
|
|
||||
tracing_subscriber::fmt().with_writer(syslog).init();
|
|
||||
} else {
|
|
||||
let subscriber = FmtSubscriber::builder()
|
|
||||
.with_max_level(level.tracing())
|
|
||||
.finish();
|
|
||||
tracing::subscriber::set_global_default(subscriber)
|
|
||||
.expect("setting default subscriber failed");
|
|
||||
|
pub fn init(config: &AcmedConfig) -> Result<()> {
|
||||
|
let mut layers = Vec::new();
|
||||
|
|
||||
|
for lf in &config.logging_facility {
|
||||
|
match &lf.output {
|
||||
|
Facility::File(path) => {
|
||||
|
let file = File::options()
|
||||
|
.create(true)
|
||||
|
.append(true)
|
||||
|
.open(path)
|
||||
|
.context(path.display().to_string())?;
|
||||
|
add_output!(layers, lf, file)
|
||||
|
}
|
||||
|
Facility::StdErr => add_output!(layers, lf, std::io::stderr),
|
||||
|
Facility::StdOut => add_output!(layers, lf, std::io::stdout),
|
||||
|
Facility::SysLog => {
|
||||
|
let identity = std::ffi::CStr::from_bytes_with_nul(crate::APP_IDENTITY).unwrap();
|
||||
|
let options = Default::default();
|
||||
|
let facility = syslog_tracing::Facility::Daemon;
|
||||
|
let syslog = syslog_tracing::Syslog::new(identity, options, facility).unwrap();
|
||||
|
add_output!(layers, lf, syslog)
|
||||
|
}
|
||||
|
}
|
||||
}
|
}
|
||||
|
|
||||
|
let subscriber = Registry::default().with(layers);
|
||||
|
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||
|
|
||||
|
Ok(())
|
||||
}
|
}
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue