1
0
Fork 0
mirror of https://github.com/containers/youki synced 2024-05-04 22:56:15 +02:00

Merge pull request #1975 from yihuaf/yihuaf/journald

Implemented sending logs to systemd-journald
This commit is contained in:
Toru Komatsu 2023-05-31 20:47:37 +09:00 committed by GitHub
commit cb75d26d8f
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 119 additions and 54 deletions

12
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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 {:?}",

View File

@ -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<String>) -> Result<LogFormat> {
match log_format.as_deref() {
fn detect_log_format(log_format: Option<&str>) -> Result<LogFormat> {
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<LevelFilter> {
fn detect_log_level(is_debug: bool) -> Result<Level> {
let filter: Cow<str> = 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<LevelFilter> {
} else {
DEFAULT_LOG_LEVEL.into()
};
Ok(LevelFilter::from_str(filter.as_ref())?)
Ok(Level::from_str(filter.as_ref())?)
}
pub fn init(
log_debug_flag: bool,
log_file: Option<PathBuf>,
log_format: Option<String>,
) -> 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")?;
#[derive(Debug, Default)]
pub struct ObservabilityConfig {
pub log_debug_flag: bool,
pub log_file: Option<PathBuf>,
pub log_format: Option<String>,
pub systemd_log: bool,
}
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<T>(config: T) -> Result<()>
where
T: Into<ObservabilityConfig>,
{
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()