1use serde::Deserialize;
15use serde::Serialize;
16use tracing::metadata::LevelFilter;
17use tracing_subscriber::EnvFilter;
18use tracing_subscriber::filter::Directive;
19
20const VERSION: u32 = 2;
21const TOOL: &str = "hermit-detcore-liteinst-v1";
22
23#[derive(Debug)]
24pub struct EffectiveFilter {
25 directives: String,
26 filter: EnvFilter,
27}
28
29impl EffectiveFilter {
30 pub fn from_default_env(level: LevelFilter) -> Self {
34 let raw = std::env::var(EnvFilter::DEFAULT_ENV).unwrap_or_default();
35 Self::from_directives_lossy(&raw, level)
36 }
37
38 pub fn from_directives_lossy(raw: &str, level: LevelFilter) -> Self {
42 let filter = EnvFilter::new(raw)
43 .add_directive("tokio=debug".parse().expect("correct directive"))
44 .add_directive(level.into());
45 let mut accepted: Vec<_> = raw
46 .split(',')
47 .filter(|directive| !directive.is_empty())
48 .filter(|directive| directive.parse::<Directive>().is_ok())
49 .collect();
50 let level_text = level.to_string();
51 accepted.push("tokio=debug");
52 accepted.push(&level_text);
53 Self {
54 directives: accepted.join(","),
55 filter,
56 }
57 }
58
59 pub fn into_filter(self) -> EnvFilter {
60 self.filter
61 }
62}
63
64#[derive(Deserialize, Serialize)]
65#[serde(deny_unknown_fields)]
66struct Payload {
67 version: u32,
68 tool: String,
69 config_wire_fingerprint: String,
70 log_filter: String,
71}
72
73pub fn encode(
74 config_wire_fingerprint: &str,
75 log_filter: &EffectiveFilter,
76) -> Result<Vec<u8>, serde_json::Error> {
77 serde_json::to_vec(&Payload {
78 version: VERSION,
79 tool: TOOL.to_owned(),
80 config_wire_fingerprint: config_wire_fingerprint.to_owned(),
81 log_filter: log_filter.directives.clone(),
82 })
83}
84
85pub fn decode(bytes: &[u8], expected_fingerprint: &str) -> Result<EffectiveFilter, String> {
86 let payload: Payload = serde_json::from_slice(bytes)
87 .map_err(|error| format!("invalid bootstrap payload: {error}"))?;
88 if payload.version != VERSION {
89 return Err("unsupported bootstrap payload version".to_owned());
90 }
91 if payload.tool != TOOL {
92 return Err("bootstrap tool mismatch".to_owned());
93 }
94 if payload.config_wire_fingerprint != expected_fingerprint {
95 return Err("bootstrap config fingerprint mismatch".to_owned());
96 }
97 let filter = EnvFilter::try_new(&payload.log_filter)
98 .map_err(|error| format!("invalid bootstrap log filter: {error}"))?;
99 Ok(EffectiveFilter {
100 directives: payload.log_filter,
101 filter,
102 })
103}
104
105#[cfg(test)]
106mod tests {
107 use serde_json::Value;
108 use serde_json::json;
109
110 use super::*;
111
112 const FINGERPRINT: &str = "fixture-config-fingerprint";
113
114 fn payload() -> Value {
115 json!({
116 "version": VERSION,
117 "tool": TOOL,
118 "config_wire_fingerprint": FINGERPRINT,
119 "log_filter": "info,tokio=debug,detcore[work{task=7}]=trace",
120 })
121 }
122
123 #[test]
124 fn schema_and_filter_round_trip() {
125 let raw = payload()["log_filter"].as_str().unwrap().to_owned();
126 let filter = EffectiveFilter::from_directives_lossy(&raw, LevelFilter::WARN);
127 let encoded = encode(FINGERPRINT, &filter).unwrap();
128 let mut expected = payload();
129 expected["log_filter"] = json!(format!("{raw},tokio=debug,warn"));
130 assert_eq!(serde_json::from_slice::<Value>(&encoded).unwrap(), expected);
131 let decoded = decode(&encoded, FINGERPRINT).unwrap();
132 assert_eq!(decoded.directives, filter.directives);
133 assert_eq!(encode(FINGERPRINT, &decoded).unwrap(), encoded);
134 }
135
136 #[test]
137 fn preserves_accepted_spelling_and_override_order() {
138 let raw = "warn,,detcore[work{task=1.0}]=info,tokio=off,broken=bogus,detcore[work{task=-1.0}]=debug,detcore[work{task=1e0}]=trace";
139 let filter = EffectiveFilter::from_directives_lossy(raw, LevelFilter::WARN);
140 let expected = "warn,detcore[work{task=1.0}]=info,tokio=off,detcore[work{task=-1.0}]=debug,detcore[work{task=1e0}]=trace,tokio=debug,warn";
141 let encoded = encode(FINGERPRINT, &filter).unwrap();
142 assert_eq!(
143 serde_json::from_slice::<Value>(&encoded).unwrap()["log_filter"],
144 expected
145 );
146 let decoded = decode(&encoded, FINGERPRINT).unwrap();
147 assert_eq!(decoded.directives, expected);
148 assert_eq!(encode(FINGERPRINT, &decoded).unwrap(), encoded);
149 }
150
151 use std::io;
152 use std::io::Write;
153 use std::sync::Arc;
154 use std::sync::Mutex;
155
156 #[derive(Clone)]
157 struct Records(Arc<Mutex<Vec<u8>>>);
158
159 impl Write for Records {
160 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
161 self.0.lock().unwrap().extend_from_slice(bytes);
162 Ok(bytes.len())
163 }
164
165 fn flush(&mut self) -> io::Result<()> {
166 Ok(())
167 }
168 }
169
170 fn capture(filter: EnvFilter) -> Vec<u8> {
171 let records = Records(Arc::new(Mutex::new(Vec::new())));
172 let writer = records.clone();
173 let subscriber = tracing_subscriber::fmt()
174 .with_env_filter(filter)
175 .without_time()
176 .with_ansi(false)
177 .with_writer(move || writer.clone())
178 .finish();
179 tracing::subscriber::with_default(subscriber, || {
180 tracing::info!(target: "bootstrap_fixture", "outside info");
181 tracing::debug!(target: "tokio", "Tokio override");
182 for task in [1.0_f64, -1.0, 2.0] {
183 let span = tracing::info_span!(target: "bootstrap_fixture", "work", task);
184 let _entered = span.enter();
185 tracing::debug!(target: "bootstrap_fixture", "inside debug");
186 tracing::trace!(target: "bootstrap_fixture", "inside trace");
187 }
188 tracing::warn!(target: "bootstrap_fixture", "warning");
189 });
190 records.0.lock().unwrap().clone()
191 }
192
193 #[test]
194 fn decoded_filter_selects_the_same_events_and_span_fields_as_the_host() {
195 for raw in [
196 "",
197 "off,tokio=off",
198 "broken=bogus,bootstrap_fixture=trace",
199 "off,bootstrap_fixture[work{task=1.0}]=trace",
200 "off,bootstrap_fixture[work{task=-1.0}]=debug,bootstrap_fixture[work{task=1e0}]=trace",
201 "bootstrap_fixture=info,bootstrap_fixture=trace,bootstrap_fixture=warn",
202 ] {
203 let host = EffectiveFilter::from_directives_lossy(raw, LevelFilter::WARN);
204 let payload = encode(FINGERPRINT, &host).unwrap();
205 let guest = decode(&payload, FINGERPRINT).unwrap();
206 let original = EnvFilter::new(raw)
208 .add_directive("tokio=debug".parse().unwrap())
209 .add_directive(LevelFilter::WARN.into());
210 let expected = capture(original);
211 assert_eq!(capture(host.into_filter()), expected, "host: {raw}");
212 assert!(
213 expected
214 .windows(b"Tokio override".len())
215 .any(|part| part == b"Tokio override"),
216 "the mandatory Tokio directive must select a real event: {raw}"
217 );
218 if raw.contains("work{task=") {
219 assert!(
220 expected
221 .windows(b"inside trace".len())
222 .any(|part| part == b"inside trace"),
223 "the dynamic matcher must select an event in its span: {raw}"
224 );
225 assert!(
226 !expected
227 .windows(b"outside info".len())
228 .any(|part| part == b"outside info"),
229 "the dynamic matcher must not enable the same target outside its span: {raw}"
230 );
231 }
232 assert_eq!(capture(guest.into_filter()), expected, "{raw}");
233 }
234 }
235
236 #[test]
239 fn default_environment_filter_subprocess() {
240 if std::env::var_os("HERMIT_BOOTSTRAP_FILTER_CHILD").is_none() {
241 return;
242 }
243 let host = EffectiveFilter::from_default_env(LevelFilter::WARN);
244 let encoded = encode(FINGERPRINT, &host).unwrap();
245 let original = EnvFilter::from_default_env()
246 .add_directive("tokio=debug".parse().unwrap())
247 .add_directive(LevelFilter::WARN.into());
248 let expected = capture(original);
249 assert!(
250 expected
251 .windows(b"Tokio override".len())
252 .any(|part| part == b"Tokio override")
253 );
254 assert_eq!(capture(host.into_filter()), expected);
255 assert_eq!(
256 capture(decode(&encoded, FINGERPRINT).unwrap().into_filter()),
257 expected
258 );
259 }
260
261 #[test]
262 fn environment_policy_matches_original_cli_for_missing_invalid_and_valid_values() {
263 use std::ffi::OsString;
264 use std::os::unix::ffi::OsStringExt;
265 use std::process::Command;
266 use std::process::Stdio;
267 use std::time::Duration;
268 use std::time::Instant;
269
270 for value in [
271 None,
272 Some(OsString::new()),
273 Some(OsString::from_vec(vec![0xff])),
274 Some(OsString::from(
275 "broken=bogus,bootstrap_fixture=trace,tokio=off",
276 )),
277 Some(OsString::from(
278 "off,bootstrap_fixture[work{task=1.0}]=trace",
279 )),
280 ] {
281 let mut child = Command::new(std::env::current_exe().unwrap());
282 child.args([
283 "--exact",
284 "liteinst_bootstrap::tests::default_environment_filter_subprocess",
285 "--nocapture",
286 ]);
287 child.env("HERMIT_BOOTSTRAP_FILTER_CHILD", "1");
288 child.env_remove(EnvFilter::DEFAULT_ENV);
289 if let Some(value) = &value {
290 child.env(EnvFilter::DEFAULT_ENV, value);
291 }
292 child.stdout(Stdio::piped()).stderr(Stdio::piped());
293 let mut child = child.spawn().unwrap();
294 let deadline = Instant::now() + Duration::from_secs(10);
295 while child.try_wait().unwrap().is_none() {
296 if Instant::now() >= deadline {
297 let _ = child.kill();
298 let output = child.wait_with_output().unwrap();
299 panic!("filter child exceeded ten seconds: {value:?}: {output:?}");
300 }
301 std::thread::sleep(Duration::from_millis(10));
302 }
303 let output = child.wait_with_output().unwrap();
304 assert!(output.status.success(), "{value:?}: {output:?}");
305 assert!(
306 String::from_utf8_lossy(&output.stdout).contains("1 passed; 0 failed"),
307 "{value:?}: {output:?}"
308 );
309 }
310 }
311
312 #[test]
313 fn refuses_wrong_version_tool_fingerprint_and_filter() {
314 for (field, value, expected) in [
315 ("version", json!(1), "unsupported bootstrap payload version"),
316 ("tool", json!("other-tool"), "bootstrap tool mismatch"),
317 (
318 "config_wire_fingerprint",
319 json!("other-config"),
320 "bootstrap config fingerprint mismatch",
321 ),
322 (
323 "log_filter",
324 json!("detcore=bogus"),
325 "invalid bootstrap log filter:",
326 ),
327 ] {
328 let mut invalid = payload();
329 invalid[field] = value;
330 let error = decode(&serde_json::to_vec(&invalid).unwrap(), FINGERPRINT).unwrap_err();
331 assert!(error.starts_with(expected), "{field}: {error}");
332 }
333 }
334
335 #[test]
336 fn refuses_missing_unknown_and_wrong_typed_fields() {
337 for field in ["version", "tool", "config_wire_fingerprint", "log_filter"] {
338 let mut missing = payload();
339 missing.as_object_mut().unwrap().remove(field);
340 assert!(decode(&serde_json::to_vec(&missing).unwrap(), FINGERPRINT).is_err());
341 let mut wrong_type = payload();
342 wrong_type[field] = json!(false);
343 assert!(decode(&serde_json::to_vec(&wrong_type).unwrap(), FINGERPRINT).is_err());
344 }
345 let mut unknown = payload();
346 unknown["extra"] = json!(0);
347 assert!(decode(&serde_json::to_vec(&unknown).unwrap(), FINGERPRINT).is_err());
348 }
349
350 #[test]
351 fn refuses_old_payload_duplicate_fields_trailing_data_and_invalid_utf8() {
352 let old = json!({"tool": TOOL, "config_wire_fingerprint": FINGERPRINT});
353 assert!(decode(&serde_json::to_vec(&old).unwrap(), FINGERPRINT).is_err());
354 let valid = serde_json::to_string(&payload()).unwrap();
355 let duplicate = format!("{{\"version\":2,{}", &valid[1..]);
356 assert!(decode(duplicate.as_bytes(), FINGERPRINT).is_err());
357 assert!(decode(format!("{valid} null").as_bytes(), FINGERPRINT).is_err());
358 assert!(decode(b"\xff", FINGERPRINT).is_err());
359 assert!(decode(b"{", FINGERPRINT).is_err());
360 }
361}