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
276/// The maximum value acceptable for `size_words` below.
277pub const MAX_SIZE_WORDS: u16 = 4095;
278
279bitfield! {
280    /// A header in the tracing format. Expected to precede every Record and Argument.
281    ///
282    /// The tracing format specifies [Record headers] and [Argument headers] as distinct types, but
283    /// their layouts are the same in practice, so we represent both bitfields using the same
284    /// struct.
285    ///
286    /// [Record headers]: https://fuchsia.dev/fuchsia-src/development/tracing/trace-format#record_header
287    /// [Argument headers]: https://fuchsia.dev/fuchsia-src/development/tracing/trace-format#argument_header
288    #[derive(IntoBytes, FromBytes, KnownLayout)]
289    pub struct Header(u64);
290    impl Debug;
291
292    /// Record type.
293    pub u8, raw_type, set_type: 3, 0;
294
295    /// Record size as a multiple of 8 bytes.
296    pub u16, size_words, set_size_words: 15, 4;
297
298    /// String ref for the associated name, if any.
299    u16, name_ref, set_name_ref: 31, 16;
300
301    /// Boolean value, if any.
302    bool, bool_val, set_bool_val: 32;
303
304    /// Reserved for record-type-specific data.
305    u16, value_ref, set_value_ref: 47, 32;
306
307    /// Severity of the record, if any.
308    pub u8, severity, set_severity: 63, 56;
309}
310
311impl Header {
312    /// Sets the length of the item the header refers to. Panics if not 8-byte aligned.
313    pub fn set_len(&mut self, new_len: usize) {
314        assert_eq!(new_len % 8, 0, "encoded message must be 8-byte aligned");
315        self.set_size_words((new_len / 8) as u16 + u16::from(!new_len.is_multiple_of(8)))
316    }
317}
318
319/// Tag derived from metadata.
320///
321/// Unlike tags, metatags are not represented as strings and instead must be resolved from event
322/// metadata. This means that they may resolve to different text for different events.
323#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
324pub enum Metatag {
325    /// The location of a span or event.
326    ///
327    /// The target is typically a module path, but this can be configured by a particular span or
328    /// event when it is constructed.
329    Target,
330}
331
332/// These literal values are specified by the tracing format:
333///
334/// https://fuchsia.dev/fuchsia-src/development/tracing/trace-format#argument_header
335#[repr(u8)]
336enum ArgType {
337    Null = 0,
338    I32 = 1,
339    U32 = 2,
340    I64 = 3,
341    U64 = 4,
342    F64 = 5,
343    String = 6,
344    Pointer = 7,
345    Koid = 8,
346    Bool = 9,
347}
348
349impl TryFrom<u8> for ArgType {
350    type Error = parse::ParseError;
351    fn try_from(b: u8) -> Result<Self, Self::Error> {
352        Ok(match b {
353            0 => ArgType::Null,
354            1 => ArgType::I32,
355            2 => ArgType::U32,
356            3 => ArgType::I64,
357            4 => ArgType::U64,
358            5 => ArgType::F64,
359            6 => ArgType::String,
360            7 => ArgType::Pointer,
361            8 => ArgType::Koid,
362            9 => ArgType::Bool,
363            _ => return Err(parse::ParseError::ValueOutOfValidRange),
364        })
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use crate::encode::{Encoder, EncoderOpts, EncodingError, MutableBuffer};
372    use fidl_fuchsia_diagnostics_types::Severity;
373    use std::fmt::Debug;
374    use std::io::Cursor;
375
376    fn parse_argument(bytes: &[u8]) -> (&[u8], Argument<'static>) {
377        let (decoded_from_full, remaining) = crate::parse::parse_argument(bytes).unwrap();
378        (remaining, decoded_from_full.into_owned())
379    }
380
381    fn parse_record(bytes: &[u8]) -> (&[u8], Record<'static>) {
382        let (decoded_from_full, remaining) = crate::parse::parse_record(bytes).unwrap();
383        (remaining, decoded_from_full.into_owned())
384    }
385
386    const BUF_LEN: usize = 1024;
387
388    pub(crate) fn assert_roundtrips<T, F>(
389        val: T,
390        encoder_method: impl Fn(&mut Encoder<Cursor<Vec<u8>>>, &T) -> Result<(), EncodingError>,
391        parser: F,
392        canonical: Option<&[u8]>,
393    ) where
394        T: Debug + PartialEq,
395        F: Fn(&[u8]) -> (&[u8], T),
396    {
397        let mut encoder = Encoder::new(Cursor::new(vec![0; BUF_LEN]), EncoderOpts::default());
398        encoder_method(&mut encoder, &val).unwrap();
399
400        // next we'll parse the record out of a buf with padding after the record
401        let (_, decoded_from_full) = parser(encoder.buf.get_ref());
402        assert_eq!(val, decoded_from_full, "decoded version with trailing padding must match");
403
404        if let Some(canonical) = canonical {
405            let recorded = encoder.buf.get_ref().split_at(canonical.len()).0;
406            assert_eq!(canonical, recorded, "encoded repr must match the canonical value provided");
407
408            let (zero_buf, decoded) = parser(recorded);
409            assert_eq!(val, decoded, "decoded version must match what we tried to encode");
410            assert_eq!(zero_buf.len(), 0, "must parse record exactly out of provided buffer");
411        }
412    }
413
414    /// Bit pattern for the log record type, severity info, and a record of two words: one header,
415    /// one timestamp.
416    const MINIMAL_LOG_HEADER: u64 = 0x3000000000000029;
417
418    #[fuchsia::test]
419    fn minimal_header() {
420        let mut poked = Header(0);
421        poked.set_type(TRACING_FORMAT_LOG_RECORD_TYPE);
422        poked.set_size_words(2);
423        poked.set_severity(Severity::Info.into_primitive());
424
425        assert_eq!(
426            poked.0, MINIMAL_LOG_HEADER,
427            "minimal log header should only describe type, size, and severity"
428        );
429    }
430
431    #[fuchsia::test]
432    fn no_args_roundtrip() {
433        let mut expected_record = MINIMAL_LOG_HEADER.to_le_bytes().to_vec();
434        let timestamp = zx::BootInstant::from_nanos(5_000_000i64);
435        expected_record.extend(timestamp.into_nanos().to_le_bytes());
436
437        assert_roundtrips(
438            Record { timestamp, severity: Severity::Info.into_primitive(), arguments: vec![] },
439            |encoder, val| encoder.write_record(val),
440            parse_record,
441            Some(&expected_record),
442        );
443    }
444
445    #[fuchsia::test]
446    fn signed_arg_roundtrip() {
447        assert_roundtrips(
448            Argument::other("signed", -1999),
449            |encoder, val| encoder.write_argument(val),
450            parse_argument,
451            None,
452        );
453    }
454
455    #[fuchsia::test]
456    fn unsigned_arg_roundtrip() {
457        assert_roundtrips(
458            Argument::other("unsigned", 42),
459            |encoder, val| encoder.write_argument(val),
460            parse_argument,
461            None,
462        );
463    }
464
465    #[fuchsia::test]
466    fn text_arg_roundtrip() {
467        assert_roundtrips(
468            Argument::other("stringarg", "owo"),
469            |encoder, val| encoder.write_argument(val),
470            parse_argument,
471            None,
472        );
473    }
474
475    #[fuchsia::test]
476    fn float_arg_roundtrip() {
477        assert_roundtrips(
478            Argument::other("float", 3.25),
479            |encoder, val| encoder.write_argument(val),
480            parse_argument,
481            None,
482        );
483    }
484
485    #[fuchsia::test]
486    fn bool_arg_roundtrip() {
487        assert_roundtrips(
488            Argument::other("bool", false),
489            |encoder, val| encoder.write_argument(val),
490            parse_argument,
491            None,
492        );
493    }
494
495    #[fuchsia::test]
496    fn arg_of_each_type_roundtrips() {
497        assert_roundtrips(
498            Record {
499                timestamp: zx::BootInstant::get(),
500                severity: Severity::Warn.into_primitive(),
501                arguments: vec![
502                    Argument::other("signed", -10),
503                    Argument::other("unsigned", 7),
504                    Argument::other("float", 3.25),
505                    Argument::other("bool", true),
506                    Argument::other("msg", "test message one"),
507                ],
508            },
509            |encoder, val| encoder.write_record(val),
510            parse_record,
511            None,
512        );
513    }
514
515    #[fuchsia::test]
516    fn multiple_string_args() {
517        assert_roundtrips(
518            Record {
519                timestamp: zx::BootInstant::get(),
520                severity: Severity::Trace.into_primitive(),
521                arguments: vec![
522                    Argument::other("msg", "test message one"),
523                    Argument::other("msg", "test message two"),
524                    Argument::other("msg", "test message three"),
525                ],
526            },
527            |encoder, val| encoder.write_record(val),
528            parse_record,
529            None,
530        );
531    }
532
533    #[fuchsia::test]
534    fn invalid_records() {
535        // invalid word size
536        let mut encoder = Encoder::new(Cursor::new(vec![0; BUF_LEN]), EncoderOpts::default());
537        let mut header = Header(0);
538        header.set_type(TRACING_FORMAT_LOG_RECORD_TYPE);
539        header.set_size_words(0); // invalid, should be at least 2 as header and time are included
540        encoder.buf.put_u64_le(header.0).unwrap();
541        encoder.buf.put_i64_le(zx::BootInstant::get().into_nanos()).unwrap();
542        encoder.write_argument(Argument::other("msg", "test message one")).unwrap();
543        assert!(crate::parse::parse_record(encoder.buf.get_ref()).is_err());
544    }
545}