• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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