Skip to content

Commit fdf8178

Browse files
authored
journald: add integration tests for journald subscriber (#1709)
Per discussion with @hawkw in #1698 I'm adding a few simple integration tests for the journald subscriber, to have some safety net when implementing the actual issue in #1698. These tests send messages of various complexity to the journal, and then use `journalctl`'s JSON output to get them back out, to check whether the message arrives in the systemd journal as it was intended to. ## Motivation Increase test coverage for the journald subscriber and codify a known good state before approaching a fix for #1698.
1 parent 414b1c6 commit fdf8178

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed

tracing-journald/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ keywords = ["tracing", "journald"]
1717
[dependencies]
1818
tracing-core = { path = "../tracing-core", version = "0.2" }
1919
tracing-subscriber = { path = "../tracing-subscriber", version = "0.3" }
20+
21+
[dev-dependencies]
22+
serde_json = "1.0.68"
23+
serde = { version = "1.0.130", features = ["derive"] }
24+
tracing = { path = "../tracing", version = "0.2" }

tracing-journald/tests/journal.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#![cfg(target_os = "linux")]
2+
3+
use std::collections::HashMap;
4+
use std::process::Command;
5+
use std::time::Duration;
6+
7+
use serde::Deserialize;
8+
use tracing::{debug, error, info, warn};
9+
use tracing_journald::Subscriber;
10+
use tracing_subscriber::subscribe::CollectExt;
11+
use tracing_subscriber::Registry;
12+
13+
fn journalctl_version() -> std::io::Result<String> {
14+
let output = Command::new("journalctl").arg("--version").output()?;
15+
Ok(String::from_utf8_lossy(&output.stdout).to_string())
16+
}
17+
18+
fn with_journald(f: impl FnOnce()) {
19+
match journalctl_version() {
20+
Ok(_) => {
21+
let sub = Registry::default().with(Subscriber::new().unwrap().with_field_prefix(None));
22+
tracing::collect::with_default(sub, f);
23+
}
24+
Err(error) => eprintln!(
25+
"SKIPPING TEST: journalctl --version failed with error: {}",
26+
error
27+
),
28+
}
29+
}
30+
31+
#[derive(Debug, PartialEq, Deserialize)]
32+
#[serde(untagged)]
33+
enum Field {
34+
Text(String),
35+
Binary(Vec<u8>),
36+
}
37+
38+
// Convenience impls to compare fields against strings and bytes with assert_eq!
39+
impl PartialEq<&str> for Field {
40+
fn eq(&self, other: &&str) -> bool {
41+
match self {
42+
Field::Text(s) => s == other,
43+
Field::Binary(_) => false,
44+
}
45+
}
46+
}
47+
48+
impl PartialEq<[u8]> for Field {
49+
fn eq(&self, other: &[u8]) -> bool {
50+
match self {
51+
Field::Text(s) => s.as_bytes() == other,
52+
Field::Binary(data) => data == other,
53+
}
54+
}
55+
}
56+
57+
/// Retry `f` 10 times 100ms apart.
58+
///
59+
/// When `f` returns an error wait 100ms and try it again, up to ten times.
60+
/// If the last attempt failed return the error returned by that attempt.
61+
///
62+
/// If `f` returns Ok immediately return the result.
63+
fn retry<T, E>(f: impl Fn() -> Result<T, E>) -> Result<T, E> {
64+
let attempts = 10;
65+
let interval = Duration::from_millis(100);
66+
for attempt in (0..attempts).rev() {
67+
match f() {
68+
Ok(result) => return Ok(result),
69+
Err(e) if attempt == 0 => return Err(e),
70+
Err(_) => std::thread::sleep(interval),
71+
}
72+
}
73+
unreachable!()
74+
}
75+
76+
/// Read from journal with `journalctl`.
77+
///
78+
/// `test_name` is a string to match in the `TEST_NAME` field
79+
/// of the `journalctl` call, to make sure to only select journal entries
80+
/// originating from and relevant to the current test.
81+
///
82+
/// Additionally filter by the `_PID` field with the PID of this
83+
/// test process, to make sure this method only reads journal entries
84+
/// created by this test process.
85+
fn read_from_journal(test_name: &str) -> Vec<HashMap<String, Field>> {
86+
let stdout = String::from_utf8(
87+
Command::new("journalctl")
88+
.args(&["--user", "--output=json"])
89+
// Filter by the PID of the current test process
90+
.arg(format!("_PID={}", std::process::id()))
91+
// tracing-journald logs strings in their debug representation
92+
.arg(format!("TEST_NAME={:?}", test_name))
93+
.output()
94+
.unwrap()
95+
.stdout,
96+
)
97+
.unwrap();
98+
99+
stdout
100+
.lines()
101+
.map(|l| {
102+
dbg!(l);
103+
serde_json::from_str(l).unwrap()
104+
})
105+
.collect()
106+
}
107+
108+
/// Read exactly one line from journal for the given test name.
109+
///
110+
/// Try to read lines for `testname` from journal, and `retry()` if the wasn't
111+
/// _exactly_ one matching line.
112+
fn retry_read_one_line_from_journal(testname: &str) -> HashMap<String, Field> {
113+
retry(|| {
114+
let mut messages = read_from_journal(testname);
115+
if messages.len() == 1 {
116+
Ok(messages.pop().unwrap())
117+
} else {
118+
Err(format!(
119+
"one messages expected, got {} messages",
120+
messages.len()
121+
))
122+
}
123+
})
124+
.unwrap()
125+
}
126+
127+
#[test]
128+
fn simple_message() {
129+
with_journald(|| {
130+
info!(test.name = "simple_message", "Hello World");
131+
132+
let message = retry_read_one_line_from_journal("simple_message");
133+
assert_eq!(message["MESSAGE"], "Hello World");
134+
assert_eq!(message["PRIORITY"], "5");
135+
});
136+
}
137+
138+
#[test]
139+
fn multiline_message() {
140+
with_journald(|| {
141+
warn!(test.name = "multiline_message", "Hello\nMultiline\nWorld");
142+
143+
let message = retry_read_one_line_from_journal("multiline_message");
144+
assert_eq!(message["MESSAGE"], "Hello\nMultiline\nWorld");
145+
assert_eq!(message["PRIORITY"], "4");
146+
});
147+
}
148+
149+
#[test]
150+
fn multiline_message_trailing_newline() {
151+
with_journald(|| {
152+
error!(
153+
test.name = "multiline_message_trailing_newline",
154+
"A trailing newline\n"
155+
);
156+
157+
let message = retry_read_one_line_from_journal("multiline_message_trailing_newline");
158+
assert_eq!(message["MESSAGE"], "A trailing newline\n");
159+
assert_eq!(message["PRIORITY"], "3");
160+
});
161+
}
162+
163+
#[test]
164+
fn internal_null_byte() {
165+
with_journald(|| {
166+
debug!(test.name = "internal_null_byte", "An internal\x00byte");
167+
168+
let message = retry_read_one_line_from_journal("internal_null_byte");
169+
assert_eq!(message["MESSAGE"], b"An internal\x00byte"[..]);
170+
assert_eq!(message["PRIORITY"], "6");
171+
});
172+
}

0 commit comments

Comments
 (0)