Skip to content

Commit a20525a

Browse files
committed
fix(ocsf): preserve console record boundaries
Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
1 parent b78beea commit a20525a

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

‎crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ where
6060
if let Some(ocsf_event) = clone_current_event() {
6161
let line = ocsf_event.format_shorthand();
6262
if let Ok(mut w) = self.writer.lock() {
63-
let line = format!("{ts} OCSF {line}\n");
64-
let _ = w.write_all(line.as_bytes());
63+
let _ = write_record(&mut *w, format!("{ts} OCSF {line}"));
6564
}
6665
}
6766
} else if self.include_non_ocsf {
@@ -72,13 +71,22 @@ where
7271
let mut message = String::new();
7372
event.record(&mut MessageVisitor(&mut message));
7473
if let Ok(mut w) = self.writer.lock() {
75-
let line = format!("{ts} {level} {target}: {message}\n");
76-
let _ = w.write_all(line.as_bytes());
74+
let _ = write_record(&mut *w, format!("{ts} {level} {target}: {message}"));
7775
}
7876
}
7977
}
8078
}
8179

80+
/// Preserve one physical line for the entire rendered record, including its
81+
/// target and any event fields. JSON serialization handles its own escaping.
82+
fn write_record(writer: &mut impl Write, mut line: String) -> std::io::Result<()> {
83+
if line.contains(['\r', '\n']) {
84+
line = line.replace('\r', "\\r").replace('\n', "\\n");
85+
}
86+
line.push('\n');
87+
writer.write_all(line.as_bytes())
88+
}
89+
8290
/// Simple visitor that extracts the message field from tracing events.
8391
struct MessageVisitor<'a>(&'a mut String);
8492

‎crates/openshell-ocsf/tests/console_output.rs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,35 @@ fn concurrent_console_formats_submit_complete_records() {
129129
}
130130
assert_eq!(json_count, 8 * 50);
131131
}
132+
133+
#[test]
134+
fn non_json_console_records_preserve_physical_line_boundaries() {
135+
let records = Records::default();
136+
let subscriber = tracing_subscriber::registry()
137+
.with(OcsfShorthandLayer::new(records.clone()))
138+
.with(OcsfJsonlLayer::new(records.clone()).with_console_format());
139+
let message = "first\r\nsecond\nthird\rfourth";
140+
let event = openshell_ocsf::ConfigStateChangeBuilder::new(&context())
141+
.message(message)
142+
.build();
143+
tracing::subscriber::with_default(subscriber, || {
144+
tracing::info!(target: "console\r\ncontinued", message = message);
145+
ocsf_emit!(event);
146+
});
147+
let records = records.0.lock().unwrap();
148+
assert_eq!(records.len(), 3);
149+
for record in records.iter() {
150+
assert_eq!(
151+
record.iter().position(|byte| *byte == b'\n'),
152+
Some(record.len() - 1)
153+
);
154+
assert!(!record.contains(&b'\r'));
155+
assert_eq!(record.last(), Some(&b'\n'));
156+
let line = std::str::from_utf8(record).unwrap();
157+
if line.contains(" OCSF-JSON ") {
158+
assert_eq!(payload(record)["message"], message);
159+
} else {
160+
assert!(line.contains("first\\r\\nsecond\\nthird\\rfourth"));
161+
}
162+
}
163+
}

0 commit comments

Comments
 (0)