pub const TRANSFORM_ID: &str = "log_field_fold";
pub const TRANSFORM_VERSION: &str = "1.0.0";
const HEADER: &str = "__tf_logfold1__";
const PH: char = '\u{0}';
const MIN_LINES: usize = 3;
use regex::Regex;
use std::sync::OnceLock;
fn var_pattern() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"0x[0-9a-fA-F]+|[0-9]+").expect("var_pattern is a valid literal"))
}
fn templatize(segment: &str) -> (String, Vec<&str>) {
let re = var_pattern();
let mut template = String::with_capacity(segment.len());
let mut caps = Vec::new();
let mut last = 0;
for m in re.find_iter(segment) {
template.push_str(&segment[last..m.start()]);
template.push(PH);
caps.push(m.as_str());
last = m.end();
}
template.push_str(&segment[last..]);
(template, caps)
}
pub fn fold_log(input: &str) -> String {
if input.is_empty() || input.contains(PH) {
return input.to_string();
}
let segments: Vec<&str> = input.split_inclusive('\n').collect();
if segments.len() < MIN_LINES {
return input.to_string();
}
let mut templates: Vec<String> = Vec::new();
let mut ids: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut rows: Vec<(usize, Vec<&str>)> = Vec::with_capacity(segments.len());
for seg in &segments {
let (template, caps) = templatize(seg);
let id = *ids.entry(template.clone()).or_insert_with(|| {
templates.push(template);
templates.len() - 1
});
rows.push((id, caps));
}
if templates.len() >= segments.len() {
return input.to_string();
}
let mut out = String::with_capacity(input.len() / 2);
out.push_str(HEADER);
out.push('\n');
out.push_str(&serde_json::to_string(&templates).expect("Vec<String> always serializes"));
for (id, caps) in &rows {
out.push('\n');
out.push_str(&id.to_string());
for cap in caps {
out.push(' ');
out.push_str(cap);
}
}
out
}
pub fn unfold_log(input: &str) -> String {
match try_unfold(input) {
Some(s) => s,
None => input.to_string(),
}
}
fn try_unfold(input: &str) -> Option<String> {
let mut lines = input.split('\n');
if lines.next()? != HEADER {
return None;
}
let templates: Vec<String> = serde_json::from_str(lines.next()?).ok()?;
let mut out = String::with_capacity(input.len() * 2);
for row in lines {
let mut fields = row.split(' ');
let id: usize = fields.next()?.parse().ok()?;
let template = templates.get(id)?;
let mut caps = fields;
let mut parts = template.split(PH);
out.push_str(parts.next()?);
for part in parts {
out.push_str(caps.next()?);
out.push_str(part);
}
if caps.next().is_some() {
return None; }
}
Some(out)
}
pub fn round_trips(before: &[u8], after: &[u8]) -> bool {
let (Ok(before_s), Ok(after_s)) = (std::str::from_utf8(before), std::str::from_utf8(after))
else {
return false;
};
unfold_log(after_s) == before_s
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_lossless(input: &str) {
let folded = fold_log(input);
assert!(
round_trips(input.as_bytes(), folded.as_bytes()),
"round_trips() rejected the fold of {input:?}"
);
assert_eq!(
unfold_log(&folded),
input,
"unfold != original for {input:?}"
);
}
#[test]
fn folds_templated_log_and_emits_skeleton_once() {
let input: String = (0..40)
.map(|i| format!("req=req-{i:04} status=200 ms={}\n", 30 + i % 20))
.collect();
let folded = fold_log(&input);
assert!(
folded.starts_with(HEADER),
"expected folded form, got {folded:?}"
);
assert_eq!(folded.matches("status").count(), 1);
assert!(
folded.len() < input.len(),
"fold ({}) not smaller than input ({})",
folded.len(),
input.len()
);
assert_lossless(&input);
}
#[test]
fn preserves_crlf_blank_lines_and_missing_final_newline() {
assert_lossless("a=1\r\na=2\r\na=3\r\n");
assert_lossless("x=1\n\nx=2\n\nx=3\n");
assert_lossless("x=1\nx=2\nx=3"); }
#[test]
fn no_shared_templates_is_left_unchanged() {
let input = "alpha\nbeta gamma\ndelta epsilon zeta\n";
assert_eq!(fold_log(input), input);
}
#[test]
fn fewer_than_min_lines_is_left_unchanged() {
let input = "req=1 ok\nreq=2 ok\n";
assert_eq!(fold_log(input), input);
}
#[test]
fn empty_input_is_a_noop() {
assert_eq!(fold_log(""), "");
assert_eq!(unfold_log(""), "");
}
#[test]
fn lines_with_no_variable_tokens_still_fold_when_identical() {
let input = "heartbeat ok\nheartbeat ok\nheartbeat ok\n";
assert_lossless(input);
}
#[test]
fn hex_and_decimal_tokens_both_captured() {
let input = "addr=0x1f val=10\naddr=0x2a val=20\naddr=0x3b val=30\n";
assert_lossless(input);
assert!(fold_log(input).starts_with(HEADER));
}
#[test]
fn input_containing_the_placeholder_char_is_not_folded() {
let input = "a\u{0}b\na\u{0}c\na\u{0}d\n";
assert_eq!(fold_log(input), input);
}
#[test]
fn genuine_input_shaped_like_the_header_round_trips_safely() {
let input = "__tf_logfold1__\n[\"x\"]\n0 boom";
let folded = fold_log(input);
assert_eq!(unfold_log(&folded), input);
}
use proptest::prelude::*;
fn arb_log() -> impl Strategy<Value = String> {
let line = prop_oneof![
(0u32..999u32).prop_map(|n| format!("req={n} status=200")),
(0u32..999u32).prop_map(|n| format!("addr=0x{n:x} ok")),
Just("heartbeat".to_string()),
"[a-z ]{0,8}".prop_map(|s| s),
];
(prop::collection::vec(line, 0..40), any::<bool>()).prop_map(|(lines, trailing)| {
let mut s = lines.join("\n");
if trailing && !s.is_empty() {
s.push('\n');
}
s
})
}
proptest! {
#[test]
fn fold_then_unfold_is_the_identity(input in arb_log()) {
let folded = fold_log(&input);
prop_assert!(round_trips(input.as_bytes(), folded.as_bytes()));
prop_assert_eq!(unfold_log(&folded), input);
}
}
}