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, Record, Value};
11use flyweights::FlyStr;
12use libc::{c_char, c_int};
13use moniker::Moniker;
14use std::{mem, str};
15mod constants;
16pub mod error;
17pub mod ffi;
18pub use constants::*;
19
20#[cfg(test)]
21mod test;
22
23#[derive(Clone)]
24pub struct MonikerWithUrl {
25    pub moniker: ExtendedMoniker,
26    pub url: FlyStr,
27}
28
29/// Transforms the given legacy log message (already parsed) into a `LogsData` containing the
30/// given identity information.
31pub fn from_logger(source: MonikerWithUrl, msg: LoggerMessage) -> LogsData {
32    let (raw_severity, severity) = Severity::parse_exact(msg.raw_severity);
33    let mut builder = LogsDataBuilder::new(BuilderArgs {
34        timestamp: msg.timestamp,
35        component_url: Some(source.url),
36        moniker: source.moniker,
37        severity,
38    })
39    .set_pid(msg.pid)
40    .set_tid(msg.tid)
41    .set_dropped(msg.dropped_logs)
42    .set_message(msg.message);
43    if let Some(raw_severity) = raw_severity {
44        builder = builder.set_raw_severity(raw_severity);
45    }
46    for tag in &msg.tags {
47        builder = builder.add_tag(tag.as_ref());
48    }
49    builder.build()
50}
51
52#[cfg(fuchsia_api_level_at_least = "HEAD")]
53fn parse_archivist_args<'a>(
54    mut builder: LogsDataBuilder,
55    input: &'a Record<'a>,
56) -> Result<(LogsDataBuilder, usize), MessageError> {
57    let mut has_component_url = false;
58    let mut has_moniker = false;
59    let mut archivist_argument_count = 0;
60    for argument in input.arguments.iter().rev() {
61        // If Archivist records are expected, they should always be at the end.
62        // If no Archivist records are expected, treat them as regular key-value-pairs.
63        match argument {
64            Argument::Other { value, name } => {
65                if name == fidl_fuchsia_diagnostics::COMPONENT_URL_ARG_NAME {
66                    if let Value::Text(url) = value {
67                        builder = builder.set_url(Some(FlyStr::new(url.clone())));
68                        archivist_argument_count += 1;
69                        has_component_url = true;
70                        continue;
71                    }
72                }
73                if name == fidl_fuchsia_diagnostics::MONIKER_ARG_NAME {
74                    if let Value::Text(moniker) = value {
75                        builder = builder.set_moniker(ExtendedMoniker::parse_str(moniker)?);
76                        archivist_argument_count += 1;
77                        has_moniker = true;
78                        continue;
79                    }
80                }
81                if name == fidl_fuchsia_diagnostics::ROLLED_OUT_ARG_NAME {
82                    if let Value::UnsignedInt(ival) = value {
83                        builder = builder.set_rolled_out(*ival);
84                        archivist_argument_count += 1;
85                        continue;
86                    }
87                }
88                break;
89            }
90            _ => break,
91        }
92    }
93    if !has_component_url {
94        return Err(MessageError::MissingUrl);
95    }
96    if !has_moniker {
97        return Err(MessageError::MissingMoniker);
98    }
99    Ok((builder, archivist_argument_count))
100}
101
102#[cfg(fuchsia_api_level_less_than = "HEAD")]
103fn parse_archivist_args<'a>(
104    builder: LogsDataBuilder,
105    _input: &'a Record<'a>,
106) -> Result<(LogsDataBuilder, usize), MessageError> {
107    Ok((builder, 0))
108}
109
110fn parse_logs_data<'a>(
111    input: &'a Record<'a>,
112    source: Option<MonikerWithUrl>,
113) -> Result<LogsData, MessageError> {
114    let (raw_severity, severity) = Severity::parse_exact(input.severity);
115    let has_attribution = source.is_some();
116    let (maybe_moniker, maybe_url) =
117        source.map(|value| (Some(value.moniker), Some(value.url))).unwrap_or((None, None));
118    let mut builder = LogsDataBuilder::new(BuilderArgs {
119        component_url: maybe_url,
120        moniker: maybe_moniker.unwrap_or(ExtendedMoniker::ComponentInstance(
121            Moniker::parse_str("placeholder").unwrap(),
122        )),
123        severity,
124        timestamp: input.timestamp,
125    });
126    if let Some(raw_severity) = raw_severity {
127        builder = builder.set_raw_severity(raw_severity);
128    }
129    let archivist_argument_count = if has_attribution {
130        0
131    } else {
132        let (new_builder, count) = parse_archivist_args(builder, input)?;
133        builder = new_builder;
134        count
135    };
136
137    for argument in input.arguments.iter().take(input.arguments.len() - archivist_argument_count) {
138        match argument {
139            Argument::Tag(tag) => {
140                builder = builder.add_tag(tag.as_ref());
141            }
142            Argument::Pid(pid) => {
143                builder = builder.set_pid(pid.raw_koid());
144            }
145            Argument::Tid(tid) => {
146                builder = builder.set_tid(tid.raw_koid());
147            }
148            Argument::Dropped(dropped) => {
149                builder = builder.set_dropped(*dropped);
150            }
151            Argument::File(file) => {
152                builder = builder.set_file(file.as_ref());
153            }
154            Argument::Line(line) => {
155                builder = builder.set_line(*line);
156            }
157            Argument::Message(msg) => {
158                builder = builder.set_message(msg.as_ref());
159            }
160            Argument::Other { value, name } => {
161                let name = LogsField::Other(name.to_string());
162                builder = builder.add_key(match value {
163                    Value::SignedInt(v) => LogsProperty::Int(name, *v),
164                    Value::UnsignedInt(v) => LogsProperty::Uint(name, *v),
165                    Value::Floating(v) => LogsProperty::Double(name, *v),
166                    Value::Text(v) => LogsProperty::String(name, v.to_string()),
167                    Value::Boolean(v) => LogsProperty::Bool(name, *v),
168                })
169            }
170        }
171    }
172
173    Ok(builder.build())
174}
175
176/// Constructs a `LogsData` from the provided bytes, assuming the bytes
177/// are in the format specified as in the [log encoding], and come from
178///
179/// an Archivist LogStream with moniker, URL, and dropped logs output enabled.
180/// [log encoding] https://fuchsia.dev/fuchsia-src/development/logs/encodings
181pub fn from_extended_record(bytes: &[u8]) -> Result<(LogsData, &[u8]), MessageError> {
182    let (input, remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
183    let record = parse_logs_data(&input, None)?;
184    Ok((record, remaining))
185}
186
187/// Constructs a `LogsData` from the provided bytes, assuming the bytes
188/// are in the format specified as in the [log encoding].
189///
190/// [log encoding] https://fuchsia.dev/fuchsia-src/development/logs/encodings
191pub fn from_structured(source: MonikerWithUrl, bytes: &[u8]) -> Result<LogsData, MessageError> {
192    let (input, _remaining) = diagnostics_log_encoding::parse::parse_record(bytes)?;
193    let record = parse_logs_data(&input, Some(source))?;
194    Ok(record)
195}
196
197#[derive(Clone, Debug, Eq, PartialEq)]
198pub struct LoggerMessage {
199    pub timestamp: zx::BootInstant,
200    pub raw_severity: u8,
201    pub pid: u64,
202    pub tid: u64,
203    pub size_bytes: usize,
204    pub dropped_logs: u64,
205    pub message: Box<str>,
206    pub tags: Vec<Box<str>>,
207}
208
209/// Parse the provided buffer as if it implements the [logger/syslog wire format].
210///
211/// Note that this is distinct from the parsing we perform for the debuglog log, which also
212/// takes a `&[u8]` and is why we don't implement this as `TryFrom`.
213///
214/// [logger/syslog wire format]: https://fuchsia.googlesource.com/fuchsia/+/HEAD/zircon/system/ulib/syslog/include/lib/syslog/wire_format.h
215impl TryFrom<&[u8]> for LoggerMessage {
216    type Error = MessageError;
217
218    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
219        if bytes.len() < MIN_PACKET_SIZE {
220            return Err(MessageError::ShortRead { len: bytes.len() });
221        }
222
223        let terminator = bytes[bytes.len() - 1];
224        if terminator != 0 {
225            return Err(MessageError::NotNullTerminated { terminator });
226        }
227
228        let pid = LittleEndian::read_u64(&bytes[..8]);
229        let tid = LittleEndian::read_u64(&bytes[8..16]);
230        let timestamp = zx::BootInstant::from_nanos(LittleEndian::read_i64(&bytes[16..24]));
231
232        let raw_severity = LittleEndian::read_i32(&bytes[24..28]);
233        let raw_severity = if raw_severity > (u8::MAX as i32) {
234            u8::MAX
235        } else if raw_severity < 0 {
236            0
237        } else {
238            u8::try_from(raw_severity).unwrap()
239        };
240        let dropped_logs = LittleEndian::read_u32(&bytes[28..METADATA_SIZE]) as u64;
241
242        // start reading tags after the header
243        let mut cursor = METADATA_SIZE;
244        let mut tag_len = bytes[cursor] as usize;
245        let mut tags = Vec::new();
246        while tag_len != 0 {
247            if tags.len() == MAX_TAGS {
248                return Err(MessageError::TooManyTags);
249            }
250
251            if tag_len > MAX_TAG_LEN - 1 {
252                return Err(MessageError::TagTooLong { index: tags.len(), len: tag_len });
253            }
254
255            if (cursor + tag_len + 1) > bytes.len() {
256                return Err(MessageError::OutOfBounds);
257            }
258
259            let tag_start = cursor + 1;
260            let tag_end = tag_start + tag_len;
261            let tag = String::from_utf8_lossy(&bytes[tag_start..tag_end]);
262            tags.push(tag.into());
263
264            cursor = tag_end;
265            tag_len = bytes[cursor] as usize;
266        }
267
268        let msg_start = cursor + 1;
269        let mut msg_end = cursor + 1;
270        while msg_end < bytes.len() {
271            if bytes[msg_end] > 0 {
272                msg_end += 1;
273                continue;
274            }
275            let message = String::from_utf8_lossy(&bytes[msg_start..msg_end]).into_owned();
276            let message_len = message.len();
277            let result = LoggerMessage {
278                timestamp,
279                raw_severity,
280                message: message.into_boxed_str(),
281                pid,
282                tid,
283                dropped_logs,
284                tags,
285                size_bytes: cursor + message_len + 1,
286            };
287            return Ok(result);
288        }
289
290        Err(MessageError::OutOfBounds)
291    }
292}
293
294#[allow(non_camel_case_types)]
295pub type fx_log_severity_t = c_int;
296
297#[repr(C)]
298#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
299pub struct fx_log_metadata_t {
300    pub pid: zx::sys::zx_koid_t,
301    pub tid: zx::sys::zx_koid_t,
302    pub time: zx::sys::zx_time_t,
303    pub severity: fx_log_severity_t,
304    pub dropped_logs: u32,
305}
306
307#[repr(C)]
308#[derive(Clone)]
309pub struct fx_log_packet_t {
310    pub metadata: fx_log_metadata_t,
311    // Contains concatenated tags and message and a null terminating character at
312    // the end.
313    // char(tag_len) + "tag1" + char(tag_len) + "tag2\0msg\0"
314    pub data: [c_char; MAX_DATAGRAM_LEN - METADATA_SIZE],
315}
316
317impl Default for fx_log_packet_t {
318    fn default() -> fx_log_packet_t {
319        fx_log_packet_t {
320            data: [0; MAX_DATAGRAM_LEN - METADATA_SIZE],
321            metadata: Default::default(),
322        }
323    }
324}
325
326impl fx_log_packet_t {
327    /// This struct has no padding bytes, but we can't use zerocopy because it needs const
328    /// generics to support arrays this large.
329    pub fn as_bytes(&self) -> &[u8] {
330        unsafe {
331            std::slice::from_raw_parts(
332                (self as *const Self) as *const u8,
333                mem::size_of::<fx_log_packet_t>(),
334            )
335        }
336    }
337
338    /// Fills data with a single value for defined region.
339    pub fn fill_data(&mut self, region: std::ops::Range<usize>, with: c_char) {
340        self.data[region].iter_mut().for_each(|c| *c = with);
341    }
342
343    /// Copies bytes to data at specifies offset.
344    pub fn add_data<T: std::convert::TryInto<c_char> + Copy>(&mut self, offset: usize, bytes: &[T])
345    where
346        <T as std::convert::TryInto<c_char>>::Error: std::fmt::Debug,
347    {
348        self.data[offset..(offset + bytes.len())]
349            .iter_mut()
350            .enumerate()
351            .for_each(|(i, x)| *x = bytes[i].try_into().unwrap());
352    }
353}