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, Timestamp,
10};
11use diagnostics_log_encoding::{
12    ARCHIVIST_URL, Argument, Header, LOG_CONTROL_BIT, MONIKER, ROLLED_OUT, Record, URL, Value, zx,
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: Timestamp::from_nanos(msg.timestamp.into_nanos()),
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: Timestamp::from_nanos(input.timestamp.into_nanos()),
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<'a> {
219    type Result;
220
221    fn format(
222        &mut self,
223        record: &Record<'a>,
224        metadata: Option<ExtendedMetadata>,
225    ) -> Result<Self::Result, MessageError>;
226}
227
228#[derive(Default)]
229pub struct RustMessageFormatter;
230
231impl<'a> MessageFormatter<'a> for RustMessageFormatter {
232    type Result = Data<Logs>;
233
234    fn format(
235        &mut self,
236        record: &Record<'a>,
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<'a>>(
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 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
341        // NOTE: In the extended metadata format, string fields (moniker and URL) are
342        // 8-byte word aligned with trailing zero padding.
343        // `(len + 7) & !7` rounds up `len` to the nearest multiple of 8.
344        // This arithmetic is overflow-safe on 64-bit platforms because moniker_len and
345        // component_url_len are originally 32-bit integers (u32).
346        let moniker_padded_len = (moniker_len + 7) & !7;
347        let component_url_padded_len = (component_url_len + 7) & !7;
348        let moniker_padded_end = offset + moniker_padded_len;
349        let url_padded_end = moniker_padded_end + component_url_padded_len;
350        if url_padded_end > remaining.len() {
351            return Err(MessageError::OutOfBounds);
352        }
353        let moniker = str::from_utf8(&remaining[offset..offset + moniker_len])?;
354        offset += moniker_padded_len;
355        let url = str::from_utf8(&remaining[offset..offset + component_url_len])?;
356        offset += component_url_padded_len;
357        (
358            Some(ExtendedMetadata {
359                moniker: ExtendedMoniker::parse_str(moniker)?,
360                url: FlyStr::new(url),
361                rolled_out_logs: 0,
362            }),
363            &remaining[offset..],
364            rolled_out_logs,
365        )
366    } else {
367        (None, remaining, 0)
368    };
369    let record = parse_logs_data(&input, source, rolled_out_logs)?;
370    Ok((record, new_remaining))
371}
372
373/// Constructs a `LogsData` from the provided bytes, assuming the bytes
374/// are in the format specified as in the [log encoding].
375///
376/// [log encoding] https://fuchsia.dev/fuchsia-src/development/logs/encodings
377pub fn from_structured(source: MonikerWithUrl, bytes: &[u8]) -> Result<LogsData, MessageError> {
378    let (input, _remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
379    let record = parse_logs_data(
380        &input,
381        Some(ExtendedMetadata { moniker: source.moniker, url: source.url, rolled_out_logs: 0 }),
382        0,
383    )?;
384    Ok(record)
385}
386
387#[derive(Clone, Debug, Eq, PartialEq)]
388pub struct LoggerMessage {
389    pub timestamp: zx::BootInstant,
390    pub raw_severity: u8,
391    pub pid: u64,
392    pub tid: u64,
393    pub size_bytes: usize,
394    pub dropped_logs: u64,
395    pub message: Box<str>,
396    pub tags: Vec<Box<str>>,
397}
398
399/// Parse the provided buffer as if it implements the [logger/syslog wire format].
400///
401/// Note that this is distinct from the parsing we perform for the debuglog log, which also
402/// takes a `&[u8]` and is why we don't implement this as `TryFrom`.
403///
404/// [logger/syslog wire format]: https://fuchsia.googlesource.com/fuchsia/+/HEAD/zircon/system/ulib/syslog/include/lib/syslog/wire_format.h
405impl TryFrom<&[u8]> for LoggerMessage {
406    type Error = MessageError;
407
408    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
409        if bytes.len() < MIN_PACKET_SIZE {
410            return Err(MessageError::ShortRead { len: bytes.len() });
411        }
412
413        let terminator = bytes[bytes.len() - 1];
414        if terminator != 0 {
415            return Err(MessageError::NotNullTerminated { terminator });
416        }
417
418        let pid = LittleEndian::read_u64(&bytes[..8]);
419        let tid = LittleEndian::read_u64(&bytes[8..16]);
420        let timestamp = zx::BootInstant::from_nanos(LittleEndian::read_i64(&bytes[16..24]));
421
422        let raw_severity = LittleEndian::read_i32(&bytes[24..28]);
423        let raw_severity = if raw_severity > (u8::MAX as i32) {
424            u8::MAX
425        } else if raw_severity < 0 {
426            0
427        } else {
428            u8::try_from(raw_severity).unwrap()
429        };
430        let dropped_logs = LittleEndian::read_u32(&bytes[28..METADATA_SIZE]) as u64;
431
432        // start reading tags after the header
433        let mut cursor = METADATA_SIZE;
434        let mut tag_len = bytes[cursor] as usize;
435        let mut tags = Vec::new();
436        while tag_len != 0 {
437            if tags.len() == MAX_TAGS {
438                return Err(MessageError::TooManyTags);
439            }
440
441            if tag_len > MAX_TAG_LEN - 1 {
442                return Err(MessageError::TagTooLong { index: tags.len(), len: tag_len });
443            }
444
445            if (cursor + tag_len + 1) > bytes.len() {
446                return Err(MessageError::OutOfBounds);
447            }
448
449            let tag_start = cursor + 1;
450            let tag_end = tag_start + tag_len;
451            let tag = String::from_utf8_lossy(&bytes[tag_start..tag_end]);
452            tags.push(tag.into());
453
454            cursor = tag_end;
455            tag_len = bytes[cursor] as usize;
456        }
457
458        let msg_start = cursor + 1;
459        let mut msg_end = cursor + 1;
460        while msg_end < bytes.len() {
461            if bytes[msg_end] > 0 {
462                msg_end += 1;
463                continue;
464            }
465            let message = String::from_utf8_lossy(&bytes[msg_start..msg_end]).into_owned();
466            let message_len = message.len();
467            let result = LoggerMessage {
468                timestamp,
469                raw_severity,
470                message: message.into_boxed_str(),
471                pid,
472                tid,
473                dropped_logs,
474                tags,
475                size_bytes: cursor + message_len + 1,
476            };
477            return Ok(result);
478        }
479
480        Err(MessageError::OutOfBounds)
481    }
482}
483
484#[allow(non_camel_case_types)]
485pub type fx_log_severity_t = c_int;
486
487#[repr(C)]
488#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
489pub struct fx_log_metadata_t {
490    pub pid: zx::sys::zx_koid_t,
491    pub tid: zx::sys::zx_koid_t,
492    pub time: zx::sys::zx_time_t,
493    pub severity: fx_log_severity_t,
494    pub dropped_logs: u32,
495}
496
497#[repr(C)]
498#[derive(Clone)]
499pub struct fx_log_packet_t {
500    pub metadata: fx_log_metadata_t,
501    // Contains concatenated tags and message and a null terminating character at
502    // the end.
503    // char(tag_len) + "tag1" + char(tag_len) + "tag2\0msg\0"
504    pub data: [c_char; MAX_DATAGRAM_LEN - METADATA_SIZE],
505}
506
507impl Default for fx_log_packet_t {
508    fn default() -> fx_log_packet_t {
509        fx_log_packet_t {
510            data: [0; MAX_DATAGRAM_LEN - METADATA_SIZE],
511            metadata: Default::default(),
512        }
513    }
514}
515
516impl fx_log_packet_t {
517    /// This struct has no padding bytes, but we can't use zerocopy because it needs const
518    /// generics to support arrays this large.
519    pub fn as_bytes(&self) -> &[u8] {
520        unsafe {
521            std::slice::from_raw_parts(
522                (self as *const Self) as *const u8,
523                mem::size_of::<fx_log_packet_t>(),
524            )
525        }
526    }
527
528    /// Fills data with a single value for defined region.
529    pub fn fill_data(&mut self, region: std::ops::Range<usize>, with: c_char) {
530        self.data[region].iter_mut().for_each(|c| *c = with);
531    }
532
533    /// Copies bytes to data at specifies offset.
534    pub fn add_data<T: std::convert::TryInto<c_char> + Copy>(&mut self, offset: usize, bytes: &[T])
535    where
536        <T as std::convert::TryInto<c_char>>::Error: std::fmt::Debug,
537    {
538        self.data[offset..(offset + bytes.len())]
539            .iter_mut()
540            .enumerate()
541            .for_each(|(i, x)| *x = bytes[i].try_into().unwrap());
542    }
543}