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
9 use tracing::{debug, error, info, info_span, warn};
10 use tracing_journald::Layer;
11 use tracing_subscriber::layer::SubscriberExt;
12 use tracing_subscriber::Registry;
13
journalctl_version() -> std::io::Result<String>14 fn journalctl_version() -> std::io::Result<String> {
15 let output = Command::new("journalctl").arg("--version").output()?;
16 Ok(String::from_utf8_lossy(&output.stdout).to_string())
17 }
18
with_journald(f: impl FnOnce())19 fn with_journald(f: impl FnOnce()) {
20 with_journald_layer(Layer::new().unwrap().with_field_prefix(None), f)
21 }
22
with_journald_layer(layer: Layer, f: impl FnOnce())23 fn with_journald_layer(layer: Layer, f: impl FnOnce()) {
24 match journalctl_version() {
25 Ok(_) => {
26 let sub = Registry::default().with(layer);
27 tracing::subscriber::with_default(sub, f);
28 }
29 Err(error) => eprintln!(
30 "SKIPPING TEST: journalctl --version failed with error: {}",
31 error
32 ),
33 }
34 }
35
36 #[derive(Debug, PartialEq, Deserialize)]
37 #[serde(untagged)]
38 enum Field {
39 Text(String),
40 Array(Vec<String>),
41 Binary(Vec<u8>),
42 }
43
44 impl Field {
as_array(&self) -> Option<&[String]>45 fn as_array(&self) -> Option<&[String]> {
46 match self {
47 Field::Text(_) => None,
48 Field::Binary(_) => None,
49 Field::Array(v) => Some(v),
50 }
51 }
52
as_text(&self) -> Option<&str>53 fn as_text(&self) -> Option<&str> {
54 match self {
55 Field::Text(v) => Some(v.as_str()),
56 Field::Binary(_) => None,
57 Field::Array(_) => None,
58 }
59 }
60 }
61
62 // Convenience impls to compare fields against strings and bytes with assert_eq!
63 impl PartialEq<&str> for Field {
eq(&self, other: &&str) -> bool64 fn eq(&self, other: &&str) -> bool {
65 match self {
66 Field::Text(s) => s == other,
67 Field::Binary(_) => false,
68 Field::Array(_) => false,
69 }
70 }
71 }
72
73 impl PartialEq<[u8]> for Field {
eq(&self, other: &[u8]) -> bool74 fn eq(&self, other: &[u8]) -> bool {
75 match self {
76 Field::Text(s) => s.as_bytes() == other,
77 Field::Binary(data) => data == other,
78 Field::Array(_) => false,
79 }
80 }
81 }
82
83 impl PartialEq<Vec<&str>> for Field {
eq(&self, other: &Vec<&str>) -> bool84 fn eq(&self, other: &Vec<&str>) -> bool {
85 match self {
86 Field::Text(_) => false,
87 Field::Binary(_) => false,
88 Field::Array(data) => data == other,
89 }
90 }
91 }
92
93 /// Retry `f` 30 times 100ms apart, i.e. a total of three seconds.
94 ///
95 /// When `f` returns an error wait 100ms and try it again, up to ten times.
96 /// If the last attempt failed return the error returned by that attempt.
97 ///
98 /// If `f` returns Ok immediately return the result.
retry<T, E>(f: impl Fn() -> Result<T, E>) -> Result<T, E>99 fn retry<T, E>(f: impl Fn() -> Result<T, E>) -> Result<T, E> {
100 let attempts = 30;
101 let interval = Duration::from_millis(100);
102 for attempt in (0..attempts).rev() {
103 match f() {
104 Ok(result) => return Ok(result),
105 Err(e) if attempt == 0 => return Err(e),
106 Err(_) => std::thread::sleep(interval),
107 }
108 }
109 unreachable!()
110 }
111
112 /// Read from journal with `journalctl`.
113 ///
114 /// `test_name` is a string to match in the `TEST_NAME` field
115 /// of the `journalctl` call, to make sure to only select journal entries
116 /// originating from and relevant to the current test.
117 ///
118 /// Additionally filter by the `_PID` field with the PID of this
119 /// test process, to make sure this method only reads journal entries
120 /// created by this test process.
read_from_journal(test_name: &str) -> Vec<HashMap<String, Field>>121 fn read_from_journal(test_name: &str) -> Vec<HashMap<String, Field>> {
122 let stdout = String::from_utf8(
123 Command::new("journalctl")
124 // We pass --all to circumvent journalctl's default limit of 4096 bytes for field values
125 .args(&["--user", "--output=json", "--all"])
126 // Filter by the PID of the current test process
127 .arg(format!("_PID={}", std::process::id()))
128 .arg(format!("TEST_NAME={}", test_name))
129 .output()
130 .unwrap()
131 .stdout,
132 )
133 .unwrap();
134
135 stdout
136 .lines()
137 .map(|l| serde_json::from_str(l).unwrap())
138 .collect()
139 }
140
141 /// Read exactly one line from journal for the given test name.
142 ///
143 /// Try to read lines for `testname` from journal, and `retry()` if the wasn't
144 /// _exactly_ one matching line.
retry_read_one_line_from_journal(testname: &str) -> HashMap<String, Field>145 fn retry_read_one_line_from_journal(testname: &str) -> HashMap<String, Field> {
146 retry(|| {
147 let mut messages = read_from_journal(testname);
148 if messages.len() == 1 {
149 Ok(messages.pop().unwrap())
150 } else {
151 Err(format!(
152 "one messages expected, got {} messages",
153 messages.len()
154 ))
155 }
156 })
157 .unwrap()
158 }
159
160 #[test]
simple_message()161 fn simple_message() {
162 with_journald(|| {
163 info!(test.name = "simple_message", "Hello World");
164
165 let message = retry_read_one_line_from_journal("simple_message");
166 assert_eq!(message["MESSAGE"], "Hello World");
167 assert_eq!(message["PRIORITY"], "5");
168 });
169 }
170
171 #[test]
multiline_message()172 fn multiline_message() {
173 with_journald(|| {
174 warn!(test.name = "multiline_message", "Hello\nMultiline\nWorld");
175
176 let message = retry_read_one_line_from_journal("multiline_message");
177 assert_eq!(message["MESSAGE"], "Hello\nMultiline\nWorld");
178 assert_eq!(message["PRIORITY"], "4");
179 });
180 }
181
182 #[test]
multiline_message_trailing_newline()183 fn multiline_message_trailing_newline() {
184 with_journald(|| {
185 error!(
186 test.name = "multiline_message_trailing_newline",
187 "A trailing newline\n"
188 );
189
190 let message = retry_read_one_line_from_journal("multiline_message_trailing_newline");
191 assert_eq!(message["MESSAGE"], "A trailing newline\n");
192 assert_eq!(message["PRIORITY"], "3");
193 });
194 }
195
196 #[test]
internal_null_byte()197 fn internal_null_byte() {
198 with_journald(|| {
199 debug!(test.name = "internal_null_byte", "An internal\x00byte");
200
201 let message = retry_read_one_line_from_journal("internal_null_byte");
202 assert_eq!(message["MESSAGE"], b"An internal\x00byte"[..]);
203 assert_eq!(message["PRIORITY"], "6");
204 });
205 }
206
207 #[test]
large_message()208 fn large_message() {
209 let large_string = "b".repeat(512_000);
210 with_journald(|| {
211 debug!(test.name = "large_message", "Message: {}", large_string);
212
213 let message = retry_read_one_line_from_journal("large_message");
214 assert_eq!(
215 message["MESSAGE"],
216 format!("Message: {}", large_string).as_str()
217 );
218 assert_eq!(message["PRIORITY"], "6");
219 });
220 }
221
222 #[test]
simple_metadata()223 fn simple_metadata() {
224 let sub = Layer::new()
225 .unwrap()
226 .with_field_prefix(None)
227 .with_syslog_identifier("test_ident".to_string());
228 with_journald_layer(sub, || {
229 info!(test.name = "simple_metadata", "Hello World");
230
231 let message = retry_read_one_line_from_journal("simple_metadata");
232 assert_eq!(message["MESSAGE"], "Hello World");
233 assert_eq!(message["PRIORITY"], "5");
234 assert_eq!(message["TARGET"], "journal");
235 assert_eq!(message["SYSLOG_IDENTIFIER"], "test_ident");
236 assert!(message["CODE_FILE"].as_text().is_some());
237 assert!(message["CODE_LINE"].as_text().is_some());
238 });
239 }
240
241 #[test]
span_metadata()242 fn span_metadata() {
243 with_journald(|| {
244 let s1 = info_span!("span1", span_field1 = "foo1");
245 let _g1 = s1.enter();
246
247 info!(test.name = "span_metadata", "Hello World");
248
249 let message = retry_read_one_line_from_journal("span_metadata");
250 assert_eq!(message["MESSAGE"], "Hello World");
251 assert_eq!(message["PRIORITY"], "5");
252 assert_eq!(message["TARGET"], "journal");
253
254 assert_eq!(message["SPAN_FIELD1"].as_text(), Some("foo1"));
255 assert_eq!(message["SPAN_NAME"].as_text(), Some("span1"));
256
257 assert!(message["CODE_FILE"].as_text().is_some());
258 assert!(message["CODE_LINE"].as_text().is_some());
259
260 assert!(message["SPAN_CODE_FILE"].as_text().is_some());
261 assert!(message["SPAN_CODE_LINE"].as_text().is_some());
262 });
263 }
264
265 #[test]
multiple_spans_metadata()266 fn multiple_spans_metadata() {
267 with_journald(|| {
268 let s1 = info_span!("span1", span_field1 = "foo1");
269 let _g1 = s1.enter();
270 let s2 = info_span!("span2", span_field1 = "foo2");
271 let _g2 = s2.enter();
272
273 info!(test.name = "multiple_spans_metadata", "Hello World");
274
275 let message = retry_read_one_line_from_journal("multiple_spans_metadata");
276 assert_eq!(message["MESSAGE"], "Hello World");
277 assert_eq!(message["PRIORITY"], "5");
278 assert_eq!(message["TARGET"], "journal");
279
280 assert_eq!(message["SPAN_FIELD1"], vec!["foo1", "foo2"]);
281 assert_eq!(message["SPAN_NAME"], vec!["span1", "span2"]);
282
283 assert!(message["CODE_FILE"].as_text().is_some());
284 assert!(message["CODE_LINE"].as_text().is_some());
285
286 assert!(message.contains_key("SPAN_CODE_FILE"));
287 assert_eq!(message["SPAN_CODE_LINE"].as_array().unwrap().len(), 2);
288 });
289 }
290
291 #[test]
spans_field_collision()292 fn spans_field_collision() {
293 with_journald(|| {
294 let s1 = info_span!("span1", span_field = "foo1");
295 let _g1 = s1.enter();
296 let s2 = info_span!("span2", span_field = "foo2");
297 let _g2 = s2.enter();
298
299 info!(
300 test.name = "spans_field_collision",
301 span_field = "foo3",
302 "Hello World"
303 );
304
305 let message = retry_read_one_line_from_journal("spans_field_collision");
306 assert_eq!(message["MESSAGE"], "Hello World");
307 assert_eq!(message["SPAN_NAME"], vec!["span1", "span2"]);
308
309 assert_eq!(message["SPAN_FIELD"], vec!["foo1", "foo2", "foo3"]);
310 });
311 }
312