Skip to main content

diagnostics_message/
lib.rs

1// Copyright 2021 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 byteorder::{ByteOrder, LittleEndian};
7use diagnostics_data::{
8    BuilderArgs, Data, ExtendedMoniker, Logs, LogsData, LogsDataBuilder, LogsField, LogsProperty,
9    Severity,
10};
11use diagnostics_log_encoding::{
12    ARCHIVIST_URL, Argument, Header, LOG_CONTROL_BIT, MONIKER, ROLLED_OUT, Record, URL, Value,
13};
14use flyweights::FlyStr;
15use libc::{c_char, c_int};
16use moniker::Moniker;
17use std::collections::HashMap;
18use std::{mem, str};
19
20#[cfg(fuchsia_api_level_at_least = "HEAD")]
21use fidl_fuchsia_diagnostics as fdiagnostics;
22
23mod constants;
24pub mod error;
25pub mod ffi;
26pub use constants::*;
27
28#[cfg(test)]
29mod test;
30
31#[derive(Clone)]
32pub struct MonikerWithUrl {
33    pub moniker: ExtendedMoniker,
34    pub url: FlyStr,
35}
36
37/// Transforms the given legacy log message (already parsed) into a `LogsData` containing the
38/// given identity information.
39pub fn from_logger(source: MonikerWithUrl, msg: LoggerMessage) -> LogsData {
40    let (raw_severity, severity) = Severity::parse_exact(msg.raw_severity);
41    let mut builder = LogsDataBuilder::new(BuilderArgs {
42        timestamp: msg.timestamp,
43        component_url: Some(source.url),
44        moniker: source.moniker,
45        severity,
46    })
47    .set_pid(msg.pid)
48    .set_tid(msg.tid)
49    .set_dropped(msg.dropped_logs)
50    .set_message(msg.message);
51    if let Some(raw_severity) = raw_severity {
52        builder = builder.set_raw_severity(raw_severity);
53    }
54    for tag in &msg.tags {
55        builder = builder.add_tag(tag.as_ref());
56    }
57    builder.build()
58}
59
60#[derive(Clone)]
61pub struct ExtendedMetadata {
62    pub moniker: ExtendedMoniker,
63    pub url: FlyStr,
64    pub rolled_out_logs: u64,
65}
66
67#[cfg(fuchsia_api_level_less_than = "HEAD")]
68fn parse_archivist_args<'a>(
69    builder: LogsDataBuilder,
70    _input: &'a Record<'a>,
71) -> Result<(LogsDataBuilder, usize), MessageError> {
72    Ok((builder, 0))
73}
74
75#[cfg(fuchsia_api_level_at_least = "HEAD")]
76fn parse_archivist_args<'a>(
77    mut builder: LogsDataBuilder,
78    input: &'a Record<'a>,
79) -> Result<(LogsDataBuilder, usize), MessageError> {
80    let mut archivist_argument_count = 0;
81    for argument in input.arguments.iter().rev() {
82        // If Archivist records are expected, they should always be at the end.
83        // If we see a non-archivist record, we can stop looking.
84        match argument {
85            Argument::Other { name, value } => {
86                if name == fdiagnostics::COMPONENT_URL_ARG_NAME {
87                    if let Value::Text(url) = value {
88                        builder = builder.set_url(Some(FlyStr::new(url.as_ref())));
89                        archivist_argument_count += 1;
90                        continue;
91                    }
92                } else if name == fdiagnostics::MONIKER_ARG_NAME {
93                    if let Value::Text(moniker) = value {
94                        builder = builder.set_moniker(ExtendedMoniker::parse_str(moniker)?);
95                        archivist_argument_count += 1;
96                        continue;
97                    }
98                } else if name == fdiagnostics::ROLLED_OUT_ARG_NAME
99                    && let Value::UnsignedInt(count) = value
100                {
101                    builder = builder.set_rolled_out(*count);
102                    archivist_argument_count += 1;
103                    continue;
104                }
105            }
106            _ => break,
107        }
108    }
109    Ok((builder, archivist_argument_count))
110}
111
112pub fn parse_logs_data<'a>(
113    input: &'a Record<'a>,
114    source: Option<ExtendedMetadata>,
115    rolled_out: u64,
116) -> Result<LogsData, MessageError> {
117    let (raw_severity, severity) = Severity::parse_exact(input.severity);
118    let has_attribution = source.is_some();
119
120    let (maybe_moniker, maybe_url) =
121        source.map(|value| (Some(value.moniker), Some(value.url))).unwrap_or((None, None));
122
123    let mut builder = LogsDataBuilder::new(BuilderArgs {
124        component_url: maybe_url,
125        moniker: maybe_moniker.unwrap_or(ExtendedMoniker::ComponentInstance(
126            Moniker::parse_str("placeholder").unwrap(),
127        )),
128        severity,
129        timestamp: input.timestamp,
130    });
131
132    if rolled_out > 0 {
133        builder = builder.set_rolled_out(rolled_out);
134    }
135
136    if let Some(raw_severity) = raw_severity {
137        builder = builder.set_raw_severity(raw_severity);
138    }
139    let archivist_argument_count = if has_attribution {
140        0
141    } else {
142        let (new_builder, count) = parse_archivist_args(builder, input)?;
143        builder = new_builder;
144        count
145    };
146
147    for argument in input.arguments.iter().take(input.arguments.len() - archivist_argument_count) {
148        match argument {
149            Argument::Tag(tag) => {
150                builder = builder.add_tag(tag.as_ref());
151            }
152            Argument::Pid(pid) => {
153                builder = builder.set_pid(pid.raw_koid());
154            }
155            Argument::Tid(tid) => {
156                builder = builder.set_tid(tid.raw_koid());
157            }
158            Argument::Dropped(dropped) => {
159                builder = builder.set_dropped(*dropped);
160            }
161            Argument::File(file) => {
162                builder = builder.set_file(file.as_ref());
163            }
164            Argument::Line(line) => {
165                builder = builder.set_line(*line);
166            }
167            Argument::Message(msg) => {
168                builder = builder.set_message(msg.as_ref());
169            }
170            Argument::Other { value, name } => {
171                let name = LogsField::Other(name.to_string());
172                builder = builder.add_key(match value {
173                    Value::SignedInt(v) => LogsProperty::Int(name, *v),
174                    Value::UnsignedInt(v) => LogsProperty::Uint(name, *v),
175                    Value::Floating(v) => LogsProperty::Double(name, *v),
176                    Value::Text(v) => LogsProperty::String(name, v.to_string()),
177                    Value::Boolean(v) => LogsProperty::Bool(name, *v),
178                })
179            }
180        }
181    }
182
183    Ok(builder.build())
184}
185
186/// A stateful parser that reconstructs fully attributed `LogsData` records from a stream of
187/// FXT log packets.
188///
189/// # Background & Architecture
190///
191/// In the Fuchsia Trace Format (FXT) structured logging protocol, log attribution metadata is
192/// separated from the actual log payload to optimize transmission overhead. Instead of repeating
193/// the full moniker and component URL on every log record, the system transmits two distinct
194/// types of records:
195///
196/// 1. **Manifest/Control Records**: Sent with the `LOG_CONTROL_BIT` set. These records map a
197///    numeric base tag ID to component identity metadata (`ExtendedMoniker` and URL).
198/// 2. **Legacy Log Records**: Contain the message content, severity, timestamp, and
199///    arguments, along with a tag ID indicating which component produced the log.
200///
201/// # Stateful Parsing
202///
203/// `MessageParser` maintains an internal `tag_map` to track the active association between
204/// numeric tag IDs and their component identity (`ExtendedMetadata`).
205///
206/// - When parsing a manifest record (`is_control == true`), `MessageParser` updates its state
207///   mapping for the derived base tag. If the record also reports rolled out (dropped) logs, a
208///   `LogsData` payload representing those dropped logs is returned. Otherwise, it registers
209///   the attribution mapping and returns `Ok((None, remaining))`.
210/// - When parsing a legacy log record (`is_control == false`), `MessageParser` resolves the
211///   record's tag to retrieve the component's identity from the internal mapping, constructing
212///   a fully attributed `LogsData` containing the correct component moniker and URL.
213#[derive(Default)]
214pub struct MessageParser {
215    tag_map: HashMap<u32, ExtendedMetadata>,
216}
217
218pub trait MessageFormatter {
219    type Result;
220
221    fn format(
222        &mut self,
223        record: &Record<'_>,
224        metadata: Option<ExtendedMetadata>,
225    ) -> Result<Self::Result, MessageError>;
226}
227
228#[derive(Default)]
229pub struct RustMessageFormatter;
230
231impl MessageFormatter for RustMessageFormatter {
232    type Result = Data<Logs>;
233
234    fn format(
235        &mut self,
236        record: &Record<'_>,
237        metadata: Option<ExtendedMetadata>,
238    ) -> Result<Self::Result, MessageError> {
239        let rolled_out = metadata.as_ref().map(|value| value.rolled_out_logs).unwrap_or(0);
240        parse_logs_data(record, metadata, rolled_out)
241    }
242}
243
244impl MessageParser {
245    /// Parses the next log record from the given `bytes`.
246    ///
247    /// This function can handle both standard log records and "Archivist" manifest records.
248    /// Archivist records update an internal map used to attribute subsequent log records.
249    ///
250    /// # Arguments
251    ///
252    /// * `bytes`: A byte slice containing one or more log records.
253    ///
254    /// # Returns
255    ///
256    /// A `Result` containing:
257    /// * `Ok((Option<LogsData>, &[u8]))`: A tuple where the first element is `Some(LogsData)`
258    ///   if a log message was parsed, or `None` if it was an Archivist manifest record. The
259    ///   second element is the remaining slice of bytes after parsing the record.
260    /// * `Err(MessageError)`: An error if parsing failed.
261    pub fn parse_next<'a, F: MessageFormatter>(
262        &mut self,
263        bytes: &'a [u8],
264        mut formatter: F,
265    ) -> Result<(Option<F::Result>, &'a [u8]), MessageError> {
266        if bytes.len() < 8 {
267            return Err(MessageError::ShortRead { len: bytes.len() });
268        }
269        let header_bytes: [u8; 8] = bytes[0..8].try_into().unwrap();
270        let header_val = u64::from_le_bytes(header_bytes);
271        let header = Header(header_val);
272        let tag = header.tag();
273        let base_tag = tag & !LOG_CONTROL_BIT;
274        let is_control = (tag & LOG_CONTROL_BIT) != 0;
275
276        let (input, remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
277
278        if is_control {
279            let mut moniker = None;
280            let mut url = None;
281            let mut rolled_out = None;
282            for arg in &input.arguments {
283                if arg.name() == MONIKER
284                    && let Value::Text(v) = arg.value()
285                {
286                    moniker = Some(v);
287                } else if arg.name() == URL
288                    && let Value::Text(v) = arg.value()
289                {
290                    url = Some(v);
291                } else if arg.name() == ROLLED_OUT
292                    && let Value::UnsignedInt(v) = arg.value()
293                {
294                    rolled_out = Some(v);
295                }
296            }
297            if let Some(count) = rolled_out {
298                let metadata =
299                    self.tag_map.get(&base_tag).cloned().unwrap_or_else(|| ExtendedMetadata {
300                        moniker: diagnostics_data::ExtendedMoniker::ComponentInstance(
301                            moniker::Moniker::parse_str("/UNKNOWN").unwrap(),
302                        ),
303                        url: flyweights::FlyStr::new(ARCHIVIST_URL),
304                        rolled_out_logs: count,
305                    });
306                let data = formatter.format(&input, Some(metadata))?;
307                return Ok((Some(data), remaining));
308            }
309            if let (Some(m), Some(u)) = (moniker, url)
310                && let Ok(extended_moniker) = ExtendedMoniker::parse_str(&m)
311            {
312                self.tag_map.insert(
313                    base_tag,
314                    ExtendedMetadata {
315                        moniker: extended_moniker,
316                        url: FlyStr::new(u),
317                        rolled_out_logs: 0,
318                    },
319                );
320            }
321            Ok((None, remaining))
322        } else {
323            let metadata = self.tag_map.get(&base_tag).cloned();
324            let data = formatter.format(&input, metadata)?;
325            Ok((Some(data), remaining))
326        }
327    }
328}
329
330/// Constructs a `LogsData` from the provided bytes, assuming the bytes
331/// are a a single FXT log record with a potentially extended metadata section.
332/// [log encoding] https://fuchsia.dev/fuchsia-src/reference/platform-spec/diagnostics/logs-encoding
333pub fn from_extended_record(bytes: &[u8]) -> Result<(LogsData, &[u8]), MessageError> {
334    let (input, remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
335    let (source, new_remaining, rolled_out_logs) = if remaining.len() >= 16 {
336        let moniker_len = u32::from_le_bytes(remaining[0..4].try_into().unwrap()) as usize;
337        let component_url_len = u32::from_le_bytes(remaining[4..8].try_into().unwrap()) as usize;
338        let rolled_out_logs = u64::from_le_bytes(remaining[8..16].try_into().unwrap());
339        let mut offset = 16;
340        let moniker = str::from_utf8(&remaining[offset..offset + moniker_len])?;
341        let moniker_padded_len = (moniker_len + 7) & !7;
342        offset += moniker_padded_len;
343        let url = str::from_utf8(&remaining[offset..offset + component_url_len])?;
344        let component_url_padded_len = (component_url_len + 7) & !7;
345        offset += component_url_padded_len;
346        (
347            Some(ExtendedMetadata {
348                moniker: ExtendedMoniker::parse_str(moniker)?,
349                url: FlyStr::new(url),
350                rolled_out_logs: 0,
351            }),
352            &remaining[offset..],
353            rolled_out_logs,
354        )
355    } else {
356        (None, remaining, 0)
357    };
358    let record = parse_logs_data(&input, source, rolled_out_logs)?;
359    Ok((record, new_remaining))
360}
361
362/// Constructs a `LogsData` from the provided bytes, assuming the bytes
363/// are in the format specified as in the [log encoding].
364///
365/// [log encoding] https://fuchsia.dev/fuchsia-src/development/logs/encodings
366pub fn from_structured(source: MonikerWithUrl, bytes: &[u8]) -> Result<LogsData, MessageError> {
367    let (input, _remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
368    let record = parse_logs_data(
369        &input,
370        Some(ExtendedMetadata { moniker: source.moniker, url: source.url, rolled_out_logs: 0 }),
371        0,
372    )?;
373    Ok(record)
374}
375
376#[derive(Clone, Debug, Eq, PartialEq)]
377pub struct LoggerMessage {
378    pub timestamp: zx::BootInstant,
379    pub raw_severity: u8,
380    pub pid: u64,
381    pub tid: u64,
382    pub size_bytes: usize,
383    pub dropped_logs: u64,
384    pub message: Box<str>,
385    pub tags: Vec<Box<str>>,
386}
387
388/// Parse the provided buffer as if it implements the [logger/syslog wire format].
389///
390/// Note that this is distinct from the parsing we perform for the debuglog log, which also
391/// takes a `&[u8]` and is why we don't implement this as `TryFrom`.
392///
393/// [logger/syslog wire format]: https://fuchsia.googlesource.com/fuchsia/+/HEAD/zircon/system/ulib/syslog/include/lib/syslog/wire_format.h
394impl TryFrom<&[u8]> for LoggerMessage {
395    type Error = MessageError;
396
397    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
398        if bytes.len() < MIN_PACKET_SIZE {
399            return Err(MessageError::ShortRead { len: bytes.len() });
400        }
401
402        let terminator = bytes[bytes.len() - 1];
403        if terminator != 0 {
404            return Err(MessageError::NotNullTerminated { terminator });
405        }
406
407        let pid = LittleEndian::read_u64(&bytes[..8]);
408        let tid = LittleEndian::read_u64(&bytes[8..16]);
409        let timestamp = zx::BootInstant::from_nanos(LittleEndian::read_i64(&bytes[16..24]));
410
411        let raw_severity = LittleEndian::read_i32(&bytes[24..28]);
412        let raw_severity = if raw_severity > (u8::MAX as i32) {
413            u8::MAX
414        } else if raw_severity < 0 {
415            0
416        } else {
417            u8::try_from(raw_severity).unwrap()
418        };
419        let dropped_logs = LittleEndian::read_u32(&bytes[28..METADATA_SIZE]) as u64;
420
421        // start reading tags after the header
422        let mut cursor = METADATA_SIZE;
423        let mut tag_len = bytes[cursor] as usize;
424        let mut tags = Vec::new();
425        while tag_len != 0 {
426            if tags.len() == MAX_TAGS {
427                return Err(MessageError::TooManyTags);
428            }
429
430            if tag_len > MAX_TAG_LEN - 1 {
431                return Err(MessageError::TagTooLong { index: tags.len(), len: tag_len });
432            }
433
434            if (cursor + tag_len + 1) > bytes.len() {
435                return Err(MessageError::OutOfBounds);
436            }
437
438            let tag_start = cursor + 1;
439            let tag_end = tag_start + tag_len;
440            let tag = String::from_utf8_lossy(&bytes[tag_start..tag_end]);
441            tags.push(tag.into());
442
443            cursor = tag_end;
444            tag_len = bytes[cursor] as usize;
445        }
446
447        let msg_start = cursor + 1;
448        let mut msg_end = cursor + 1;
449        while msg_end < bytes.len() {
450            if bytes[msg_end] > 0 {
451                msg_end += 1;
452                continue;
453            }
454            let message = String::from_utf8_lossy(&bytes[msg_start..msg_end]).into_owned();
455            let message_len = message.len();
456            let result = LoggerMessage {
457                timestamp,
458                raw_severity,
459                message: message.into_boxed_str(),
460                pid,
461                tid,
462                dropped_logs,
463                tags,
464                size_bytes: cursor + message_len + 1,
465            };
466            return Ok(result);
467        }
468
469        Err(MessageError::OutOfBounds)
470    }
471}
472
473#[allow(non_camel_case_types)]
474pub type fx_log_severity_t = c_int;
475
476#[repr(C)]
477#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
478pub struct fx_log_metadata_t {
479    pub pid: zx::sys::zx_koid_t,
480    pub tid: zx::sys::zx_koid_t,
481    pub time: zx::sys::zx_time_t,
482    pub severity: fx_log_severity_t,
483    pub dropped_logs: u32,
484}
485
486#[repr(C)]
487#[derive(Clone)]
488pub struct fx_log_packet_t {
489    pub metadata: fx_log_metadata_t,
490    // Contains concatenated tags and message and a null terminating character at
491    // the end.
492    // char(tag_len) + "tag1" + char(tag_len) + "tag2\0msg\0"
493    pub data: [c_char; MAX_DATAGRAM_LEN - METADATA_SIZE],
494}
495
496impl Default for fx_log_packet_t {
497    fn default() -> fx_log_packet_t {
498        fx_log_packet_t {
499            data: [0; MAX_DATAGRAM_LEN - METADATA_SIZE],
500            metadata: Default::default(),
501        }
502    }
503}
504
505impl fx_log_packet_t {
506    /// This struct has no padding bytes, but we can't use zerocopy because it needs const
507    /// generics to support arrays this large.
508    pub fn as_bytes(&self) -> &[u8] {
509        unsafe {
510            std::slice::from_raw_parts(
511                (self as *const Self) as *const u8,
512                mem::size_of::<fx_log_packet_t>(),
513            )
514        }
515    }
516
517    /// Fills data with a single value for defined region.
518    pub fn fill_data(&mut self, region: std::ops::Range<usize>, with: c_char) {
519        self.data[region].iter_mut().for_each(|c| *c = with);
520    }
521
522    /// Copies bytes to data at specifies offset.
523    pub fn add_data<T: std::convert::TryInto<c_char> + Copy>(&mut self, offset: usize, bytes: &[T])
524    where
525        <T as std::convert::TryInto<c_char>>::Error: std::fmt::Debug,
526    {
527        self.data[offset..(offset + bytes.len())]
528            .iter_mut()
529            .enumerate()
530            .for_each(|(i, x)| *x = bytes[i].try_into().unwrap());
531    }
532}