Skip to main content

redline_core/
encode.rs

1use alloc::vec::Vec;
2use smallvec::SmallVec;
3use tracing_core::{Level, Metadata};
4
5use crate::record::{FieldValue, OwnedField, OwnedRecord, SpanSnapshot, Timestamp};
6
7/// Output format used by `redline`.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum OutputFormat {
10    /// Newline-delimited JSON.
11    Ndjson,
12    /// Compact binary frames for lower write volume and later decoding.
13    Binary,
14}
15
16/// Controls which span context is included during encoding.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct EncodeConfig {
19    /// Include the current span object.
20    pub include_current_span: bool,
21    /// Include the full entered span chain.
22    pub include_span_list: bool,
23}
24
25impl Default for EncodeConfig {
26    fn default() -> Self {
27        Self {
28            include_current_span: true,
29            include_span_list: false,
30        }
31    }
32}
33
34/// Distinguishes event and span callsites.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum CallsiteKind {
37    Event,
38    Span,
39}
40
41/// Static metadata sent once per callsite in binary mode.
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct CallsiteMetadata {
44    pub id: u32,
45    pub name: &'static str,
46    pub target: &'static str,
47    pub level: Level,
48    pub file: Option<&'static str>,
49    pub line: Option<u32>,
50    pub module_path: Option<&'static str>,
51    pub fields: SmallVec<[&'static str; 8]>,
52    pub kind: CallsiteKind,
53}
54
55impl CallsiteMetadata {
56    /// Builds callsite metadata from `tracing` metadata.
57    pub fn from_metadata(id: u32, metadata: &'static Metadata<'static>) -> Self {
58        let mut fields = SmallVec::new();
59        for field in metadata.fields().iter() {
60            fields.push(field.name());
61        }
62
63        Self {
64            id,
65            name: metadata.name(),
66            target: metadata.target(),
67            level: *metadata.level(),
68            file: metadata.file(),
69            line: metadata.line(),
70            module_path: metadata.module_path(),
71            fields,
72            kind: if metadata.is_span() {
73                CallsiteKind::Span
74            } else {
75                CallsiteKind::Event
76            },
77        }
78    }
79}
80
81/// Binary frame type identifier.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum BinaryFrameKind {
84    Metadata = 1,
85    Event = 2,
86}
87
88const KEY_TIMESTAMP_UNIX_NS: &[u8] = br#""timestamp_unix_ns":"#;
89const KEY_LEVEL: &[u8] = br#""level":"#;
90const KEY_TARGET: &[u8] = br#""target":"#;
91const KEY_NAME: &[u8] = br#""name":"#;
92const KEY_METADATA_ID: &[u8] = br#""metadata_id":"#;
93const KEY_FIELDS: &[u8] = br#""fields":"#;
94const KEY_SPAN: &[u8] = br#""span":"#;
95const KEY_SPANS: &[u8] = br#""spans":"#;
96const KEY_ID: &[u8] = br#""id":"#;
97
98/// Encodes one owned record as NDJSON.
99pub fn encode_ndjson_record(config: EncodeConfig, record: &OwnedRecord, out: &mut Vec<u8>) {
100    out.clear();
101    out.reserve(estimate_ndjson_record_len(config, record));
102    out.push(b'{');
103    out.extend_from_slice(KEY_TIMESTAMP_UNIX_NS);
104    push_timestamp_unix_nanos(out, record.timestamp);
105    out.push(b',');
106    out.extend_from_slice(KEY_LEVEL);
107    push_json_string(out, level_str(record.level));
108    out.push(b',');
109    out.extend_from_slice(KEY_TARGET);
110    push_json_string(out, record.target);
111    out.push(b',');
112    out.extend_from_slice(KEY_NAME);
113    push_json_string(out, record.name);
114    out.push(b',');
115    out.extend_from_slice(KEY_METADATA_ID);
116    push_json_u32(out, record.metadata_id);
117    out.push(b',');
118    out.extend_from_slice(KEY_FIELDS);
119    push_fields_object(out, &record.fields);
120
121    if config.include_current_span
122        && let Some(span) = &record.current_span
123    {
124        out.push(b',');
125        out.extend_from_slice(KEY_SPAN);
126        push_span(out, span);
127    }
128
129    if config.include_span_list {
130        out.push(b',');
131        out.extend_from_slice(KEY_SPANS);
132        push_spans(out, &record.spans);
133    }
134
135    out.extend_from_slice(b"}\n");
136}
137
138/// Encodes one callsite metadata frame for binary output.
139pub fn encode_binary_metadata(metadata: &CallsiteMetadata, out: &mut Vec<u8>) {
140    out.clear();
141    out.push(BinaryFrameKind::Metadata as u8);
142    let len_offset = reserve_length(out);
143    push_u32_le(out, metadata.id);
144    out.push(match metadata.kind {
145        CallsiteKind::Event => 1,
146        CallsiteKind::Span => 2,
147    });
148    out.push(level_code(metadata.level));
149    push_optional_string(out, metadata.file);
150    push_optional_u32(out, metadata.line);
151    push_optional_string(out, metadata.module_path);
152    push_string(out, metadata.name);
153    push_string(out, metadata.target);
154    push_u16_le(out, metadata.fields.len() as u16);
155    for field in &metadata.fields {
156        push_string(out, field);
157    }
158    write_length(out, len_offset);
159}
160
161/// Encodes one owned record as a binary event frame.
162pub fn encode_binary_record(config: EncodeConfig, record: &OwnedRecord, out: &mut Vec<u8>) {
163    out.clear();
164    out.push(BinaryFrameKind::Event as u8);
165    let len_offset = reserve_length(out);
166    push_u64_le(out, record.timestamp.unix_seconds);
167    push_u32_le(out, record.timestamp.subsec_nanos);
168    push_u32_le(out, record.metadata_id);
169    push_u64_le(
170        out,
171        record
172            .current_span
173            .as_ref()
174            .map(|span| span.id)
175            .unwrap_or_default(),
176    );
177    push_fields(out, &record.fields);
178
179    if config.include_current_span {
180        match &record.current_span {
181            Some(span) => {
182                out.push(1);
183                push_span_binary(out, span);
184            }
185            None => out.push(0),
186        }
187    } else {
188        out.push(0);
189    }
190
191    if config.include_span_list {
192        push_u16_le(out, record.spans.len() as u16);
193        for span in &record.spans {
194            push_span_binary(out, span);
195        }
196    } else {
197        push_u16_le(out, 0);
198    }
199
200    write_length(out, len_offset);
201}
202
203fn push_span(out: &mut Vec<u8>, span: &SpanSnapshot) {
204    out.push(b'{');
205    out.extend_from_slice(KEY_ID);
206    push_json_u64(out, span.id);
207    out.push(b',');
208    out.extend_from_slice(KEY_METADATA_ID);
209    push_json_u32(out, span.metadata_id);
210    out.push(b',');
211    out.extend_from_slice(KEY_NAME);
212    push_json_string(out, span.name);
213    out.push(b',');
214    out.extend_from_slice(KEY_TARGET);
215    push_json_string(out, span.target);
216    out.push(b',');
217    out.extend_from_slice(KEY_LEVEL);
218    push_json_string(out, level_str(span.level));
219    out.push(b',');
220    out.extend_from_slice(KEY_FIELDS);
221    push_fields_object(out, &span.fields);
222    out.push(b'}');
223}
224
225fn push_spans(out: &mut Vec<u8>, spans: &[SpanSnapshot]) {
226    out.push(b'[');
227    for (index, span) in spans.iter().enumerate() {
228        if index > 0 {
229            out.push(b',');
230        }
231        push_span(out, span);
232    }
233    out.push(b']');
234}
235
236fn push_fields_object(out: &mut Vec<u8>, fields: &[OwnedField]) {
237    out.push(b'{');
238    for (index, field) in fields.iter().enumerate() {
239        if index > 0 {
240            out.push(b',');
241        }
242        push_json_key(out, field.name);
243        push_field_value(out, &field.value);
244    }
245    out.push(b'}');
246}
247
248fn push_field_value(out: &mut Vec<u8>, value: &FieldValue) {
249    match value {
250        FieldValue::Bool(value) => push_bool(out, *value),
251        FieldValue::I64(value) => push_i64(out, *value),
252        FieldValue::U64(value) => push_json_u64(out, *value),
253        FieldValue::I128(value) => push_i128(out, *value),
254        FieldValue::U128(value) => push_u128(out, *value),
255        FieldValue::F64(value) => push_f64(out, *value),
256        FieldValue::Str(value) | FieldValue::Debug(value) => push_json_string(out, value.as_str()),
257        FieldValue::Bytes(value) => push_json_hex_bytes(out, value),
258    }
259}
260
261fn push_json_key(out: &mut Vec<u8>, key: &str) {
262    push_json_string(out, key);
263    out.push(b':');
264}
265
266fn push_json_string(out: &mut Vec<u8>, value: &str) {
267    out.push(b'"');
268    let bytes = value.as_bytes();
269    if bytes.iter().all(|byte| !needs_json_escape(*byte)) {
270        out.extend_from_slice(bytes);
271        out.push(b'"');
272        return;
273    }
274
275    let mut start = 0;
276    for (index, byte) in bytes.iter().copied().enumerate() {
277        if !needs_json_escape(byte) {
278            continue;
279        }
280
281        if start < index {
282            out.extend_from_slice(&bytes[start..index]);
283        }
284
285        match byte {
286            b'"' => out.extend_from_slice(br#"\""#),
287            b'\\' => out.extend_from_slice(br#"\\"#),
288            b'\n' => out.extend_from_slice(br#"\n"#),
289            b'\r' => out.extend_from_slice(br#"\r"#),
290            b'\t' => out.extend_from_slice(br#"\t"#),
291            0x00..=0x1f => {
292                out.extend_from_slice(br#"\u00"#);
293                let high = byte >> 4;
294                let low = byte & 0x0f;
295                out.push(hex_digit(high));
296                out.push(hex_digit(low));
297            }
298            _ => out.push(byte),
299        }
300        start = index + 1;
301    }
302    if start < bytes.len() {
303        out.extend_from_slice(&bytes[start..]);
304    }
305    out.push(b'"');
306}
307
308fn push_json_hex_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
309    out.push(b'"');
310    for byte in bytes {
311        out.push(hex_digit(byte >> 4));
312        out.push(hex_digit(byte & 0x0f));
313    }
314    out.push(b'"');
315}
316
317fn hex_digit(value: u8) -> u8 {
318    match value {
319        0..=9 => b'0' + value,
320        _ => b'a' + (value - 10),
321    }
322}
323
324fn push_bool(out: &mut Vec<u8>, value: bool) {
325    out.extend_from_slice(if value { b"true" } else { b"false" });
326}
327
328fn push_i64(out: &mut Vec<u8>, value: i64) {
329    let mut buffer = itoa::Buffer::new();
330    out.extend_from_slice(buffer.format(value).as_bytes());
331}
332
333fn push_i128(out: &mut Vec<u8>, value: i128) {
334    let mut buffer = itoa::Buffer::new();
335    out.extend_from_slice(buffer.format(value).as_bytes());
336}
337
338fn push_json_u32(out: &mut Vec<u8>, value: u32) {
339    let mut buffer = itoa::Buffer::new();
340    out.extend_from_slice(buffer.format(value).as_bytes());
341}
342
343fn push_json_u64(out: &mut Vec<u8>, value: u64) {
344    let mut buffer = itoa::Buffer::new();
345    out.extend_from_slice(buffer.format(value).as_bytes());
346}
347
348fn push_u128(out: &mut Vec<u8>, value: u128) {
349    let mut buffer = itoa::Buffer::new();
350    out.extend_from_slice(buffer.format(value).as_bytes());
351}
352
353fn push_f64(out: &mut Vec<u8>, value: f64) {
354    let mut buffer = ryu::Buffer::new();
355    out.extend_from_slice(buffer.format(value).as_bytes());
356}
357
358fn push_timestamp_unix_nanos(out: &mut Vec<u8>, timestamp: Timestamp) {
359    push_json_u64(out, timestamp.unix_seconds);
360
361    let mut nanos = timestamp.subsec_nanos;
362    let mut digits = [b'0'; 9];
363    for index in (0..9).rev() {
364        digits[index] = b'0' + (nanos % 10) as u8;
365        nanos /= 10;
366    }
367    out.extend_from_slice(&digits);
368}
369
370fn level_str(level: Level) -> &'static str {
371    match level {
372        Level::ERROR => "ERROR",
373        Level::WARN => "WARN",
374        Level::INFO => "INFO",
375        Level::DEBUG => "DEBUG",
376        Level::TRACE => "TRACE",
377    }
378}
379
380fn level_code(level: Level) -> u8 {
381    match level {
382        Level::ERROR => 1,
383        Level::WARN => 2,
384        Level::INFO => 3,
385        Level::DEBUG => 4,
386        Level::TRACE => 5,
387    }
388}
389
390fn reserve_length(out: &mut Vec<u8>) -> usize {
391    let offset = out.len();
392    out.extend_from_slice(&0u32.to_le_bytes());
393    offset
394}
395
396fn write_length(out: &mut [u8], len_offset: usize) {
397    let len = (out.len() - len_offset - 4) as u32;
398    out[len_offset..len_offset + 4].copy_from_slice(&len.to_le_bytes());
399}
400
401fn push_optional_string(out: &mut Vec<u8>, value: Option<&str>) {
402    match value {
403        Some(value) => {
404            out.push(1);
405            push_string(out, value);
406        }
407        None => out.push(0),
408    }
409}
410
411fn push_optional_u32(out: &mut Vec<u8>, value: Option<u32>) {
412    match value {
413        Some(value) => {
414            out.push(1);
415            push_u32_le(out, value);
416        }
417        None => out.push(0),
418    }
419}
420
421fn push_string(out: &mut Vec<u8>, value: &str) {
422    push_u32_le(out, value.len() as u32);
423    out.extend_from_slice(value.as_bytes());
424}
425
426fn push_fields(out: &mut Vec<u8>, fields: &[OwnedField]) {
427    push_u16_le(out, fields.len() as u16);
428    for field in fields {
429        push_string(out, field.name);
430        push_field_binary(out, &field.value);
431    }
432}
433
434fn push_span_binary(out: &mut Vec<u8>, span: &SpanSnapshot) {
435    push_u64_le(out, span.id);
436    push_u32_le(out, span.metadata_id);
437    out.push(level_code(span.level));
438    push_string(out, span.name);
439    push_string(out, span.target);
440    push_fields(out, &span.fields);
441}
442
443fn push_field_binary(out: &mut Vec<u8>, value: &FieldValue) {
444    match value {
445        FieldValue::Bool(false) => out.push(1),
446        FieldValue::Bool(true) => out.push(2),
447        FieldValue::I64(value) => {
448            out.push(3);
449            out.extend_from_slice(&value.to_le_bytes());
450        }
451        FieldValue::U64(value) => {
452            out.push(4);
453            out.extend_from_slice(&value.to_le_bytes());
454        }
455        FieldValue::I128(value) => {
456            out.push(5);
457            out.extend_from_slice(&value.to_le_bytes());
458        }
459        FieldValue::U128(value) => {
460            out.push(6);
461            out.extend_from_slice(&value.to_le_bytes());
462        }
463        FieldValue::F64(value) => {
464            out.push(7);
465            out.extend_from_slice(&value.to_le_bytes());
466        }
467        FieldValue::Str(value) => {
468            out.push(8);
469            push_string(out, value.as_str());
470        }
471        FieldValue::Debug(value) => {
472            out.push(9);
473            push_string(out, value.as_str());
474        }
475        FieldValue::Bytes(value) => {
476            out.push(10);
477            push_u32_le(out, value.len() as u32);
478            out.extend_from_slice(value);
479        }
480    }
481}
482
483fn push_u16_le(out: &mut Vec<u8>, value: u16) {
484    out.extend_from_slice(&value.to_le_bytes());
485}
486
487fn push_u32_le(out: &mut Vec<u8>, value: u32) {
488    out.extend_from_slice(&value.to_le_bytes());
489}
490
491fn push_u64_le(out: &mut Vec<u8>, value: u64) {
492    out.extend_from_slice(&value.to_le_bytes());
493}
494
495fn estimate_ndjson_record_len(config: EncodeConfig, record: &OwnedRecord) -> usize {
496    let mut estimate = 96 + record.target.len() + record.name.len();
497    estimate += estimate_fields_len(&record.fields);
498
499    if config.include_current_span
500        && let Some(span) = &record.current_span
501    {
502        estimate += 64 + span.target.len() + span.name.len();
503        estimate += estimate_fields_len(&span.fields);
504    }
505
506    if config.include_span_list {
507        for span in &record.spans {
508            estimate += 64 + span.target.len() + span.name.len();
509            estimate += estimate_fields_len(&span.fields);
510        }
511    }
512
513    estimate
514}
515
516fn estimate_fields_len(fields: &[OwnedField]) -> usize {
517    fields
518        .iter()
519        .map(|field| field.name.len() + estimate_field_value_len(&field.value) + 8)
520        .sum()
521}
522
523fn estimate_field_value_len(value: &FieldValue) -> usize {
524    match value {
525        FieldValue::Bool(_) => 5,
526        FieldValue::I64(_) | FieldValue::U64(_) => 24,
527        FieldValue::I128(_) | FieldValue::U128(_) => 40,
528        FieldValue::F64(_) => 32,
529        FieldValue::Str(value) | FieldValue::Debug(value) => value.as_str().len() + 2,
530        FieldValue::Bytes(value) => value.len() * 2 + 2,
531    }
532}
533
534fn needs_json_escape(byte: u8) -> bool {
535    matches!(byte, b'"' | b'\\' | b'\n' | b'\r' | b'\t' | 0x00..=0x1f)
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use crate::record::{FieldValue, OwnedField, OwnedRecord, Timestamp};
542    use alloc::string::String;
543    use smallvec::smallvec;
544
545    #[test]
546    fn ndjson_escapes_control_characters() {
547        let record = OwnedRecord {
548            timestamp: Timestamp::new(1, 2),
549            metadata_id: 7,
550            name: "event",
551            target: "app",
552            level: Level::INFO,
553            fields: smallvec![OwnedField {
554                name: "message",
555                value: FieldValue::Str("line\nbreak".into()),
556            }],
557            current_span: None,
558            spans: SmallVec::new(),
559        };
560
561        let mut out = Vec::new();
562        encode_ndjson_record(EncodeConfig::default(), &record, &mut out);
563
564        let rendered = String::from_utf8(out).unwrap();
565        assert!(rendered.contains(r#""message":"line\nbreak""#));
566        assert!(rendered.ends_with('\n'));
567    }
568
569    #[test]
570    fn binary_frame_prefixes_length() {
571        let mut fields = SmallVec::new();
572        fields.push("message");
573        let metadata = CallsiteMetadata {
574            id: 1,
575            name: "event",
576            target: "app",
577            level: Level::INFO,
578            file: None,
579            line: None,
580            module_path: None,
581            fields,
582            kind: CallsiteKind::Event,
583        };
584
585        let mut out = Vec::new();
586        encode_binary_metadata(&metadata, &mut out);
587        assert_eq!(out[0], BinaryFrameKind::Metadata as u8);
588        let length = u32::from_le_bytes([out[1], out[2], out[3], out[4]]) as usize;
589        assert_eq!(length + 5, out.len());
590    }
591}