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