diff --git a/Cargo.lock b/Cargo.lock index 0e5237a36..670329327 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3885,6 +3885,17 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-journald" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba316a74e8fc3c3896a850dba2375928a9fa171b085ecddfc7c054d39970f3fd" +dependencies = [ + "libc", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.1.3" @@ -5423,6 +5434,7 @@ dependencies = [ "tabwriter", "tempfile", "tracing", + "tracing-journald", "tracing-subscriber", "vergen", "wasmedge-sdk", diff --git a/crates/youki/Cargo.toml b/crates/youki/Cargo.toml index 578cb6a6c..d390905c3 100644 --- a/crates/youki/Cargo.toml +++ b/crates/youki/Cargo.toml @@ -48,6 +48,7 @@ wasmtime = {version = "9.0.2", optional = true } wasmtime-wasi = {version = "9.0.2", optional = true } tracing = { version = "0.1.37", features = ["attributes"]} tracing-subscriber = { version = "0.3.16", features = ["json", "env-filter"] } +tracing-journald = "0.3.0" [dev-dependencies] serial_test = "2.0.0" diff --git a/crates/youki/src/main.rs b/crates/youki/src/main.rs index 1f8fbc93d..dfb2923da 100644 --- a/crates/youki/src/main.rs +++ b/crates/youki/src/main.rs @@ -2,7 +2,7 @@ //! Container Runtime written in Rust, inspired by [railcar](https://github.com/oracle/railcar) //! This crate provides a container runtime which can be used by a high-level container runtime to run containers. mod commands; -mod logger; +mod observability; mod rootpath; mod workload; @@ -15,6 +15,14 @@ use crate::commands::info; use liboci_cli::{CommonCmd, GlobalOpts, StandardCmd}; +// Additional options that are not defined in OCI runtime-spec, but are used by Youki. +#[derive(Parser, Debug)] +struct YoukiExtendOpts { + /// Enable logging to systemd-journald + #[clap(long)] + pub systemd_log: bool, +} + // High-level commandline option definition // This takes global options as well as individual commands as specified in [OCI runtime-spec](https://github.com/opencontainers/runtime-spec/blob/master/runtime.md) // Also check [runc commandline documentation](https://github.com/opencontainers/runc/blob/master/man/runc.8.md) for more explanation @@ -24,6 +32,9 @@ struct Opts { #[clap(flatten)] global: GlobalOpts, + #[clap(flatten)] + youki_extend: YoukiExtendOpts, + #[clap(subcommand)] subcmd: SubCommand, } @@ -78,10 +89,10 @@ fn main() -> Result<()> { let opts = Opts::parse(); let mut app = Opts::command(); - if let Err(e) = crate::logger::init(opts.global.debug, opts.global.log, opts.global.log_format) - { - eprintln!("log init failed: {e:?}"); - } + crate::observability::init(&opts).map_err(|err| { + eprintln!("failed to initialize observability: {}", err); + err + })?; tracing::debug!( "started by user {} with {:?}", diff --git a/crates/youki/src/logger.rs b/crates/youki/src/observability.rs similarity index 66% rename from crates/youki/src/logger.rs rename to crates/youki/src/observability.rs index 704e9697a..0c18d5875 100644 --- a/crates/youki/src/logger.rs +++ b/crates/youki/src/observability.rs @@ -1,11 +1,10 @@ -//! Default Youki Logger - use anyhow::{bail, Context, Result}; use std::borrow::Cow; use std::fs::OpenOptions; use std::path::PathBuf; use std::str::FromStr; -use tracing::metadata::LevelFilter; +use tracing::Level; +use tracing_subscriber::prelude::*; const LOG_LEVEL_ENV_NAME: &str = "YOUKI_LOG_LEVEL"; const LOG_FORMAT_TEXT: &str = "text"; @@ -23,15 +22,15 @@ const DEFAULT_LOG_LEVEL: &str = "debug"; #[cfg(not(debug_assertions))] const DEFAULT_LOG_LEVEL: &str = "warn"; -fn detect_log_format(log_format: Option) -> Result { - match log_format.as_deref() { +fn detect_log_format(log_format: Option<&str>) -> Result { + match log_format { None | Some(LOG_FORMAT_TEXT) => Ok(LogFormat::Text), Some(LOG_FORMAT_JSON) => Ok(LogFormat::Json), Some(unknown) => bail!("unknown log format: {}", unknown), } } -fn detect_log_level(is_debug: bool) -> Result { +fn detect_log_level(is_debug: bool) -> Result { let filter: Cow = if is_debug { "debug".into() } else if let Ok(level) = std::env::var(LOG_LEVEL_ENV_NAME) { @@ -39,38 +38,73 @@ fn detect_log_level(is_debug: bool) -> Result { } else { DEFAULT_LOG_LEVEL.into() }; - Ok(LevelFilter::from_str(filter.as_ref())?) + Ok(Level::from_str(filter.as_ref())?) +} + +#[derive(Debug, Default)] +pub struct ObservabilityConfig { + pub log_debug_flag: bool, + pub log_file: Option, + pub log_format: Option, + pub systemd_log: bool, } -pub fn init( - log_debug_flag: bool, - log_file: Option, - log_format: Option, -) -> Result<()> { - let level = detect_log_level(log_debug_flag).context("failed to parse log level")?; - let log_format = detect_log_format(log_format).context("failed to detect log format")?; +impl From<&crate::Opts> for ObservabilityConfig { + fn from(opts: &crate::Opts) -> Self { + Self { + log_debug_flag: opts.global.debug, + log_file: opts.global.log.to_owned(), + log_format: opts.global.log_format.to_owned(), + systemd_log: opts.youki_extend.systemd_log, + } + } +} + +pub fn init(config: T) -> Result<()> +where + T: Into, +{ + let config = config.into(); + let level = + detect_log_level(config.log_debug_flag).with_context(|| "failed to parse log level")?; + let log_level_filter = tracing_subscriber::filter::LevelFilter::from(level); + let log_format = detect_log_format(config.log_format.as_deref()) + .with_context(|| "failed to detect log format")?; + let systemd_journald = if config.systemd_log { + Some(tracing_journald::layer()?.with_syslog_identifier("youki".to_string())) + } else { + None + }; + let subscriber = tracing_subscriber::registry() + .with(log_level_filter) + .with(systemd_journald); // I really dislike how we have to specify individual branch for each // combination, but I can't find any better way to do this. The tracing - // crate makes it hard to build a single layer with different conditions. - match (log_file, log_format) { + // crate makes it hard to build a single format layer with different + // conditions. + match (config.log_file.as_ref(), log_format) { (None, LogFormat::Text) => { // Text to stderr - tracing_subscriber::fmt() - .with_max_level(level) - .without_time() - .with_writer(std::io::stderr) + subscriber + .with( + tracing_subscriber::fmt::layer() + .without_time() + .with_writer(std::io::stderr), + ) .try_init() .map_err(|e| anyhow::anyhow!("failed to init logger: {}", e))?; } (None, LogFormat::Json) => { // JSON to stderr - tracing_subscriber::fmt() - .json() - .flatten_event(true) - .with_span_list(false) - .with_max_level(level) - .with_writer(std::io::stderr) + subscriber + .with( + tracing_subscriber::fmt::layer() + .json() + .flatten_event(true) + .with_span_list(false) + .with_writer(std::io::stderr), + ) .try_init() .map_err(|e| anyhow::anyhow!("failed to init logger: {}", e))?; } @@ -82,9 +116,8 @@ pub fn init( .truncate(false) .open(path) .with_context(|| "failed to open log file")?; - tracing_subscriber::fmt() - .with_writer(file) - .with_max_level(level) + subscriber + .with(tracing_subscriber::fmt::layer().with_writer(file)) .try_init() .map_err(|e| anyhow::anyhow!("failed to init logger: {}", e))?; } @@ -96,12 +129,14 @@ pub fn init( .truncate(false) .open(path) .with_context(|| "failed to open log file")?; - tracing_subscriber::fmt() - .json() - .flatten_event(true) - .with_span_list(false) - .with_writer(file) - .with_max_level(level) + subscriber + .with( + tracing_subscriber::fmt::layer() + .json() + .flatten_event(true) + .with_span_list(false) + .with_writer(file), + ) .try_init() .map_err(|e| anyhow::anyhow!("failed to init logger: {}", e))?; } @@ -142,7 +177,7 @@ mod tests { #[test] fn test_detect_log_level_is_debug() { let _guard = LogLevelGuard::new("error").unwrap(); - assert_eq!(detect_log_level(true).unwrap(), LevelFilter::DEBUG) + assert_eq!(detect_log_level(true).unwrap(), tracing::Level::DEBUG) } #[test] @@ -151,9 +186,9 @@ mod tests { let _guard = LogLevelGuard::new("error").unwrap(); env::remove_var(LOG_LEVEL_ENV_NAME); if cfg!(debug_assertions) { - assert_eq!(detect_log_level(false).unwrap(), LevelFilter::DEBUG) + assert_eq!(detect_log_level(false).unwrap(), tracing::Level::DEBUG) } else { - assert_eq!(detect_log_level(false).unwrap(), LevelFilter::WARN) + assert_eq!(detect_log_level(false).unwrap(), tracing::Level::WARN) } } @@ -161,7 +196,7 @@ mod tests { #[serial] fn test_detect_log_level_from_env() { let _guard = LogLevelGuard::new("error").unwrap(); - assert_eq!(detect_log_level(false).unwrap(), LevelFilter::ERROR) + assert_eq!(detect_log_level(false).unwrap(), tracing::Level::ERROR) } #[test] @@ -170,8 +205,11 @@ mod tests { let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); let log_file = Path::join(temp_dir.path(), "test.log"); let _guard = LogLevelGuard::new("error").unwrap(); - init(false, Some(log_file), None) - .map_err(|err| TestCallbackError::Other(err.into()))?; + let config = ObservabilityConfig { + log_file: Some(log_file), + ..Default::default() + }; + init(config).map_err(|err| TestCallbackError::Other(err.into()))?; Ok(()) }; libcontainer::test_utils::test_in_child_process(cb) @@ -189,8 +227,11 @@ mod tests { let _guard = LogLevelGuard::new("error").unwrap(); // Note, we can only init the tracing once, so we have to test in a // single unit test. The orders are important here. - init(false, Some(log_file.to_owned()), None) - .map_err(|err| TestCallbackError::Other(err.into()))?; + let config = ObservabilityConfig { + log_file: Some(log_file.clone()), + ..Default::default() + }; + init(config).map_err(|err| TestCallbackError::Other(err.into()))?; assert!( log_file .as_path() @@ -230,12 +271,12 @@ mod tests { let _guard = LogLevelGuard::new("error").unwrap(); // Note, we can only init the tracing once, so we have to test in a // single unit test. The orders are important here. - init( - false, - Some(log_file.to_owned()), - Some(LOG_FORMAT_JSON.to_owned()), - ) - .map_err(|err| TestCallbackError::Other(err.into()))?; + let config = ObservabilityConfig { + log_file: Some(log_file.clone()), + log_format: Some(LOG_FORMAT_JSON.to_owned()), + ..Default::default() + }; + init(config).map_err(|err| TestCallbackError::Other(err.into()))?; assert!( log_file .as_path()