diagnostics_log_encoding/
lib.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
3
4//! This crate provides an implementation of Fuchsia Diagnostic Streams, often referred to as
5//! "logs."
6
7#![warn(missing_docs)]
8
9use bitfield::bitfield;
10use std::borrow::{Borrow, Cow};
11use zerocopy::{FromBytes, IntoBytes, KnownLayout};
12
13mod constants;
14pub mod encode;
15pub mod parse;
16
17pub use constants::*;
18
19/// A raw severity.
20pub type RawSeverity = u8;
21
22/// A log record.
23#[derive(Clone, Debug, PartialEq)]
24pub struct Record<'a> {
25    /// Time at which the log was emitted.
26    pub timestamp: zx::BootInstant,
27    /// The severity of the log.
28    pub severity: RawSeverity,
29    /// Arguments associated with the log.
30    pub arguments: Vec<Argument<'a>>,
31}
32
33impl Record<'_> {
34    /// Consumes the current value and returns one in the static lifetime.
35    pub fn into_owned(self) -> Record<'static> {
36        Record {
37            timestamp: self.timestamp,
38            severity: self.severity,
39            arguments: self.arguments.into_iter().map(|arg| arg.into_owned()).collect(),
40        }
41    }
42}
43
44/// An argument of the log record identified by a name and with an associated value.
45#[derive(Clone, Debug, PartialEq)]
46pub enum Argument<'a> {
47    /// Process ID
48    Pid(zx::Koid),
49    /// Thread ID
50    Tid(zx::Koid),
51    /// A log tag
52    Tag(Cow<'a, str>),
53    /// Number of dropped logs
54    Dropped(u64),
55    /// A filename
56    File(Cow<'a, str>),
57    /// A log message
58    Message(Cow<'a, str>),
59    /// A line number in a file
60    Line(u64),
61    /// A custom argument with a given name and value
62    Other {
63        /// The name of the argument.
64        name: Cow<'a, str>,
65        /// The value of the argument.
66        value: Value<'a>,
67    },
68}
69
70impl<'a> Argument<'a> {
71    /// Creates a new argument given its name and a value.
72    pub fn new(name: impl Into<Cow<'a, str>>, value: impl Into<Value<'a>>) -> Self {
73        let name: Cow<'a, str> = name.into();
74        match (name.as_ref(), value.into()) {
75            (constants::PID, Value::UnsignedInt(pid)) => Self::pid(zx::Koid::from_raw(pid)),
76            (constants::TID, Value::UnsignedInt(pid)) => Self::tid(zx::Koid::from_raw(pid)),
77            (constants::TAG, Value::Text(tag)) => Self::tag(tag),
78            (constants::NUM_DROPPED, Value::UnsignedInt(dropped)) => Self::dropped(dropped),
79            (constants::FILE, Value::Text(file)) => Self::file(file),
80            (constants::LINE, Value::UnsignedInt(line)) => Self::line(line),
81            (constants::MESSAGE, Value::Text(msg)) => Self::message(msg),
82            (_, value) => Self::other(name, value),
83        }
84    }
85
86    #[inline]
87    /// Creates a new argument for a process id.
88    pub fn pid(koid: zx::Koid) -> Self {
89        Argument::Pid(koid)
90    }
91
92    #[inline]
93    /// Creates a new argument for a thread id.
94    pub fn tid(koid: zx::Koid) -> Self {
95        Argument::Tid(koid)
96    }
97
98    #[inline]
99    /// Creates a new argument for a log message.
100    pub fn message(message: impl Into<Cow<'a, str>>) -> Self {
101        Argument::Message(message.into())
102    }
103
104    #[inline]
105    /// Creates a new argument for a tag.
106    pub fn tag(value: impl Into<Cow<'a, str>>) -> Self {
107        Argument::Tag(value.into())
108    }
109
110    #[inline]
111    /// Creates a new argument for the number of dropped logs.
112    pub fn dropped(value: u64) -> Self {
113        Argument::Dropped(value)
114    }
115
116    #[inline]
117    /// Creates a new argument for a file.
118    pub fn file(value: impl Into<Cow<'a, str>>) -> Self {
119        Argument::File(value.into())
120    }
121
122    #[inline]
123    /// Creates a new argument for a line number.
124    pub fn line(value: u64) -> Self {
125        Argument::Line(value)
126    }
127
128    #[inline]
129    /// Creates a new key-value argument.
130    pub fn other(name: impl Into<Cow<'a, str>>, value: impl Into<Value<'a>>) -> Self {
131        Argument::Other { name: name.into(), value: value.into() }
132    }
133
134    /// Consumes the current value and returns one in the static lifetime.
135    pub fn into_owned(self) -> Argument<'static> {
136        match self {
137            Self::Pid(pid) => Argument::Pid(pid),
138            Self::Tid(tid) => Argument::Tid(tid),
139            Self::Tag(tag) => Argument::Tag(Cow::Owned(tag.into_owned())),
140            Self::Dropped(dropped) => Argument::Dropped(dropped),
141            Self::File(file) => Argument::File(Cow::Owned(file.into_owned())),
142            Self::Line(line) => Argument::Line(line),
143            Self::Message(msg) => Argument::Message(Cow::Owned(msg.into_owned())),
144            Self::Other { name, value } => {
145                Argument::Other { name: Cow::Owned(name.into_owned()), value: value.into_owned() }
146            }
147        }
148    }
149
150    /// Returns the name of the argument.
151    pub fn name(&self) -> &str {
152        match self {
153            Self::Pid(_) => constants::PID,
154            Self::Tid(_) => constants::TID,
155            Self::Tag(_) => constants::TAG,
156            Self::Dropped(_) => constants::NUM_DROPPED,
157            Self::File(_) => constants::FILE,
158            Self::Line(_) => constants::LINE,
159            Self::Message(_) => constants::MESSAGE,
160            Self::Other { name, .. } => name.borrow(),
161        }
162    }
163
164    /// Returns the value of the argument.
165    pub fn value(&'a self) -> Value<'a> {
166        match self {
167            Self::Pid(pid) => Value::UnsignedInt(pid.raw_koid()),
168            Self::Tid(tid) => Value::UnsignedInt(tid.raw_koid()),
169            Self::Tag(tag) => Value::Text(Cow::Borrowed(tag.as_ref())),
170            Self::Dropped(num_dropped) => Value::UnsignedInt(*num_dropped),
171            Self::File(file) => Value::Text(Cow::Borrowed(file.as_ref())),
172            Self::Message(msg) => Value::Text(Cow::Borrowed(msg.as_ref())),
173            Self::Line(line) => Value::UnsignedInt(*line),
174            Self::Other { value, .. } => value.clone_borrowed(),
175        }
176    }
177}
178
179/// The value of a logging argument.
180#[derive(Clone, Debug, PartialEq)]
181pub enum Value<'a> {
182    /// A signed integer value for a logging argument.
183    SignedInt(i64),
184    /// An unsigned integer value for a logging argument.
185    UnsignedInt(u64),
186    /// A floating point value for a logging argument.
187    Floating(f64),
188    /// A boolean value for a logging argument.
189    Boolean(bool),
190    /// A string value for a logging argument.
191    Text(Cow<'a, str>),
192}
193
194impl<'a> Value<'a> {
195    fn into_owned(self) -> Value<'static> {
196        match self {
197            Self::Text(s) => Value::Text(Cow::Owned(s.into_owned())),
198            Self::SignedInt(n) => Value::SignedInt(n),
199            Self::UnsignedInt(n) => Value::UnsignedInt(n),
200            Self::Floating(n) => Value::Floating(n),
201            Self::Boolean(n) => Value::Boolean(n),
202        }
203    }
204
205    fn clone_borrowed(&'a self) -> Value<'a> {
206        match self {
207            Self::Text(s) => Self::Text(Cow::Borrowed(s.as_ref())),
208            Self::SignedInt(n) => Self::SignedInt(*n),
209            Self::UnsignedInt(n) => Self::UnsignedInt(*n),
210            Self::Floating(n) => Self::Floating(*n),
211            Self::Boolean(n) => Self::Boolean(*n),
212        }
213    }
214}
215
216impl From<i32> for Value<'_> {
217    fn from(number: i32) -> Value<'static> {
218        Value::SignedInt(number as i64)
219    }
220}
221
222impl From<i64> for Value<'_> {
223    fn from(number: i64) -> Value<'static> {
224        Value::SignedInt(number)
225    }
226}
227
228impl From<u64> for Value<'_> {
229    fn from(number: u64) -> Value<'static> {
230        Value::UnsignedInt(number)
231    }
232}
233
234impl From<u32> for Value<'_> {
235    fn from(number: u32) -> Value<'static> {
236        Value::UnsignedInt(number as u64)
237    }
238}
239
240impl From<zx::Koid> for Value<'_> {
241    fn from(koid: zx::Koid) -> Value<'static> {
242        Value::UnsignedInt(koid.raw_koid())
243    }
244}
245
246impl From<f64> for Value<'_> {
247    fn from(number: f64) -> Value<'static> {
248        Value::Floating(number)
249    }
250}
251
252impl<'a> From<&'a str> for Value<'a> {
253    fn from(text: &'a str) -> Value<'a> {
254        Value::Text(Cow::Borrowed(text))
255    }
256}
257
258impl From<String> for Value<'static> {
259    fn from(text: String) -> Value<'static> {
260        Value::Text(Cow::Owned(text))
261    }
262}
263
264impl<'a> From<Cow<'a, str>> for Value<'a> {
265    fn from(text: Cow<'a, str>) -> Value<'a> {
266        Value::Text(text)
267    }
268}
269
270impl From<bool> for Value<'static> {
271    fn from(boolean: bool) -> Value<'static> {
272        Value::Boolean(boolean)
273    }
274}
275
276bitfield! {
277    /// A header in the tracing format. Expected to precede every Record and Argument.
278    ///
279    /// The tracing format specifies [Record headers] and [Argument headers] as distinct types, but
280    /// their layouts are the same in practice, so we represent both bitfields using the same
281    /// struct.
282    ///
283    /// [Record headers]: https://fuchsia.dev/fuchsia-src/development/tracing/trace-format#record_header
284    /// [Argument headers]: https://fuchsia.dev/fuchsia-src/development/tracing/trace-format#argument_header
285    #[derive(IntoBytes, FromBytes, KnownLayout)]
286    pub struct Header(u64);
287    impl Debug;
288
289    /// Record type.
290    pub u8, raw_type, set_type: 3, 0;
291
292    /// Record size as a multiple of 8 bytes.
293    pub u16, size_words, set_size_words: 15, 4;
294
295    /// String ref for the associated name, if any.
296    u16, name_ref, set_name_ref: 31, 16;
297
298    /// Boolean value, if any.
299    bool, bool_val, set_bool_val: 32;
300
301    /// Reserved for record-type-specific data.
302    u16, value_ref, set_value_ref: 47, 32;
303
304    /// Severity of the record, if any.
305    pub u8, severity, set_severity: 63, 56;
306}
307
308impl Header {
309    /// Sets the length of the item the header refers to. Panics if not 8-byte aligned.
310    pub fn set_len(&mut self, new_len: usize) {
311        assert_eq!(new_len % 8, 0, "encoded message must be 8-byte aligned");
312        self.set_size_words((new_len / 8) as u16 + u16::from(new_len % 8 > 0))
313    }
314}
315
316/// Tag derived from metadata.
317///
318/// Unlike tags, metatags are not represented as strings and instead must be resolved from event
319/// metadata. This means that they may resolve to different text for different events.
320#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
321pub enum Metatag {
322    /// The location of a span or event.
323    ///
324    /// The target is typically a module path, but this can be configured by a particular span or
325    /// event when it is constructed.
326    Target,
327}
328
329/// These literal values are specified by the tracing format:
330///
331/// https://fuchsia.dev/fuchsia-src/development/tracing/trace-format#argument_header
332#[repr(u8)]
333enum ArgType {
334    Null = 0,
335    I32 = 1,
336    U32 = 2,
337    I64 = 3,
338    U64 = 4,
339    F64 = 5,
340    String = 6,
341    Pointer = 7,
342    Koid = 8,
343    Bool = 9,
344}
345
346impl TryFrom<u8> for ArgType {
347    type Error = parse::ParseError;
348    fn try_from(b: u8) -> Result<Self, Self::Error> {
349        Ok(match b {
350            0 => ArgType::Null,
351            1 => ArgType::I32,
352            2 => ArgType::U32,
353            3 => ArgType::I64,
354            4 => ArgType::U64,
355            5 => ArgType::F64,
356            6 => ArgType::String,
357            7 => ArgType::Pointer,
358            8 => ArgType::Koid,
359            9 => ArgType::Bool,
360            _ => return Err(parse::ParseError::ValueOutOfValidRange),
361        })
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use crate::encode::{Encoder, EncoderOpts, EncodingError, MutableBuffer};
369    use fidl_fuchsia_diagnostics::Severity;
370    use std::fmt::Debug;
371    use std::io::Cursor;
372
373    fn parse_argument(bytes: &[u8]) -> (&[u8], Argument<'static>) {
374        let (decoded_from_full, remaining) = crate::parse::parse_argument(bytes).unwrap();
375        (remaining, decoded_from_full.into_owned())
376    }
377
378    fn parse_record(bytes: &[u8]) -> (&[u8], Record<'static>) {
379        let (decoded_from_full, remaining) = crate::parse::parse_record(bytes).unwrap();
380        (remaining, decoded_from_full.into_owned())
381    }
382
383    const BUF_LEN: usize = 1024;
384
385    pub(crate) fn assert_roundtrips<T, F>(
386        val: T,
387        encoder_method: impl Fn(&mut Encoder<Cursor<Vec<u8>>>, &T) -> Result<(), EncodingError>,
388        parser: F,
389        canonical: Option<&[u8]>,
390    ) where
391        T: Debug + PartialEq,
392        F: Fn(&[u8]) -> (&[u8], T),
393    {
394        let mut encoder = Encoder::new(Cursor::new(vec![0; BUF_LEN]), EncoderOpts::default());
395        encoder_method(&mut encoder, &val).unwrap();
396
397        // next we'll parse the record out of a buf with padding after the record
398        let (_, decoded_from_full) = parser(encoder.buf.get_ref());
399        assert_eq!(val, decoded_from_full, "decoded version with trailing padding must match");
400
401        if let Some(canonical) = canonical {
402            let recorded = encoder.buf.get_ref().split_at(canonical.len()).0;
403            assert_eq!(canonical, recorded, "encoded repr must match the canonical value provided");
404
405            let (zero_buf, decoded) = parser(recorded);
406            assert_eq!(val, decoded, "decoded version must match what we tried to encode");
407            assert_eq!(zero_buf.len(), 0, "must parse record exactly out of provided buffer");
408        }
409    }
410
411    /// Bit pattern for the log record type, severity info, and a record of two words: one header,
412    /// one timestamp.
413    const MINIMAL_LOG_HEADER: u64 = 0x3000000000000029;
414
415    #[fuchsia::test]
416    fn minimal_header() {
417        let mut poked = Header(0);
418        poked.set_type(TRACING_FORMAT_LOG_RECORD_TYPE);
419        poked.set_size_words(2);
420        poked.set_severity(Severity::Info.into_primitive());
421
422        assert_eq!(
423            poked.0, MINIMAL_LOG_HEADER,
424            "minimal log header should only describe type, size, and severity"
425        );
426    }
427
428    #[fuchsia::test]
429    fn no_args_roundtrip() {
430        let mut expected_record = MINIMAL_LOG_HEADER.to_le_bytes().to_vec();
431        let timestamp = zx::BootInstant::from_nanos(5_000_000i64);
432        expected_record.extend(timestamp.into_nanos().to_le_bytes());
433
434        assert_roundtrips(
435            Record { timestamp, severity: Severity::Info.into_primitive(), arguments: vec![] },
436            |encoder, val| encoder.write_record(val),
437            parse_record,
438            Some(&expected_record),
439        );
440    }
441
442    #[fuchsia::test]
443    fn signed_arg_roundtrip() {
444        assert_roundtrips(
445            Argument::other("signed", -1999),
446            |encoder, val| encoder.write_argument(val),
447            parse_argument,
448            None,
449        );
450    }
451
452    #[fuchsia::test]
453    fn unsigned_arg_roundtrip() {
454        assert_roundtrips(
455            Argument::other("unsigned", 42),
456            |encoder, val| encoder.write_argument(val),
457            parse_argument,
458            None,
459        );
460    }
461
462    #[fuchsia::test]
463    fn text_arg_roundtrip() {
464        assert_roundtrips(
465            Argument::other("stringarg", "owo"),
466            |encoder, val| encoder.write_argument(val),
467            parse_argument,
468            None,
469        );
470    }
471
472    #[fuchsia::test]
473    fn float_arg_roundtrip() {
474        assert_roundtrips(
475            Argument::other("float", 3.25),
476            |encoder, val| encoder.write_argument(val),
477            parse_argument,
478            None,
479        );
480    }
481
482    #[fuchsia::test]
483    fn bool_arg_roundtrip() {
484        assert_roundtrips(
485            Argument::other("bool", false),
486            |encoder, val| encoder.write_argument(val),
487            parse_argument,
488            None,
489        );
490    }
491
492    #[fuchsia::test]
493    fn arg_of_each_type_roundtrips() {
494        assert_roundtrips(
495            Record {
496                timestamp: zx::BootInstant::get(),
497                severity: Severity::Warn.into_primitive(),
498                arguments: vec![
499                    Argument::other("signed", -10),
500                    Argument::other("unsigned", 7),
501                    Argument::other("float", 3.25),
502                    Argument::other("bool", true),
503                    Argument::other("msg", "test message one"),
504                ],
505            },
506            |encoder, val| encoder.write_record(val),
507            parse_record,
508            None,
509        );
510    }
511
512    #[fuchsia::test]
513    fn multiple_string_args() {
514        assert_roundtrips(
515            Record {
516                timestamp: zx::BootInstant::get(),
517                severity: Severity::Trace.into_primitive(),
518                arguments: vec![
519                    Argument::other("msg", "test message one"),
520                    Argument::other("msg", "test message two"),
521                    Argument::other("msg", "test message three"),
522                ],
523            },
524            |encoder, val| encoder.write_record(val),
525            parse_record,
526            None,
527        );
528    }
529
530    #[fuchsia::test]
531    fn invalid_records() {
532        // invalid word size
533        let mut encoder = Encoder::new(Cursor::new(vec![0; BUF_LEN]), EncoderOpts::default());
534        let mut header = Header(0);
535        header.set_type(TRACING_FORMAT_LOG_RECORD_TYPE);
536        header.set_size_words(0); // invalid, should be at least 2 as header and time are included
537        encoder.buf.put_u64_le(header.0).unwrap();
538        encoder.buf.put_i64_le(zx::BootInstant::get().into_nanos()).unwrap();
539        encoder.write_argument(Argument::other("msg", "test message one")).unwrap();
540        assert!(crate::parse::parse_record(encoder.buf.get_ref()).is_err());
541    }
542}