tracing/tracing-journald/tests/journal.rs
Gabriel Goller d4ca988252
journald: make level mappings configurable (#2824)
This allows to manually map tracing levels to journald levels.

It seems that @little-dude, who started the original PR, doesn't have
time to finish this, so I picked it up.  Reapplied the changes to the
newest master branch and fixed the latest comments/issues.

This will also fix/close:

Closes #2649
Closes #2661
Closes #2347 (the original pr)
2024-01-11 12:28:41 -08:00

376 lines
12 KiB
Rust

#![cfg(target_os = "linux")]
use std::collections::HashMap;
use std::process::Command;
use std::time::Duration;
use serde::Deserialize;
use tracing::{debug, error, info, info_span, trace, warn};
use tracing_journald::{Priority, PriorityMappings, Subscriber};
use tracing_subscriber::subscribe::CollectExt;
use tracing_subscriber::Registry;
fn journalctl_version() -> std::io::Result<String> {
let output = Command::new("journalctl").arg("--version").output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn with_journald(f: impl FnOnce()) {
with_journald_subscriber(
Subscriber::new()
.unwrap()
.with_field_prefix(None)
.with_priority_mappings(PriorityMappings {
trace: Priority::Informational,
..PriorityMappings::new()
}),
f,
)
}
fn with_journald_subscriber(subscriber: Subscriber, f: impl FnOnce()) {
match journalctl_version() {
Ok(_) => {
let sub = Registry::default().with(subscriber);
tracing::collect::with_default(sub, f);
}
Err(error) => eprintln!(
"SKIPPING TEST: journalctl --version failed with error: {}",
error
),
}
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(untagged)]
enum Field {
Text(String),
Array(Vec<String>),
Binary(Vec<u8>),
}
impl Field {
fn as_array(&self) -> Option<&[String]> {
match self {
Field::Text(_) => None,
Field::Binary(_) => None,
Field::Array(v) => Some(v),
}
}
fn as_text(&self) -> Option<&str> {
match self {
Field::Text(v) => Some(v.as_str()),
Field::Binary(_) => None,
Field::Array(_) => None,
}
}
}
// Convenience impls to compare fields against strings and bytes with assert_eq!
impl PartialEq<&str> for Field {
fn eq(&self, other: &&str) -> bool {
match self {
Field::Text(s) => s == other,
Field::Binary(_) => false,
Field::Array(_) => false,
}
}
}
impl PartialEq<[u8]> for Field {
fn eq(&self, other: &[u8]) -> bool {
match self {
Field::Text(s) => s.as_bytes() == other,
Field::Binary(data) => data == other,
Field::Array(_) => false,
}
}
}
impl PartialEq<Vec<&str>> for Field {
fn eq(&self, other: &Vec<&str>) -> bool {
match self {
Field::Text(_) => false,
Field::Binary(_) => false,
Field::Array(data) => data == other,
}
}
}
/// Retry `f` 30 times 100ms apart, i.e. a total of three seconds.
///
/// When `f` returns an error wait 100ms and try it again, up to ten times.
/// If the last attempt failed return the error returned by that attempt.
///
/// If `f` returns Ok immediately return the result.
fn retry<T, E>(f: impl Fn() -> Result<T, E>) -> Result<T, E> {
let attempts = 30;
let interval = Duration::from_millis(100);
for attempt in (0..attempts).rev() {
match f() {
Ok(result) => return Ok(result),
Err(e) if attempt == 0 => return Err(e),
Err(_) => std::thread::sleep(interval),
}
}
unreachable!()
}
/// Read from journal with `journalctl`.
///
/// `test_name` is a string to match in the `TEST_NAME` field
/// of the `journalctl` call, to make sure to only select journal entries
/// originating from and relevant to the current test.
///
/// Additionally filter by the `_PID` field with the PID of this
/// test process, to make sure this method only reads journal entries
/// created by this test process.
fn read_from_journal(test_name: &str) -> Vec<HashMap<String, Field>> {
let stdout = String::from_utf8(
Command::new("journalctl")
// We pass --all to circumvent journalctl's default limit of 4096 bytes for field values
.args(["--user", "--output=json", "--all"])
// Filter by the PID of the current test process
.arg(format!("_PID={}", std::process::id()))
.arg(format!("TEST_NAME={}", test_name))
.output()
.unwrap()
.stdout,
)
.unwrap();
stdout
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect()
}
/// Read exactly one line from journal for the given test name.
///
/// Try to read lines for `testname` from journal, and `retry()` if the wasn't
/// _exactly_ one matching line.
fn retry_read_one_line_from_journal(testname: &str) -> HashMap<String, Field> {
retry(|| {
let mut messages = read_from_journal(testname);
if messages.len() == 1 {
Ok(messages.pop().unwrap())
} else {
Err(format!(
"one messages expected, got {} messages",
messages.len()
))
}
})
.unwrap()
}
#[test]
fn simple_message() {
with_journald(|| {
info!(test.name = "simple_message", "Hello World");
let message = retry_read_one_line_from_journal("simple_message");
assert_eq!(message["MESSAGE"], "Hello World");
assert_eq!(message["PRIORITY"], "5");
});
}
#[test]
fn custom_priorities() {
fn check_message(level: &str, priority: &str) {
let entry = retry_read_one_line_from_journal(&format!("custom_priority.{}", level));
assert_eq!(entry["MESSAGE"], format!("hello {}", level).as_str());
assert_eq!(entry["PRIORITY"], priority);
}
let priorities = PriorityMappings {
error: Priority::Critical,
warn: Priority::Error,
info: Priority::Warning,
debug: Priority::Notice,
trace: Priority::Informational,
};
let subscriber = Subscriber::new()
.unwrap()
.with_field_prefix(None)
.with_priority_mappings(priorities);
let test = || {
trace!(test.name = "custom_priority.trace", "hello trace");
check_message("trace", "6");
debug!(test.name = "custom_priority.debug", "hello debug");
check_message("debug", "5");
info!(test.name = "custom_priority.info", "hello info");
check_message("info", "4");
warn!(test.name = "custom_priority.warn", "hello warn");
check_message("warn", "3");
error!(test.name = "custom_priority.error", "hello error");
check_message("error", "2");
};
with_journald_subscriber(subscriber, test);
}
#[test]
fn multiline_message() {
with_journald(|| {
warn!(test.name = "multiline_message", "Hello\nMultiline\nWorld");
let message = retry_read_one_line_from_journal("multiline_message");
assert_eq!(message["MESSAGE"], "Hello\nMultiline\nWorld");
assert_eq!(message["PRIORITY"], "4");
});
}
#[test]
fn multiline_message_trailing_newline() {
with_journald(|| {
error!(
test.name = "multiline_message_trailing_newline",
"A trailing newline\n"
);
let message = retry_read_one_line_from_journal("multiline_message_trailing_newline");
assert_eq!(message["MESSAGE"], "A trailing newline\n");
assert_eq!(message["PRIORITY"], "3");
});
}
#[test]
fn internal_null_byte() {
with_journald(|| {
debug!(test.name = "internal_null_byte", "An internal\x00byte");
let message = retry_read_one_line_from_journal("internal_null_byte");
assert_eq!(message["MESSAGE"], b"An internal\x00byte"[..]);
assert_eq!(message["PRIORITY"], "6");
});
}
#[test]
fn large_message() {
let large_string = "b".repeat(512_000);
with_journald(|| {
debug!(test.name = "large_message", "Message: {}", large_string);
let message = retry_read_one_line_from_journal("large_message");
assert_eq!(
message["MESSAGE"],
format!("Message: {}", large_string).as_str()
);
assert_eq!(message["PRIORITY"], "6");
});
}
#[test]
fn simple_metadata() {
let sub = Subscriber::new()
.unwrap()
.with_field_prefix(None)
.with_syslog_identifier("test_ident".to_string());
with_journald_subscriber(sub, || {
info!(test.name = "simple_metadata", "Hello World");
let message = retry_read_one_line_from_journal("simple_metadata");
assert_eq!(message["MESSAGE"], "Hello World");
assert_eq!(message["PRIORITY"], "5");
assert_eq!(message["TARGET"], "journal");
assert_eq!(message["SYSLOG_IDENTIFIER"], "test_ident");
assert!(message["CODE_FILE"].as_text().is_some());
assert!(message["CODE_LINE"].as_text().is_some());
});
}
#[test]
fn journal_fields() {
let sub = Subscriber::new()
.unwrap()
.with_field_prefix(None)
.with_custom_fields([("SYSLOG_FACILITY", "17")])
.with_custom_fields([("ABC", "dEf"), ("XYZ", "123")]);
with_journald_subscriber(sub, || {
info!(test.name = "journal_fields", "Hello World");
let message = retry_read_one_line_from_journal("journal_fields");
assert_eq!(message["MESSAGE"], "Hello World");
assert_eq!(message["PRIORITY"], "5");
assert_eq!(message["TARGET"], "journal");
assert_eq!(message["SYSLOG_FACILITY"], "17");
assert_eq!(message["ABC"], "dEf");
assert_eq!(message["XYZ"], "123");
assert!(message["CODE_FILE"].as_text().is_some());
assert!(message["CODE_LINE"].as_text().is_some());
});
}
#[test]
fn span_metadata() {
with_journald(|| {
let s1 = info_span!("span1", span_field1 = "foo1");
let _g1 = s1.enter();
info!(test.name = "span_metadata", "Hello World");
let message = retry_read_one_line_from_journal("span_metadata");
assert_eq!(message["MESSAGE"], "Hello World");
assert_eq!(message["PRIORITY"], "5");
assert_eq!(message["TARGET"], "journal");
assert_eq!(message["SPAN_FIELD1"].as_text(), Some("foo1"));
assert_eq!(message["SPAN_NAME"].as_text(), Some("span1"));
assert!(message["CODE_FILE"].as_text().is_some());
assert!(message["CODE_LINE"].as_text().is_some());
assert!(message["SPAN_CODE_FILE"].as_text().is_some());
assert!(message["SPAN_CODE_LINE"].as_text().is_some());
});
}
#[test]
fn multiple_spans_metadata() {
with_journald(|| {
let s1 = info_span!("span1", span_field1 = "foo1");
let _g1 = s1.enter();
let s2 = info_span!("span2", span_field1 = "foo2");
let _g2 = s2.enter();
info!(test.name = "multiple_spans_metadata", "Hello World");
let message = retry_read_one_line_from_journal("multiple_spans_metadata");
assert_eq!(message["MESSAGE"], "Hello World");
assert_eq!(message["PRIORITY"], "5");
assert_eq!(message["TARGET"], "journal");
assert_eq!(message["SPAN_FIELD1"], vec!["foo1", "foo2"]);
assert_eq!(message["SPAN_NAME"], vec!["span1", "span2"]);
assert!(message["CODE_FILE"].as_text().is_some());
assert!(message["CODE_LINE"].as_text().is_some());
assert!(message.contains_key("SPAN_CODE_FILE"));
assert_eq!(message["SPAN_CODE_LINE"].as_array().unwrap().len(), 2);
});
}
#[test]
fn spans_field_collision() {
with_journald(|| {
let s1 = info_span!("span1", span_field = "foo1");
let _g1 = s1.enter();
let s2 = info_span!("span2", span_field = "foo2");
let _g2 = s2.enter();
info!(
test.name = "spans_field_collision",
span_field = "foo3",
"Hello World"
);
let message = retry_read_one_line_from_journal("spans_field_collision");
assert_eq!(message["MESSAGE"], "Hello World");
assert_eq!(message["SPAN_NAME"], vec!["span1", "span2"]);
assert_eq!(message["SPAN_FIELD"], vec!["foo1", "foo2", "foo3"]);
});
}