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