Browse Source

Defines the log in the configuration file, not in the CLI

ng
Rodolphe Bréard 3 weeks ago
parent
commit
1fe846932d
Failed to extract signature
  1. 11
      CHANGELOG.md
  2. 13
      Cargo.lock
  3. 2
      Cargo.toml
  4. 2
      config/01_log_stderr.toml
  5. 2
      config/02_log_syslog.toml
  6. 9
      man/en/acmed.8
  7. 48
      man/en/acmed.toml.5
  8. 54
      src/cli.rs
  9. 33
      src/config.rs
  10. 171
      src/config/log.rs
  11. 83
      src/log.rs
  12. 20
      src/main.rs
  13. 14
      tests/config/simple/simple.toml

11
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

13
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]]

2
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]

2
config/01_log_stderr.toml

@ -0,0 +1,2 @@
[[logging_facility]]
output = "stderr"

2
config/02_log_syslog.toml

@ -0,0 +1,2 @@
[[logging_facility]]
output = "syslog"

9
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

48
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

54
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<PathBuf>,
}
#[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] = &[

33
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<GlobalOptions>,
#[serde(default)]
pub endpoint: HashMap<String, Endpoint>,
#[serde(default)]
pub logging_facility: Vec<LoggingFacility>,
#[serde(default, rename = "rate-limit")]
pub(in crate::config) rate_limit: HashMap<String, RateLimit>,
#[serde(default)]
@ -138,10 +142,8 @@ impl<'de> Deserialize<'de> for AcmedConfig {
}
}
#[tracing::instrument(level = "trace", err(Debug))]
pub fn load<P: AsRef<Path> + std::fmt::Debug>(config_dir: P) -> Result<AcmedConfig> {
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<P: AsRef<Path> + std::fmt::Debug>(config_dir: P) -> Result<AcmedConf
.collect::<Vec<_>>(),
)
.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<Vec<PathBuf>> {
}
}
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<T> {
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()]);
}

171
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<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);
}
}

83
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 {
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, 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");
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(())
}

20
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

14
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" },

Loading…
Cancel
Save