Skip to main content

diagnostics_message/
ffi.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::error::MessageError;
6use crate::{ExtendedMetadata, MessageFormatter};
7use bumpalo::Bump;
8use bumpalo::collections::{String as BumpaloString, Vec as BumpaloVec};
9use diagnostics_data::{ExtendedMoniker, Severity};
10use diagnostics_log_encoding::{Argument, Record, Value};
11use flyweights::FlyStr;
12use static_assertions::const_assert;
13use std::fmt::Write;
14use std::marker::PhantomData;
15use std::ops::Deref;
16use std::str;
17use zx::BootInstant;
18
19pub use crate::constants::*;
20
21/// Array for FFI purposes between C++ and Rust.
22/// If len is 0, ptr is allowed to be nullptr,
23/// otherwise, ptr must be valid.
24#[repr(C)]
25pub struct CppArray<'a, T> {
26    /// Number of elements in the array
27    pub len: usize,
28    /// Pointer to the first element in the array,
29    /// may be null in the case of a 0 length array,
30    /// but is not guaranteed to always be null of
31    /// len is 0.
32    pub ptr: *const T,
33
34    phantom: PhantomData<&'a T>,
35}
36
37impl<T> Deref for CppArray<'_, T> {
38    type Target = [T];
39
40    fn deref(&self) -> &Self::Target {
41        // SAFETY: The `CPPArray` is constructed from valid slices or arrays,
42        // ensuring `self.ptr` points to `self.len` elements of type `T`.
43        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
44    }
45}
46
47impl<T> Default for CppArray<'_, T> {
48    fn default() -> Self {
49        CppArray { len: 0, ptr: std::ptr::null(), phantom: PhantomData }
50    }
51}
52
53impl<'a> From<&'a str> for CppString<'a> {
54    fn from(value: &'a str) -> Self {
55        Self { inner: CppArray { len: value.len(), ptr: value.as_ptr(), phantom: PhantomData } }
56    }
57}
58
59/// Represents a UTF-8 encoded string for FFI purposes.
60/// This is equivalent to a CppArray<u8> as it is
61/// #[repr(transparent)], but with the additional
62/// constraint that the contents of the array
63/// is a valid UTF-8 string.
64#[derive(Default)]
65#[repr(transparent)]
66pub struct CppString<'a> {
67    inner: CppArray<'a, u8>,
68}
69
70impl Deref for CppString<'_> {
71    type Target = str;
72
73    fn deref(&self) -> &Self::Target {
74        // SAFETY: CppString is always constructed from valid UTF-8.
75        unsafe {
76            std::str::from_utf8_unchecked(std::slice::from_raw_parts(
77                self.inner.ptr,
78                self.inner.len,
79            ))
80        }
81    }
82}
83
84impl<'a> From<Option<&'a str>> for CppString<'a> {
85    fn from(value: Option<&'a str>) -> Self {
86        value.map(|v| v.into()).unwrap_or_default()
87    }
88}
89
90impl<'a, T> From<&'a [T]> for CppArray<'a, T> {
91    fn from(value: &'a [T]) -> Self {
92        CppArray { len: value.len(), ptr: value.as_ptr(), phantom: PhantomData }
93    }
94}
95
96impl<'a> From<BumpaloString<'a>> for CppString<'a> {
97    fn from(value: BumpaloString<'a>) -> Self {
98        value.into_bump_str().into()
99    }
100}
101
102/// Log message representation for FFI with C++
103#[repr(C)]
104pub struct LogMessage<'a> {
105    /// Severity of a log message.
106    pub severity: u8,
107    /// Tags in a log message, guaranteed to be non-null.
108    pub tags: CppArray<'a, CppString<'a>>,
109    /// Process ID from a LogMessage, or 0 if unknown
110    pub pid: u64,
111    /// Thread ID from a LogMessage, or 0 if unknown
112    pub tid: u64,
113    /// Number of dropped log messages.
114    pub dropped: u64,
115    /// The UTF-encoded log message, guaranteed to be valid UTF-8.
116    pub message: CppString<'a>,
117    /// Timestamp on the boot timeline of the log message,
118    /// in nanoseconds.
119    pub timestamp: i64,
120}
121
122// These are allocated using the Bumpalo allocator.
123const_assert!(!std::mem::needs_drop::<LogMessage<'_>>());
124
125pub struct CPPLogMessageBuilder<'a> {
126    severity: u8,
127    tags: BumpaloVec<'a, BumpaloString<'a>>,
128    pid: Option<u64>,
129    tid: Option<u64>,
130    dropped: u64,
131    file: Option<String>,
132    line: Option<u64>,
133    moniker: Option<BumpaloString<'a>>,
134    message: Option<String>,
135    timestamp: i64,
136    kvps: String,
137    allocator: &'a Bump,
138}
139
140// Escape quotes in a string per the Feedback format
141fn escape_quotes(input: &str, output: &mut String) {
142    for ch in input.chars() {
143        if ch == '"' || ch == '\\' {
144            output.push('\\');
145        }
146        output.push(ch);
147    }
148}
149
150impl<'a> CPPLogMessageBuilder<'a> {
151    fn set_raw_severity(mut self, raw_severity: u8) -> Self {
152        self.severity = raw_severity;
153        self
154    }
155
156    fn add_tag(mut self, tag: impl Into<String>) -> Self {
157        self.tags.push(BumpaloString::from_str_in(&tag.into(), self.allocator));
158        self
159    }
160
161    fn set_pid(mut self, pid: u64) -> Self {
162        self.pid = Some(pid);
163        self
164    }
165
166    fn set_tid(mut self, tid: u64) -> Self {
167        self.tid = Some(tid);
168        self
169    }
170
171    fn set_dropped(mut self, dropped: u64) -> Self {
172        self.dropped = dropped;
173        self
174    }
175
176    fn set_file(mut self, file: impl Into<String>) -> Self {
177        self.file = Some(file.into());
178        self
179    }
180
181    fn set_line(mut self, line: u64) -> Self {
182        self.line = Some(line);
183        self
184    }
185
186    fn set_message(mut self, msg: impl Into<String>) -> Self {
187        self.message = Some(msg.into());
188        self
189    }
190
191    fn add_kvp(mut self, kvp: &Argument<'_>) -> Self {
192        if !self.kvps.is_empty() {
193            self.kvps.push(' ');
194        }
195
196        self.kvps.push_str(kvp.name());
197        self.kvps.push('=');
198        match kvp.value() {
199            Value::Text(value) => {
200                self.kvps.push('"');
201                escape_quotes(&value, &mut self.kvps);
202                self.kvps.push('"');
203            }
204            Value::SignedInt(value) => {
205                write!(self.kvps, "{value}").unwrap();
206            }
207            Value::UnsignedInt(value) => {
208                write!(self.kvps, "{value}").unwrap();
209            }
210            Value::Floating(value) => {
211                write!(self.kvps, "{value}").unwrap();
212            }
213            Value::Boolean(value) => {
214                if value {
215                    write!(self.kvps, "true").unwrap();
216                } else {
217                    write!(self.kvps, "false").unwrap();
218                }
219            }
220        }
221        self
222    }
223
224    fn set_moniker(mut self, value: &str) -> Self {
225        self.moniker = Some(BumpaloString::from_str_in(value, self.allocator));
226        self
227    }
228
229    pub fn build(mut self) -> &'a mut LogMessage<'a> {
230        let allocator = self.allocator;
231
232        // Format the message in accordance with the Feedback format
233        let msg_str = self
234            .message
235            .as_ref()
236            .map(|value| bumpalo::format!(in &allocator,"{value}",))
237            .unwrap_or_else(|| BumpaloString::new_in(allocator));
238
239        let mut output = match (&self.file, &self.line) {
240            (Some(file), Some(line)) => {
241                let mut value = bumpalo::format!(in &allocator, "[{file}({line})]",);
242                if !msg_str.is_empty() {
243                    value.push(' ');
244                }
245                value
246            }
247            _ => BumpaloString::new_in(allocator),
248        };
249
250        output.push_str(&msg_str);
251        if !msg_str.is_empty() && !self.kvps.is_empty() {
252            output.push(' ');
253        }
254        output.push_str(&self.kvps);
255
256        if let Some(moniker) = &self.moniker {
257            let component_name = moniker.split("/").last();
258            if let Some(component_name) = component_name
259                && !self.tags.iter().any(|value| value.as_str() == component_name)
260            {
261                self.tags.insert(0, bumpalo::format!(in &allocator, "{}", component_name));
262            }
263        }
264
265        let tags: &[_] = self
266            .allocator
267            .alloc_slice_fill_iter(self.tags.drain(..).map(|s| s.into_bump_str().into()));
268
269        allocator.alloc(LogMessage {
270            severity: self.severity,
271            dropped: self.dropped,
272            tags: tags.into(),
273            pid: self.pid.unwrap_or(0),
274            tid: self.tid.unwrap_or(0),
275            message: output.into_bump_str().into(),
276            timestamp: self.timestamp,
277        })
278    }
279}
280
281struct CPPLogMessageBuilderBuilder<'a>(&'a Bump);
282
283impl<'a> CPPLogMessageBuilderBuilder<'a> {
284    fn configure(
285        self,
286        _component_url: Option<FlyStr>,
287        moniker: Option<ExtendedMoniker>,
288        severity: Severity,
289        timestamp: BootInstant,
290    ) -> Result<CPPLogMessageBuilder<'a>, MessageError> {
291        Ok(CPPLogMessageBuilder {
292            severity: severity as u8,
293            tags: BumpaloVec::new_in(self.0),
294            pid: None,
295            tid: None,
296            dropped: 0,
297            file: None,
298            timestamp: timestamp.into_nanos(),
299            line: None,
300            allocator: self.0,
301            kvps: String::new(),
302            moniker: moniker.map(|value| bumpalo::format!(in self.0,"{}", value)),
303            message: None,
304        })
305    }
306}
307
308pub fn build_logs_data<'a>(
309    input: &Record<'_>,
310    source: Option<ExtendedMetadata>,
311    allocator: &'a Bump,
312) -> Result<&'a mut LogMessage<'a>, MessageError> {
313    let builder = CPPLogMessageBuilderBuilder(allocator);
314    let (raw_severity, severity) = Severity::parse_exact(input.severity);
315    let (maybe_moniker, maybe_url, _) = source
316        .map(|value| (Some(value.moniker), Some(value.url), Some(value.rolled_out_logs)))
317        .unwrap_or((None, None, None));
318    let mut builder =
319        builder.configure(maybe_url.map(FlyStr::new), None, severity, input.timestamp)?;
320    if let Some(moniker) = maybe_moniker {
321        builder = builder.set_moniker(moniker.as_ref());
322    }
323    if let Some(raw_severity) = raw_severity {
324        builder = builder.set_raw_severity(raw_severity);
325    }
326
327    for argument in input.arguments.iter() {
328        match argument {
329            Argument::Tag(tag) => {
330                builder = builder.add_tag(tag.as_ref());
331            }
332            Argument::Pid(pid) => {
333                builder = builder.set_pid(pid.raw_koid());
334            }
335            Argument::Tid(tid) => {
336                builder = builder.set_tid(tid.raw_koid());
337            }
338            Argument::Dropped(dropped) => {
339                builder = builder.set_dropped(*dropped);
340            }
341            Argument::File(file) => {
342                builder = builder.set_file(file.as_ref());
343            }
344            Argument::Line(line) => {
345                builder = builder.set_line(*line);
346            }
347            Argument::Message(msg) => {
348                builder = builder.set_message(msg.as_ref());
349            }
350            Argument::Other { value: _, name: _ } => builder = builder.add_kvp(argument),
351        }
352    }
353
354    Ok(builder.build())
355}
356
357/// Constructs a `CPPLogsMessage` from the provided bytes, assuming the bytes
358/// are in the format specified as in the [log encoding], and come from
359///
360/// an Archivist LogStream with moniker, URL, and dropped logs output enabled.
361/// [log encoding] https://fuchsia.dev/fuchsia-src/development/logs/encodings
362pub fn ffi_from_extended_record<'a, 'b>(
363    bytes: &'a [u8],
364    allocator: &'b Bump,
365) -> Result<(&'b mut LogMessage<'b>, &'a [u8]), MessageError> {
366    let (input, remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
367    let (source, new_remaining) = if remaining.len() >= 16 {
368        let moniker_len = u32::from_le_bytes(remaining[0..4].try_into().unwrap()) as usize;
369        let component_url_len = u32::from_le_bytes(remaining[4..8].try_into().unwrap()) as usize;
370        let rolled_out_logs = u64::from_le_bytes(remaining[8..16].try_into().unwrap());
371        let mut offset: usize = 16;
372
373        // NOTE: This addition is safe as all platforms Fuchsia supports are 64-bit,
374        // so usize will never overflow.
375        let moniker_padded_len = (moniker_len + 7) & !7;
376        let component_url_padded_len = (component_url_len + 7) & !7;
377        let moniker_padded_end = offset + moniker_padded_len;
378        let url_padded_end = moniker_padded_end + component_url_padded_len;
379        if url_padded_end > remaining.len() {
380            return Err(MessageError::OutOfBounds);
381        }
382
383        let moniker = str::from_utf8(&remaining[offset..offset + moniker_len])?;
384        offset += moniker_padded_len;
385        let url = str::from_utf8(&remaining[offset..offset + component_url_len])?;
386        offset += component_url_padded_len;
387        (
388            Some(ExtendedMetadata {
389                moniker: ExtendedMoniker::parse_str(moniker)?,
390                url: url.into(),
391                rolled_out_logs,
392            }),
393            &remaining[offset..],
394        )
395    } else {
396        (None, remaining)
397    };
398    let record = build_logs_data(&input, source, allocator)?;
399    Ok((record, new_remaining))
400}
401
402pub struct CPPMessageFormatter<'a>(pub &'a Bump);
403impl<'a> MessageFormatter for &CPPMessageFormatter<'a> {
404    type Result = &'a mut LogMessage<'a>;
405
406    fn format(
407        &mut self,
408        record: &Record<'_>,
409        metadata: Option<ExtendedMetadata>,
410    ) -> Result<Self::Result, MessageError> {
411        build_logs_data(record, metadata, self.0)
412    }
413}
414
415#[cfg(test)]
416mod test {
417    use super::*;
418    use crate::MessageParser;
419    use bumpalo::Bump;
420    use diagnostics_log_encoding::encode::{Encoder, EncoderOpts};
421    use diagnostics_log_encoding::{Argument, Header, LOG_CONTROL_BIT, Record};
422    use std::io::Cursor;
423    use zx::BootInstant;
424
425    fn overwrite_header_tag(bytes: &mut [u8], tag: u32) {
426        if bytes.len() >= 8 {
427            let mut header = Header(u64::from_le_bytes(bytes[0..8].try_into().unwrap()));
428            header.set_tag(tag);
429            bytes[0..8].copy_from_slice(&header.0.to_le_bytes());
430        }
431    }
432
433    #[fuchsia::test]
434    fn test_short_read() {
435        let mut parser = MessageParser::default();
436        let allocator = Bump::new();
437        let formatter = CPPMessageFormatter(&allocator);
438        let bytes = vec![0u8; 7];
439        let res = parser.parse_next(&bytes, &formatter);
440        assert!(matches!(res, Err(MessageError::ShortRead { len: 7 })));
441    }
442
443    #[fuchsia::test]
444    fn test_normal_parsing() {
445        let mut parser = MessageParser::default();
446        let allocator = Bump::new();
447        let formatter = CPPMessageFormatter(&allocator);
448
449        let record = Record {
450            timestamp: BootInstant::from_nanos(72),
451            severity: 0x30,
452            arguments: vec![Argument::message("hello world")],
453        };
454        let mut buffer = Cursor::new(vec![0u8; 1024]);
455        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
456        encoder.write_record(record).unwrap();
457
458        let len = buffer.position() as usize;
459        let mut bytes = buffer.into_inner();
460        bytes.truncate(len);
461
462        let res = parser.parse_next(&bytes, &formatter).unwrap();
463        assert!(res.0.is_some());
464        let log_message = &res.0.unwrap();
465        assert_eq!(&*log_message.message, "hello world");
466        assert_eq!(log_message.timestamp, 72);
467        assert_eq!(log_message.severity, 0x30);
468    }
469
470    #[fuchsia::test]
471    fn test_escaping_in_kvp() {
472        let mut parser = MessageParser::default();
473        let allocator = Bump::new();
474        let formatter = CPPMessageFormatter(&allocator);
475
476        let record = Record {
477            timestamp: BootInstant::from_nanos(72),
478            severity: 0x30,
479            arguments: vec![
480                Argument::message("hello world"),
481                Argument::new("key", r#"val"with\escapes"#),
482            ],
483        };
484        let mut buffer = Cursor::new(vec![0u8; 1024]);
485        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
486        encoder.write_record(record).unwrap();
487
488        let len = buffer.position() as usize;
489        let mut bytes = buffer.into_inner();
490        bytes.truncate(len);
491
492        let res = parser.parse_next(&bytes, &formatter).unwrap();
493        assert!(res.0.is_some());
494        let log_message = &res.0.unwrap();
495        assert_eq!(&*log_message.message, r#"hello world key="val\"with\\escapes""#);
496    }
497
498    #[fuchsia::test]
499    fn test_out_of_bounds_extended_record() {
500        let allocator = Bump::new();
501
502        let record = Record {
503            timestamp: BootInstant::from_nanos(72),
504            severity: 0x30,
505            arguments: vec![Argument::message("hello world")],
506        };
507        let mut buffer = Cursor::new(vec![0u8; 1024]);
508        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
509        encoder.write_record(record).unwrap();
510        let len = buffer.position() as usize;
511        let mut bytes = buffer.into_inner();
512        bytes.truncate(len);
513
514        // Append a corrupt moniker_len or component_url_len in the remaining slice.
515        let extended_metadata_suffix = [
516            0xE8, 0x03, 0x00, 0x00, // moniker_len = 1000
517            0xE8, 0x03, 0x00, 0x00, // component_url_len = 1000
518            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rolled_out_logs = 0
519        ];
520        bytes.extend_from_slice(&extended_metadata_suffix);
521
522        let res = ffi_from_extended_record(&bytes, &allocator);
523        assert!(matches!(res, Err(MessageError::OutOfBounds)));
524    }
525
526    #[fuchsia::test]
527    fn test_control_message_tags() {
528        let allocator = Bump::new();
529        let formatter = CPPMessageFormatter(&allocator);
530        let mut parser = MessageParser::default();
531
532        let tag_id = 0;
533
534        let control_record = Record {
535            timestamp: BootInstant::from_nanos(72),
536            severity: 0x30,
537            arguments: vec![
538                Argument::new("moniker", "test/moniker"),
539                Argument::new("url", "fuchsia-pkg://test"),
540            ],
541        };
542
543        let mut buffer = Cursor::new(vec![0u8; 1024]);
544        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
545        encoder.write_record(control_record).unwrap();
546
547        let len = buffer.position() as usize;
548        let mut bytes = buffer.into_inner();
549        bytes.truncate(len);
550
551        overwrite_header_tag(&mut bytes, LOG_CONTROL_BIT);
552
553        let (log, _) = parser.parse_next(&bytes, &formatter).unwrap();
554        assert!(log.is_none());
555
556        let tag_data = parser.tag_map.get(&tag_id).unwrap();
557        assert_eq!(tag_data.moniker, ExtendedMoniker::parse_str("test/moniker").unwrap());
558        assert_eq!(tag_data.url, "fuchsia-pkg://test");
559
560        let rolled_out_record = Record {
561            timestamp: BootInstant::from_nanos(73),
562            severity: 0x30,
563            arguments: vec![Argument::new("rolled_out", 5u64)],
564        };
565
566        let mut buffer2 = Cursor::new(vec![0u8; 1024]);
567        let mut encoder2 = Encoder::new(&mut buffer2, EncoderOpts::default());
568        encoder2.write_record(rolled_out_record).unwrap();
569
570        let len2 = buffer2.position() as usize;
571        let mut bytes2 = buffer2.into_inner();
572        bytes2.truncate(len2);
573
574        overwrite_header_tag(&mut bytes2, LOG_CONTROL_BIT);
575
576        let (log2, _) = parser.parse_next(&bytes2, &formatter).unwrap();
577        assert!(log2.is_some());
578
579        let tag_data2 = parser.tag_map.get(&tag_id).unwrap();
580        assert_eq!(&*log2.unwrap().message, "rolled_out=5");
581        assert_eq!(tag_data2.moniker, ExtendedMoniker::parse_str("test/moniker").unwrap());
582
583        let normal_record = Record {
584            timestamp: BootInstant::from_nanos(74),
585            severity: 0x30,
586            arguments: vec![Argument::message("some log with tag")],
587        };
588
589        let mut buffer3 = Cursor::new(vec![0u8; 1024]);
590        let mut encoder3 = Encoder::new(&mut buffer3, EncoderOpts::default());
591        encoder3.write_record(normal_record).unwrap();
592
593        let len3 = buffer3.position() as usize;
594        let mut bytes3 = buffer3.into_inner();
595        bytes3.truncate(len3);
596
597        overwrite_header_tag(&mut bytes3, tag_id);
598
599        let (log3, _) = parser.parse_next(&bytes3, &formatter).unwrap();
600
601        let log_msg3 = &log3.unwrap();
602        assert_eq!(&*log_msg3.message, "some log with tag");
603        let tags = &*log_msg3.tags;
604        assert_eq!(tags.len(), 1);
605        assert_eq!(&*tags[0], "moniker");
606    }
607
608    #[fuchsia::test]
609    fn test_message_with_kvps() {
610        let mut parser = MessageParser::default();
611        let allocator = Bump::new();
612        let formatter = CPPMessageFormatter(&allocator);
613
614        let record = Record {
615            timestamp: BootInstant::from_nanos(100),
616            severity: 0x10,
617            arguments: vec![
618                Argument::message("A message"),
619                Argument::new("key1", "value1"),
620                Argument::new("key2", 123u64),
621            ],
622        };
623        let mut buffer = Cursor::new(vec![0u8; 1024]);
624        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
625        encoder.write_record(record).unwrap();
626
627        let len = buffer.position() as usize;
628        let mut bytes = buffer.into_inner();
629        bytes.truncate(len);
630
631        let res = parser.parse_next(&bytes, &formatter).unwrap();
632        assert!(res.0.is_some());
633        let log_message = res.0.unwrap();
634        assert_eq!(&*log_message.message, "A message key1=\"value1\" key2=123");
635    }
636
637    #[fuchsia::test]
638    fn test_file_line_message_with_kvps() {
639        let mut parser = MessageParser::default();
640        let allocator = Bump::new();
641        let formatter = CPPMessageFormatter(&allocator);
642
643        let record = Record {
644            timestamp: BootInstant::from_nanos(100),
645            severity: 0x10,
646            arguments: vec![
647                Argument::file("src/file.rs"),
648                Argument::line(42),
649                Argument::message("Another message"),
650                Argument::new("temp", 30.5),
651                Argument::new("valid", true),
652            ],
653        };
654        let mut buffer = Cursor::new(vec![0u8; 1024]);
655        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
656        encoder.write_record(record).unwrap();
657
658        let len = buffer.position() as usize;
659        let mut bytes = buffer.into_inner();
660        bytes.truncate(len);
661
662        let res = parser.parse_next(&bytes, &formatter).unwrap();
663        assert!(res.0.is_some());
664        let log_message = res.0.unwrap();
665        assert_eq!(&*log_message.message, "[src/file.rs(42)] Another message temp=30.5 valid=true");
666    }
667
668    #[fuchsia::test]
669    fn test_only_kvps() {
670        let mut parser = MessageParser::default();
671        let allocator = Bump::new();
672        let formatter = CPPMessageFormatter(&allocator);
673
674        let record = Record {
675            timestamp: BootInstant::from_nanos(100),
676            severity: 0x10,
677            arguments: vec![Argument::new("status", "ok"), Argument::new("code", 200i64)],
678        };
679        let mut buffer = Cursor::new(vec![0u8; 1024]);
680        let mut encoder = Encoder::new(&mut buffer, EncoderOpts::default());
681        encoder.write_record(record).unwrap();
682
683        let len = buffer.position() as usize;
684        let mut bytes = buffer.into_inner();
685        bytes.truncate(len);
686
687        let res = parser.parse_next(&bytes, &formatter).unwrap();
688        assert!(res.0.is_some());
689        let log_message = res.0.unwrap();
690        assert_eq!(&*log_message.message, "status=\"ok\" code=200");
691    }
692}