use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use nmbrs_metrics::labels::Labels;
#[derive(Clone, Debug)]
struct TemporalParts {
yyyy: String,
yy: String,
mm: String,
dd: String,
hh: String,
mi: String,
ss: String,
}
impl TemporalParts {
fn now_local() -> Self {
let ts = crate::session::now_log_timestamp();
let take = |start: usize, len: usize| -> String {
ts.get(start..start + len).unwrap_or("").to_string()
};
let yyyy = take(0, 4);
Self {
yy: yyyy.get(2..4).unwrap_or("").to_string(),
yyyy,
mm: take(5, 2),
dd: take(8, 2),
hh: take(11, 2),
mi: take(14, 2),
ss: take(17, 2),
}
}
fn apply(&self, mut s: String) -> String {
for (tok, val) in [
("YYYY", &self.yyyy),
("YY", &self.yy),
("MM", &self.mm),
("DD", &self.dd),
("HH", &self.hh),
("MI", &self.mi),
("SS", &self.ss),
] {
s = s.replace(tok, val);
}
s
}
}
#[derive(Debug)]
struct TraceRoute {
filter: Option<(String, String)>,
template: String,
handles: Mutex<HashMap<PathBuf, File>>,
}
impl TraceRoute {
fn parse(spec: &str, session_dir: &str, temporal: &TemporalParts) -> Result<Self, String> {
let (filter, path_part) = if let Some(rest) = spec.strip_prefix('[') {
if let Some(close) = rest.find(']') {
let inside = &rest[..close];
let after = &rest[close + 1..];
if let Some((k, v)) = inside.split_once('=') {
let after = after.strip_prefix(':').ok_or_else(|| {
format!(
"trace spec: filter '[{inside}]' must be followed by ':path' \
(got '{spec}')"
)
})?;
(
Some((k.trim().to_string(), v.trim().to_string())),
after.to_string(),
)
} else {
(None, spec.to_string())
}
} else {
return Err(format!("trace spec: unclosed '[' in '{spec}'"));
}
} else {
(None, spec.to_string())
};
if path_part.is_empty() {
return Err(format!("trace spec: empty path in '{spec}'"));
}
let with_sdir = path_part.replace("SDIR", session_dir);
let template = temporal.apply(with_sdir);
Ok(Self {
filter,
template,
handles: Mutex::new(HashMap::new()),
})
}
fn matches(&self, labels: &Labels) -> bool {
match &self.filter {
None => true,
Some((k, v)) => labels.get(k).is_some_and(|val| val == v.as_str()),
}
}
fn render(&self, labels: &Labels) -> Option<PathBuf> {
let mut out = String::with_capacity(self.template.len());
let bytes = self.template.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'[' {
let close = bytes[i + 1..].iter().position(|&b| b == b']')?;
let key = std::str::from_utf8(&bytes[i + 1..i + 1 + close]).ok()?;
let val = labels.get(key)?;
out.push_str(&sanitize_path_segment(val));
i += 1 + close + 1;
} else {
out.push(bytes[i] as char);
i += 1;
}
}
let p = PathBuf::from(&out);
if p.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return None;
}
Some(p)
}
fn write(&self, path: &PathBuf, line: &[u8]) {
let mut handles = match self.handles.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
let file = match handles.get_mut(path) {
Some(f) => f,
None => {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
let _ = std::fs::create_dir_all(parent);
}
match OpenOptions::new().create(true).append(true).open(path) {
Ok(f) => {
handles.insert(path.clone(), f);
handles.get_mut(path).unwrap()
}
Err(_) => return,
}
}
};
let _ = file.write_all(line);
}
}
fn sanitize_path_segment(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect()
}
static GLOBAL_ROUTER: OnceLock<Option<TraceRouter>> = OnceLock::new();
#[derive(Debug)]
struct TraceRouter {
routes: Vec<TraceRoute>,
}
pub fn init(specs: &[String], session_dir: &std::path::Path) -> Result<usize, String> {
if specs.is_empty() {
let _ = GLOBAL_ROUTER.set(None);
return Ok(0);
}
let temporal = TemporalParts::now_local();
let session_dir_str = session_dir.to_string_lossy().to_string();
let mut routes = Vec::with_capacity(specs.len());
for spec in specs {
routes.push(TraceRoute::parse(spec, &session_dir_str, &temporal)?);
}
let n = routes.len();
let _ = GLOBAL_ROUTER.set(Some(TraceRouter { routes }));
Ok(n)
}
pub fn enabled() -> bool {
matches!(GLOBAL_ROUTER.get(), Some(Some(_)))
}
pub fn log(labels: &Labels, message: &str) {
let Some(Some(router)) = GLOBAL_ROUTER.get() else {
return;
};
let ts = crate::session::now_log_timestamp();
let line = format!("{ts} TRC {message}\n");
let bytes = line.as_bytes();
for route in &router.routes {
if !route.matches(labels) {
continue;
}
let Some(path) = route.render(labels) else {
continue;
};
route.write(&path, bytes);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temporal_stub() -> TemporalParts {
TemporalParts {
yyyy: "2026".into(),
yy: "26".into(),
mm: "05".into(),
dd: "12".into(),
hh: "14".into(),
mi: "23".into(),
ss: "05".into(),
}
}
#[test]
fn parse_plain_path() {
let r = TraceRoute::parse("SDIR/trace.log", "/s", &temporal_stub()).unwrap();
assert!(r.filter.is_none());
assert_eq!(r.template, "/s/trace.log");
}
#[test]
fn parse_filter() {
let r = TraceRoute::parse(
"[optimize_for=latency]:SDIR/lat.trace",
"/s",
&temporal_stub(),
)
.unwrap();
assert_eq!(r.filter, Some(("optimize_for".into(), "latency".into())));
assert_eq!(r.template, "/s/lat.trace");
}
#[test]
fn parse_template_with_temporal() {
let r =
TraceRoute::parse("SDIR/[optimize_for]_YYMMDD.trace", "/s", &temporal_stub()).unwrap();
assert!(r.filter.is_none());
assert_eq!(r.template, "/s/[optimize_for]_260512.trace");
}
#[test]
fn render_substitutes_label_value() {
let r = TraceRoute::parse("SDIR/[optimize_for].trace", "/s", &temporal_stub()).unwrap();
let labels = Labels::of("optimize_for", "recall");
let p = r.render(&labels).unwrap();
assert_eq!(p, PathBuf::from("/s/recall.trace"));
}
#[test]
fn render_skips_missing_label() {
let r = TraceRoute::parse("SDIR/[missing].trace", "/s", &temporal_stub()).unwrap();
let labels = Labels::of("other", "v");
assert!(r.render(&labels).is_none());
}
#[test]
fn render_sanitises_path_separator() {
let r = TraceRoute::parse("SDIR/[k].trace", "/s", &temporal_stub()).unwrap();
let labels = Labels::of("k", "evil/../../etc");
let p = r.render(&labels).unwrap();
assert!(p.to_string_lossy().contains("evil_"));
assert!(
!p.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
);
}
#[test]
fn filter_matches_label() {
let r = TraceRoute::parse("[k=v]:SDIR/x", "/s", &temporal_stub()).unwrap();
assert!(r.matches(&Labels::of("k", "v")));
assert!(!r.matches(&Labels::of("k", "other")));
assert!(!r.matches(&Labels::of("other", "v")));
}
}