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