From 1fe846932d3c64f1d64756af3eb25a41c63c2996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolphe=20Br=C3=A9ard?= Date: Thu, 2 Jan 2025 13:27:44 +0100 Subject: [PATCH] Defines the log in the configuration file, not in the CLI --- CHANGELOG.md | 11 ++ Cargo.lock | 13 +++ Cargo.toml | 2 +- config/01_log_stderr.toml | 2 + config/02_log_syslog.toml | 2 + man/en/acmed.8 | 9 -- man/en/acmed.toml.5 | 48 ++++++++- src/cli.rs | 54 ---------- src/config.rs | 33 ++++-- src/config/log.rs | 171 ++++++++++++++++++++++++++++++++ src/log.rs | 85 ++++++++++------ src/main.rs | 20 ++-- tests/config/simple/simple.toml | 14 +++ 13 files changed, 352 insertions(+), 112 deletions(-) create mode 100644 config/01_log_stderr.toml create mode 100644 config/02_log_syslog.toml create mode 100644 src/config/log.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f8d216..10a99f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Logging facilities can now be defined in the configuration file. + ### Changed - Instead of loading a default configuration file, ACMEd now loads all the @@ -24,12 +28,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 groups has been replaced by tables. - The name of user-defined hooks and groups cannot start with `internal:`, which is now reserved for internal hooks. +- The default logging level is now info. ### Removed - OpenSSL support has been removed. - tacd has been removed. - The `include` directive has been removed from the configuration. +- The `acmed` command does not accepts the `--log-stderr`, `--log-syslog` and + `--log-level` arguments anymore. + +### Fixed + +- Logging to syslog now uses the daemon facility. ## [0.24.0] - 2024-12-21 diff --git a/Cargo.lock b/Cargo.lock index 2388b07..3007d85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,6 +1338,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" @@ -1345,9 +1355,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "nu-ansi-term", + "serde", + "serde_json", "sharded-slab", "thread_local", "tracing-core", + "tracing-serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8f79217..1bd6387 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ serde = { version = "1.0.216", default-features = false, features = ["derive"] } syslog-tracing = { version = "0.3.1", default-features = false } tokio = { version = "1.42.0", default-features = false, features = ["rt", "rt-multi-thread", "time", "sync"] } tracing = { version = "0.1.41", default-features = false, features = ["std", "attributes"] } -tracing-subscriber = { version = "0.3.19", default-features = false, features = ["ansi", "fmt", "std"] } +tracing-subscriber = { version = "0.3.19", default-features = false, features = ["ansi", "fmt", "std", "registry", "json"] } walkdir = { version = "2.5.0", default-features = false } [target.'cfg(unix)'.dependencies] diff --git a/config/01_log_stderr.toml b/config/01_log_stderr.toml new file mode 100644 index 0000000..087d0a4 --- /dev/null +++ b/config/01_log_stderr.toml @@ -0,0 +1,2 @@ +[[logging_facility]] +output = "stderr" diff --git a/config/02_log_syslog.toml b/config/02_log_syslog.toml new file mode 100644 index 0000000..7df7c1a --- /dev/null +++ b/config/02_log_syslog.toml @@ -0,0 +1,2 @@ +[[logging_facility]] +output = "syslog" diff --git a/man/en/acmed.8 b/man/en/acmed.8 index 02c78b5..16214a7 100644 --- a/man/en/acmed.8 +++ b/man/en/acmed.8 @@ -15,9 +15,6 @@ .Op Fl c|--config Ar DIR .Op Fl f|--foreground .Op Fl h|--help -.Op Fl -log-stderr -.Op Fl -log-syslog -.Op Fl -log-level Ar LEVEL .Op Fl -no-pid-file .Op Fl -pid-file Ar FILE .Op Fl -root-cert Ar FILE @@ -37,12 +34,6 @@ Specify an alternative configuration directory. Runs in the foreground. .It Fl h, -help Prints help information. -.It Fl -log-stderr -Prints log messages to the standard error output. -.It Fl -log-syslog -Sends log messages via syslog. -.It Fl -log-level Ar LEVEL -Specify the log level. Possible values: error, warn, info, debug and trace. .It Fl -no-pid-file Do not create any PID file. .It Fl -pid-file Ar FILE diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 7754f4d..b12cde0 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -372,8 +372,52 @@ file-pre-edit post-operation .El .El +.It Ic logging_facility +Table where each element defines a logging facility. Possible fields and values are: +.Bl -tag +.It Cm output Ar string +Path of the file where the log will be written. If the file does not exists, it will be created. It the file already exists, logs will be appended at the end of the file. The following special values may be specified: +.Bl -dash -compact +.It +stderr: write the logs into the terminal's standard error stream +.It +stdout: write the logs into the terminal's standard stream +.It +syslog: write the logs into syslog using the daemon facility +.El +.It Cm format Ar string +Log format. Possible values are: +.Bl -dash -compact +.It +compact +.It +full +.Aq default +.It +json +.It +pretty +.El +.It Cm level Ar string +Log level. Possible values are: +.Bl -dash -compact +.It +error +.It +warn +.It +info +.Aq default +.It +debug +.It +trace +.El +.It Cm ansi Ar boolean +Defines whether or not the log will use the ANSI terminal escape codes for colors and other text formatting. Default if true for terminal outputs (strerr and stdout) and false for syslog and files. +.El .It Ic rate-limit -Table where each element defines a HTTPS rate limit. +Table where each element defines a HTTPS rate limit. Possible fields and values are: .Bl -tag .It Cm number Ar integer Number of requests authorized withing the time period. @@ -388,7 +432,7 @@ It is strongly recommended to split the configuration into several files, each f .Pp .Bl -tag -compact .It 0 -global configuration +global configuration (including logging facilities) .It 1 hooks and hook groups .It 2 diff --git a/src/cli.rs b/src/cli.rs index 544db03..87d28c4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,3 @@ -use crate::log::Level; use clap::{Args, Parser}; use std::path::{Path, PathBuf}; @@ -9,13 +8,6 @@ pub struct CliArgs { #[arg(short, long, value_name = "DIR", default_value = get_default_config_dir().into_os_string())] pub config: PathBuf, - /// Specify the log level - #[arg(long, value_name = "LEVEL", value_enum, default_value_t = crate::DEFAULT_LOG_LEVEL)] - pub log_level: Level, - - #[command(flatten)] - pub log: Log, - /// Runs in the foreground #[arg(short, long, default_value_t = false)] pub foreground: bool, @@ -28,18 +20,6 @@ pub struct CliArgs { pub root_cert: Vec, } -#[derive(Args, Debug)] -#[group(multiple = false)] -pub struct Log { - /// Sends log messages via syslog - #[arg(long)] - pub log_syslog: bool, - - /// Prints log messages to the standard error output - #[arg(long)] - pub log_stderr: bool, -} - #[derive(Args, Debug)] #[group(multiple = false)] pub struct Pid { @@ -96,9 +76,6 @@ mod tests { let args: &[&str] = &[]; let pa = CliArgs::try_parse_from(args).unwrap(); assert_eq!(pa.config, get_default_config_dir()); - assert_eq!(pa.log_level, Level::Warn); - assert_eq!(pa.log.log_syslog, false); - assert_eq!(pa.log.log_stderr, false); assert_eq!(pa.foreground, false); assert_eq!(pa.pid.pid_file, get_default_pid_file()); assert_eq!(pa.pid.no_pid_file, false); @@ -115,9 +92,6 @@ mod tests { "acmed", "--config", "/tmp/test.toml", - "--log-level", - "debug", - "--log-syslog", "--foreground", "--pid-file", "/tmp/debug/acmed.pid", @@ -130,9 +104,6 @@ mod tests { ]; let pa = CliArgs::try_parse_from(argv).unwrap(); assert_eq!(pa.config, PathBuf::from("/tmp/test.toml")); - assert_eq!(pa.log_level, Level::Debug); - assert_eq!(pa.log.log_syslog, true); - assert_eq!(pa.log.log_stderr, false); assert_eq!(pa.foreground, true); assert_eq!( pa.pid.get_pid_file(), @@ -154,9 +125,6 @@ mod tests { "acmed", "--config", "/tmp/test.toml", - "--log-level", - "debug", - "--log-stderr", "--foreground", "--no-pid-file", "--root-cert", @@ -168,9 +136,6 @@ mod tests { ]; let pa = CliArgs::try_parse_from(argv).unwrap(); assert_eq!(pa.config, PathBuf::from("/tmp/test.toml")); - assert_eq!(pa.log_level, Level::Debug); - assert_eq!(pa.log.log_syslog, false); - assert_eq!(pa.log.log_stderr, true); assert_eq!(pa.foreground, true); assert_eq!(pa.pid.get_pid_file(), None); assert_eq!( @@ -189,9 +154,6 @@ mod tests { "acmed", "-c", "/tmp/test.toml", - "--log-level", - "debug", - "--log-syslog", "-f", "--pid-file", "/tmp/debug/acmed.pid", @@ -204,9 +166,6 @@ mod tests { ]; let pa = CliArgs::try_parse_from(argv).unwrap(); assert_eq!(pa.config, PathBuf::from("/tmp/test.toml")); - assert_eq!(pa.log_level, Level::Debug); - assert_eq!(pa.log.log_syslog, true); - assert_eq!(pa.log.log_stderr, false); assert_eq!(pa.foreground, true); assert_eq!( pa.pid.get_pid_file(), @@ -228,9 +187,6 @@ mod tests { "acmed", "-c", "/tmp/test.toml", - "--log-level", - "debug", - "--log-stderr", "-f", "--no-pid-file", "--root-cert", @@ -242,9 +198,6 @@ mod tests { ]; let pa = CliArgs::try_parse_from(argv).unwrap(); assert_eq!(pa.config, PathBuf::from("/tmp/test.toml")); - assert_eq!(pa.log_level, Level::Debug); - assert_eq!(pa.log.log_syslog, false); - assert_eq!(pa.log.log_stderr, true); assert_eq!(pa.foreground, true); assert_eq!(pa.pid.get_pid_file(), None); assert_eq!( @@ -257,13 +210,6 @@ mod tests { ); } - #[test] - fn err_log_output() { - let argv: &[&str] = &["acmed", "--log-stderr", "--log-syslog"]; - let pa = CliArgs::try_parse_from(argv); - assert!(pa.is_err()); - } - #[test] fn err_pid_file() { let argv: &[&str] = &[ diff --git a/src/config.rs b/src/config.rs index d9c457d..9dd29f1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,7 @@ mod duration; mod endpoint; mod global; mod hook; +mod log; mod rate_limit; pub use account::*; @@ -12,6 +13,7 @@ pub use duration::*; pub use endpoint::*; pub use global::*; pub use hook::*; +pub use log::*; pub use rate_limit::*; use anyhow::{Context, Result}; @@ -30,6 +32,8 @@ pub struct AcmedConfig { pub(in crate::config) global: Option, #[serde(default)] pub endpoint: HashMap, + #[serde(default)] + pub logging_facility: Vec, #[serde(default, rename = "rate-limit")] pub(in crate::config) rate_limit: HashMap, #[serde(default)] @@ -138,10 +142,8 @@ impl<'de> Deserialize<'de> for AcmedConfig { } } -#[tracing::instrument(level = "trace", err(Debug))] pub fn load + std::fmt::Debug>(config_dir: P) -> Result { let config_dir = config_dir.as_ref(); - tracing::debug!("loading config directory"); let settings = Config::builder() .add_source( get_files(config_dir)? @@ -150,9 +152,7 @@ pub fn load + std::fmt::Debug>(config_dir: P) -> Result>(), ) .build()?; - tracing::trace!("loaded config" = ?settings); let config: AcmedConfig = settings.try_deserialize().context("invalid setting")?; - tracing::debug!("computed config" = ?config); Ok(config) } @@ -170,12 +170,10 @@ fn get_files(config_dir: &Path) -> Result> { } } file_lst.sort(); - tracing::debug!("configuration files found" = ?file_lst); Ok(file_lst) } #[cfg(test)] -#[tracing::instrument(level = "trace", err(Debug))] fn load_str<'de, T: serde::de::Deserialize<'de>>(config_str: &str) -> Result { let settings = Config::builder() .add_source(File::from_str(config_str, config::FileFormat::Toml)) @@ -197,6 +195,7 @@ mod tests { assert!(cfg.hook.is_empty()); assert!(cfg.group.is_empty()); assert!(cfg.account.is_empty()); + assert!(cfg.logging_facility.is_empty()); assert!(cfg.certificate.is_empty()); } @@ -253,6 +252,28 @@ mod tests { let i = c.identifiers.first().unwrap(); assert_eq!(i.dns, Some("example.org".to_string())); assert_eq!(i.challenge, AcmeChallenge::Http01); + assert_eq!(cfg.logging_facility.len(), 3); + let stderr = cfg.logging_facility.first().unwrap(); + assert_eq!(stderr.output, Facility::StdErr); + assert_eq!(stderr.format, LogFormat::Pretty); + assert_eq!(stderr.level, Level::Trace); + assert_eq!(stderr.ansi, None); + assert_eq!(stderr.is_ansi(), true); + let syslog = cfg.logging_facility.get(1).unwrap(); + assert_eq!(syslog.output, Facility::SysLog); + assert_eq!(syslog.format, LogFormat::Full); + assert_eq!(syslog.level, Level::Info); + assert_eq!(syslog.ansi, Some(false)); + assert_eq!(syslog.is_ansi(), false); + let file = cfg.logging_facility.get(2).unwrap(); + assert_eq!( + file.output, + Facility::File(PathBuf::from("/tmp/acmed_test.log.json")) + ); + assert_eq!(file.format, LogFormat::Json); + assert_eq!(file.level, Level::Debug); + assert_eq!(file.ansi, None); + assert_eq!(file.is_ansi(), false); assert_eq!(c.hooks, vec!["super-hook".to_string()]); } diff --git a/src/config/log.rs b/src/config/log.rs new file mode 100644 index 0000000..6df4210 --- /dev/null +++ b/src/config/log.rs @@ -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, +} + +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(deserializer: D) -> Result + 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 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::(""); + assert!(res.is_err()); + } + + #[test] + fn empty_output() { + let cfg = r#"output = """#; + let res = load_str::(cfg); + assert!(res.is_err()); + } + + #[test] + fn logging_facility_minimal() { + let cfg = r#"output = "test.log""#; + let c = load_str::(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::(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::(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::(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::(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); + } +} diff --git a/src/log.rs b/src/log.rs index 45bfe50..efa6d85 100644 --- a/src/log.rs +++ b/src/log.rs @@ -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(()) } diff --git a/src/main.rs b/src/main.rs index b395435..b597d28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,26 +15,30 @@ use std::process; pub const APP_IDENTITY: &[u8] = b"acmed\0"; pub const APP_THREAD_NAME: &str = "acmed-runtime"; -pub const DEFAULT_LOG_LEVEL: log::Level = log::Level::Warn; pub const INTERNAL_HOOK_PREFIX: &str = "internal:"; fn main() { - // CLI + // Load the command-line interface let args = cli::CliArgs::parse(); - // Initialize the logging system - log::init(args.log_level, !args.log.log_stderr); - tracing::trace!("computed args" = ?args); - // Load the configuration let cfg = match config::load(args.config.as_path()) { Ok(cfg) => cfg, - Err(_) => std::process::exit(3), + Err(e) => { + eprintln!("error while loading the configuration: {e:#}"); + std::process::exit(2) + } }; + // Initialize the logging system + if let Err(e) = log::init(&cfg) { + eprintln!("error while initializing the logging system: {e:#}"); + std::process::exit(3) + } + // Initialize the server (PID file and daemon) if init_server(args.foreground, args.pid.get_pid_file()).is_err() { - std::process::exit(3); + std::process::exit(4); } // Starting ACMEd diff --git a/tests/config/simple/simple.toml b/tests/config/simple/simple.toml index e907fed..5d27176 100644 --- a/tests/config/simple/simple.toml +++ b/tests/config/simple/simple.toml @@ -3,6 +3,20 @@ accounts_directory = "/tmp/example/account/dir" certificates_directory = "/tmp/example/cert/dir/" root_certificates = ["tests/root_certs/igc-a 2011.pem"] +[[logging_facility]] +output = "stderr" +format = "pretty" +level = "trace" + +[[logging_facility]] +output = "syslog" +ansi = false + +[[logging_facility]] +output = "/tmp/acmed_test.log.json" +format = "json" +level = "debug" + [account."toto"] contacts = [ { mailto = "acme@example.org" },