use chrono::{Local, TimeZone, Utc};
use diagnostics_hierarchy::HierarchyMatcher;
use fidl_fuchsia_diagnostics::{DataType, Selector};
use fidl_fuchsia_inspect as finspect;
use flyweights::FlyStr;
use itertools::Itertools;
use moniker::EXTENDED_MONIKER_COMPONENT_MANAGER_STR;
use selectors::SelectorExt;
use serde::de::{DeserializeOwned, Deserializer};
use serde::{Deserialize, Serialize, Serializer};
use std::borrow::{Borrow, Cow};
use std::cmp::Ordering;
use std::fmt;
use std::hash::Hash;
use std::ops::Deref;
use std::str::FromStr;
use std::sync::LazyLock;
use std::time::Duration;
use termion::{color, style};
use thiserror::Error;
pub use diagnostics_hierarchy::{hierarchy, DiagnosticsHierarchy, Property};
pub use diagnostics_log_types_serde::Severity;
pub use moniker::ExtendedMoniker;
#[cfg(target_os = "fuchsia")]
mod logs_legacy;
const SCHEMA_VERSION: u64 = 1;
const MICROS_IN_SEC: u128 = 1000000;
const ROOT_MONIKER_REPR: &str = "<root>";
static DEFAULT_TREE_NAME: LazyLock<FlyStr> =
LazyLock::new(|| FlyStr::new(finspect::DEFAULT_TREE_NAME));
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Hash, Eq)]
#[serde(rename_all = "lowercase")]
pub enum InspectHandleName {
Name(FlyStr),
Filename(FlyStr),
}
impl std::fmt::Display for InspectHandleName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_ref())
}
}
impl InspectHandleName {
pub fn name(n: impl Into<FlyStr>) -> Self {
Self::Name(n.into())
}
pub fn filename(n: impl Into<FlyStr>) -> Self {
Self::Filename(n.into())
}
pub fn as_name(&self) -> Option<&str> {
if let Self::Name(n) = self {
Some(n.as_str())
} else {
None
}
}
pub fn as_filename(&self) -> Option<&str> {
if let Self::Filename(f) = self {
Some(f.as_str())
} else {
None
}
}
}
impl AsRef<str> for InspectHandleName {
fn as_ref(&self) -> &str {
match self {
Self::Filename(f) => f.as_str(),
Self::Name(n) => n.as_str(),
}
}
}
#[derive(Default, Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
pub enum DataSource {
#[default]
Unknown,
Inspect,
Logs,
}
pub trait MetadataError {
fn dropped_payload() -> Self;
fn message(&self) -> Option<&str>;
}
pub trait Metadata: DeserializeOwned + Serialize + Clone + Send {
type Error: Clone + MetadataError;
fn timestamp(&self) -> Timestamp;
fn errors(&self) -> Option<&[Self::Error]>;
fn set_errors(&mut self, errors: Vec<Self::Error>);
fn has_errors(&self) -> bool {
self.errors().map(|e| !e.is_empty()).unwrap_or_default()
}
}
pub trait DiagnosticsData {
type Metadata: Metadata;
type Key: AsRef<str> + Clone + DeserializeOwned + Eq + FromStr + Hash + Send + 'static;
const DATA_TYPE: DataType;
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct Inspect;
impl DiagnosticsData for Inspect {
type Metadata = InspectMetadata;
type Key = String;
const DATA_TYPE: DataType = DataType::Inspect;
}
impl Metadata for InspectMetadata {
type Error = InspectError;
fn timestamp(&self) -> Timestamp {
self.timestamp
}
fn errors(&self) -> Option<&[Self::Error]> {
self.errors.as_deref()
}
fn set_errors(&mut self, errors: Vec<Self::Error>) {
self.errors = Some(errors);
}
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct Logs;
impl DiagnosticsData for Logs {
type Metadata = LogsMetadata;
type Key = LogsField;
const DATA_TYPE: DataType = DataType::Logs;
}
impl Metadata for LogsMetadata {
type Error = LogError;
fn timestamp(&self) -> Timestamp {
self.timestamp
}
fn errors(&self) -> Option<&[Self::Error]> {
self.errors.as_deref()
}
fn set_errors(&mut self, errors: Vec<Self::Error>) {
self.errors = Some(errors);
}
}
pub fn serialize_timestamp<S>(timestamp: &Timestamp, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i64(timestamp.into_nanos())
}
pub fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<Timestamp, D::Error>
where
D: Deserializer<'de>,
{
let nanos = i64::deserialize(deserializer)?;
Ok(Timestamp::from_nanos(nanos))
}
#[cfg(target_os = "fuchsia")]
mod zircon {
pub type Timestamp = zx::BootInstant;
}
#[cfg(target_os = "fuchsia")]
pub use zircon::Timestamp;
#[cfg(not(target_os = "fuchsia"))]
mod host {
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::Add;
use std::time::Duration;
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct Timestamp(i64);
impl Timestamp {
pub fn into_nanos(self) -> i64 {
self.0
}
pub fn from_nanos(nanos: i64) -> Self {
Self(nanos)
}
}
impl Add<Duration> for Timestamp {
type Output = Timestamp;
fn add(self, rhs: Duration) -> Self::Output {
Timestamp(self.0 + rhs.as_nanos() as i64)
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
}
#[cfg(not(target_os = "fuchsia"))]
pub use host::Timestamp;
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct InspectMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<InspectError>>,
#[serde(flatten)]
pub name: InspectHandleName,
pub component_url: FlyStr,
#[serde(serialize_with = "serialize_timestamp", deserialize_with = "deserialize_timestamp")]
pub timestamp: Timestamp,
#[serde(skip_serializing_if = "std::ops::Not::not")]
#[serde(default)]
pub escrowed: bool,
}
impl InspectMetadata {
pub fn component_url(&self) -> &str {
self.component_url.as_str()
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct LogsMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<LogError>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub component_url: Option<FlyStr>,
#[serde(serialize_with = "serialize_timestamp", deserialize_with = "deserialize_timestamp")]
pub timestamp: Timestamp,
#[serde(with = "diagnostics_log_types_serde::severity")]
pub severity: Severity,
#[serde(skip_serializing_if = "Option::is_none")]
raw_severity: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tid: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u64>,
#[serde(skip)]
dropped: Option<u64>,
#[serde(skip)]
size_bytes: Option<usize>,
}
impl LogsMetadata {
pub fn component_url(&self) -> Option<&str> {
self.component_url.as_ref().map(|s| s.as_str())
}
pub fn raw_severity(&self) -> u8 {
match self.raw_severity {
Some(s) => s,
None => self.severity as u8,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct Data<D: DiagnosticsData> {
#[serde(default)]
pub data_source: DataSource,
#[serde(bound(
deserialize = "D::Metadata: DeserializeOwned",
serialize = "D::Metadata: Serialize"
))]
pub metadata: D::Metadata,
#[serde(deserialize_with = "moniker_deserialize", serialize_with = "moniker_serialize")]
pub moniker: ExtendedMoniker,
pub payload: Option<DiagnosticsHierarchy<D::Key>>,
#[serde(default)]
pub version: u64,
}
fn moniker_deserialize<'de, D>(deserializer: D) -> Result<ExtendedMoniker, D::Error>
where
D: serde::Deserializer<'de>,
{
let moniker_str = String::deserialize(deserializer)?;
ExtendedMoniker::parse_str(&moniker_str).map_err(serde::de::Error::custom)
}
fn moniker_serialize<S>(moniker: &ExtendedMoniker, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.collect_str(moniker)
}
impl<D> Data<D>
where
D: DiagnosticsData,
{
pub fn drop_payload(&mut self) {
self.metadata.set_errors(vec![
<<D as DiagnosticsData>::Metadata as Metadata>::Error::dropped_payload(),
]);
self.payload = None;
}
pub fn sort_payload(&mut self) {
if let Some(payload) = &mut self.payload {
payload.sort();
}
}
pub fn filter(mut self, selectors: &[Selector]) -> Result<Option<Self>, Error> {
let Some(hierarchy) = self.payload else {
return Ok(None);
};
let matching_selectors =
match self.moniker.match_against_selectors(selectors).collect::<Result<Vec<_>, _>>() {
Ok(selectors) if selectors.is_empty() => return Ok(None),
Ok(selectors) => selectors,
Err(e) => {
return Err(Error::Internal(e));
}
};
let matcher: HierarchyMatcher = match matching_selectors.try_into() {
Ok(hierarchy_matcher) => hierarchy_matcher,
Err(e) => {
return Err(Error::Internal(e.into()));
}
};
self.payload = match diagnostics_hierarchy::filter_hierarchy(hierarchy, &matcher) {
Some(hierarchy) => Some(hierarchy),
None => return Ok(None),
};
Ok(Some(self))
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
pub type InspectData = Data<Inspect>;
pub type LogsData = Data<Logs>;
pub type LogsHierarchy = DiagnosticsHierarchy<LogsField>;
pub type LogsProperty = Property<LogsField>;
impl Data<Inspect> {
pub fn name(&self) -> &str {
self.metadata.name.as_ref()
}
}
pub struct InspectDataBuilder {
data: Data<Inspect>,
}
impl InspectDataBuilder {
pub fn new(
moniker: ExtendedMoniker,
component_url: impl Into<FlyStr>,
timestamp: impl Into<Timestamp>,
) -> Self {
Self {
data: Data {
data_source: DataSource::Inspect,
moniker,
payload: None,
version: 1,
metadata: InspectMetadata {
errors: None,
name: InspectHandleName::name(DEFAULT_TREE_NAME.clone()),
component_url: component_url.into(),
timestamp: timestamp.into(),
escrowed: false,
},
},
}
}
pub fn escrowed(mut self, escrowed: bool) -> Self {
self.data.metadata.escrowed = escrowed;
self
}
pub fn with_hierarchy(
mut self,
hierarchy: DiagnosticsHierarchy<<Inspect as DiagnosticsData>::Key>,
) -> Self {
self.data.payload = Some(hierarchy);
self
}
pub fn with_errors(mut self, errors: Vec<InspectError>) -> Self {
self.data.metadata.errors = Some(errors);
self
}
pub fn with_name(mut self, name: InspectHandleName) -> Self {
self.data.metadata.name = name;
self
}
pub fn build(self) -> Data<Inspect> {
self.data
}
}
pub struct LogsDataBuilder {
errors: Vec<LogError>,
msg: Option<String>,
tags: Vec<String>,
pid: Option<u64>,
tid: Option<u64>,
file: Option<String>,
line: Option<u64>,
args: BuilderArgs,
keys: Vec<Property<LogsField>>,
raw_severity: Option<u8>,
}
pub struct BuilderArgs {
pub moniker: ExtendedMoniker,
pub timestamp: Timestamp,
pub component_url: Option<FlyStr>,
pub severity: Severity,
}
impl LogsDataBuilder {
pub fn new(args: BuilderArgs) -> Self {
LogsDataBuilder {
args,
errors: vec![],
msg: None,
file: None,
line: None,
pid: None,
tags: vec![],
tid: None,
keys: vec![],
raw_severity: None,
}
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn set_dropped(mut self, value: u64) -> Self {
if value == 0 {
return self;
}
let val = self.errors.iter_mut().find_map(|error| {
if let LogError::DroppedLogs { count } = error {
Some(count)
} else {
None
}
});
if let Some(v) = val {
*v = value;
} else {
self.errors.push(LogError::DroppedLogs { count: value });
}
self
}
pub fn set_raw_severity(mut self, severity: u8) -> Self {
self.raw_severity = Some(severity);
self
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn set_rolled_out(mut self, value: u64) -> Self {
if value == 0 {
return self;
}
let val = self.errors.iter_mut().find_map(|error| {
if let LogError::RolledOutLogs { count } = error {
Some(count)
} else {
None
}
});
if let Some(v) = val {
*v = value;
} else {
self.errors.push(LogError::RolledOutLogs { count: value });
}
self
}
pub fn set_severity(mut self, severity: Severity) -> Self {
self.args.severity = severity;
self.raw_severity = None;
self
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn set_pid(mut self, value: u64) -> Self {
self.pid = Some(value);
self
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn set_tid(mut self, value: u64) -> Self {
self.tid = Some(value);
self
}
pub fn build(self) -> LogsData {
let mut args = vec![];
if let Some(msg) = self.msg {
args.push(LogsProperty::String(LogsField::MsgStructured, msg));
}
let mut payload_fields = vec![DiagnosticsHierarchy::new("message", args, vec![])];
if !self.keys.is_empty() {
let val = DiagnosticsHierarchy::new("keys", self.keys, vec![]);
payload_fields.push(val);
}
let mut payload = LogsHierarchy::new("root", vec![], payload_fields);
payload.sort();
let (raw_severity, severity) =
self.raw_severity.map(Severity::parse_exact).unwrap_or((None, self.args.severity));
let mut ret = LogsData::for_logs(
self.args.moniker,
Some(payload),
self.args.timestamp,
self.args.component_url,
severity,
self.errors,
);
ret.metadata.raw_severity = raw_severity;
ret.metadata.file = self.file;
ret.metadata.line = self.line;
ret.metadata.pid = self.pid;
ret.metadata.tid = self.tid;
ret.metadata.tags = Some(self.tags);
ret
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn add_error(mut self, error: LogError) -> Self {
self.errors.push(error);
self
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn set_message(mut self, msg: impl Into<String>) -> Self {
self.msg = Some(msg.into());
self
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn set_file(mut self, file: impl Into<String>) -> Self {
self.file = Some(file.into());
self
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn set_line(mut self, line: u64) -> Self {
self.line = Some(line);
self
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn add_key(mut self, kvp: Property<LogsField>) -> Self {
self.keys.push(kvp);
self
}
#[must_use = "You must call build on your builder to consume its result"]
pub fn add_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
}
impl Data<Logs> {
pub fn for_logs(
moniker: ExtendedMoniker,
payload: Option<LogsHierarchy>,
timestamp: impl Into<Timestamp>,
component_url: Option<FlyStr>,
severity: impl Into<Severity>,
errors: Vec<LogError>,
) -> Self {
let errors = if errors.is_empty() { None } else { Some(errors) };
Data {
moniker,
version: SCHEMA_VERSION,
data_source: DataSource::Logs,
payload,
metadata: LogsMetadata {
timestamp: timestamp.into(),
component_url,
severity: severity.into(),
raw_severity: None,
errors,
file: None,
line: None,
pid: None,
tags: None,
tid: None,
dropped: None,
size_bytes: None,
},
}
}
pub fn set_raw_severity(&mut self, raw_severity: u8) {
self.metadata.raw_severity = Some(raw_severity);
self.metadata.severity = Severity::from(raw_severity);
}
pub fn set_severity(&mut self, severity: Severity) {
self.metadata.severity = severity;
self.metadata.raw_severity = None;
}
pub fn msg(&self) -> Option<&str> {
self.payload_message().as_ref().and_then(|p| {
p.properties.iter().find_map(|property| match property {
LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg.as_str()),
_ => None,
})
})
}
pub fn msg_mut(&mut self) -> Option<&mut String> {
self.payload_message_mut().and_then(|p| {
p.properties.iter_mut().find_map(|property| match property {
LogsProperty::String(LogsField::MsgStructured, msg) => Some(msg),
_ => None,
})
})
}
pub fn payload_message(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
self.payload
.as_ref()
.and_then(|p| p.children.iter().find(|property| property.name.as_str() == "message"))
}
pub fn payload_keys(&self) -> Option<&DiagnosticsHierarchy<LogsField>> {
self.payload
.as_ref()
.and_then(|p| p.children.iter().find(|property| property.name.as_str() == "keys"))
}
pub fn payload_keys_strings(&self) -> Box<dyn Iterator<Item = String> + '_> {
let maybe_iter = self.payload_keys().map(|p| {
Box::new(p.properties.iter().filter_map(|property| match property {
LogsProperty::String(LogsField::Tag, _tag) => None,
LogsProperty::String(LogsField::ProcessId, _tag) => None,
LogsProperty::String(LogsField::ThreadId, _tag) => None,
LogsProperty::String(LogsField::Dropped, _tag) => None,
LogsProperty::String(LogsField::Msg, _tag) => None,
LogsProperty::String(LogsField::FilePath, _tag) => None,
LogsProperty::String(LogsField::LineNumber, _tag) => None,
LogsProperty::String(
key @ (LogsField::Other(_) | LogsField::MsgStructured),
value,
) => Some(format!("{}={}", key, value)),
LogsProperty::Bytes(key @ (LogsField::Other(_) | LogsField::MsgStructured), _) => {
Some(format!("{} = <bytes>", key))
}
LogsProperty::Int(
key @ (LogsField::Other(_) | LogsField::MsgStructured),
value,
) => Some(format!("{}={}", key, value)),
LogsProperty::Uint(
key @ (LogsField::Other(_) | LogsField::MsgStructured),
value,
) => Some(format!("{}={}", key, value)),
LogsProperty::Double(
key @ (LogsField::Other(_) | LogsField::MsgStructured),
value,
) => Some(format!("{}={}", key, value)),
LogsProperty::Bool(
key @ (LogsField::Other(_) | LogsField::MsgStructured),
value,
) => Some(format!("{}={}", key, value)),
LogsProperty::DoubleArray(
key @ (LogsField::Other(_) | LogsField::MsgStructured),
value,
) => Some(format!("{}={:?}", key, value)),
LogsProperty::IntArray(
key @ (LogsField::Other(_) | LogsField::MsgStructured),
value,
) => Some(format!("{}={:?}", key, value)),
LogsProperty::UintArray(
key @ (LogsField::Other(_) | LogsField::MsgStructured),
value,
) => Some(format!("{}={:?}", key, value)),
LogsProperty::StringList(
key @ (LogsField::Other(_) | LogsField::MsgStructured),
value,
) => Some(format!("{}={:?}", key, value)),
_ => None,
}))
});
match maybe_iter {
Some(i) => Box::new(i),
None => Box::new(std::iter::empty()),
}
}
pub fn payload_message_mut(&mut self) -> Option<&mut DiagnosticsHierarchy<LogsField>> {
self.payload.as_mut().and_then(|p| {
p.children.iter_mut().find(|property| property.name.as_str() == "message")
})
}
pub fn file_path(&self) -> Option<&str> {
self.metadata.file.as_deref()
}
pub fn line_number(&self) -> Option<&u64> {
self.metadata.line.as_ref()
}
pub fn pid(&self) -> Option<u64> {
self.metadata.pid
}
pub fn tid(&self) -> Option<u64> {
self.metadata.tid
}
pub fn tags(&self) -> Option<&Vec<String>> {
self.metadata.tags.as_ref()
}
pub fn severity(&self) -> Severity {
self.metadata.severity
}
pub fn dropped_logs(&self) -> Option<u64> {
self.metadata.errors.as_ref().and_then(|errors| {
errors.iter().find_map(|e| match e {
LogError::DroppedLogs { count } => Some(*count),
_ => None,
})
})
}
pub fn rolled_out_logs(&self) -> Option<u64> {
self.metadata.errors.as_ref().and_then(|errors| {
errors.iter().find_map(|e| match e {
LogError::RolledOutLogs { count } => Some(*count),
_ => None,
})
})
}
pub fn component_name(&self) -> Cow<'_, str> {
match &self.moniker {
ExtendedMoniker::ComponentManager => {
Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
}
ExtendedMoniker::ComponentInstance(moniker) => {
if moniker.is_root() {
Cow::Borrowed(ROOT_MONIKER_REPR)
} else {
Cow::Owned(moniker.path().iter().last().unwrap().to_string())
}
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct LogTextDisplayOptions {
pub show_full_moniker: bool,
pub show_metadata: bool,
pub show_tags: bool,
pub show_file: bool,
pub color: LogTextColor,
pub time_format: LogTimeDisplayFormat,
}
impl Default for LogTextDisplayOptions {
fn default() -> Self {
Self {
show_full_moniker: true,
show_metadata: true,
show_tags: true,
show_file: true,
color: Default::default(),
time_format: Default::default(),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub enum LogTextColor {
#[default]
None,
BySeverity,
Highlight,
}
impl LogTextColor {
fn begin_record(&self, f: &mut fmt::Formatter<'_>, severity: Severity) -> fmt::Result {
match self {
LogTextColor::BySeverity => match severity {
Severity::Fatal => {
write!(f, "{}{}", color::Bg(color::Red), color::Fg(color::White))?
}
Severity::Error => write!(f, "{}", color::Fg(color::Red))?,
Severity::Warn => write!(f, "{}", color::Fg(color::Yellow))?,
Severity::Info => (),
Severity::Debug => write!(f, "{}", color::Fg(color::LightBlue))?,
Severity::Trace => write!(f, "{}", color::Fg(color::LightMagenta))?,
},
LogTextColor::Highlight => write!(f, "{}", color::Fg(color::LightYellow))?,
LogTextColor::None => {}
}
Ok(())
}
fn begin_lost_message_counts(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let LogTextColor::BySeverity = self {
write!(f, "{}", color::Fg(color::Yellow))?;
}
Ok(())
}
fn end_record(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogTextColor::BySeverity | LogTextColor::Highlight => write!(f, "{}", style::Reset)?,
LogTextColor::None => {}
};
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Timezone {
Local,
Utc,
}
impl Timezone {
fn format(&self, seconds: i64, rem_nanos: u32) -> impl std::fmt::Display {
const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%3f";
match self {
Timezone::Local => {
Local.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
}
Timezone::Utc => {
Utc.timestamp_opt(seconds, rem_nanos).unwrap().format(TIMESTAMP_FORMAT)
}
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub enum LogTimeDisplayFormat {
#[default]
Original,
WallTime {
tz: Timezone,
offset: i64,
},
}
impl LogTimeDisplayFormat {
fn write_timestamp(&self, f: &mut fmt::Formatter<'_>, time: Timestamp) -> fmt::Result {
const NANOS_IN_SECOND: i64 = 1_000_000_000;
match self {
Self::Original | Self::WallTime { offset: 0, .. } => {
let time: Duration =
Duration::from_nanos(time.into_nanos().try_into().unwrap_or(0));
write!(f, "[{:05}.{:06}]", time.as_secs(), time.as_micros() % MICROS_IN_SEC)?;
}
Self::WallTime { tz, offset } => {
let adjusted = time.into_nanos() + offset;
let seconds = adjusted / NANOS_IN_SECOND;
let rem_nanos = (adjusted % NANOS_IN_SECOND) as u32;
let formatted = tz.format(seconds, rem_nanos);
write!(f, "[{}]", formatted)?;
}
}
Ok(())
}
}
pub struct LogTextPresenter<'a> {
log: &'a Data<Logs>,
options: LogTextDisplayOptions,
}
impl<'a> LogTextPresenter<'a> {
pub fn new(log: &'a Data<Logs>, options: LogTextDisplayOptions) -> Self {
Self { log, options }
}
}
impl fmt::Display for Data<Logs> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
LogTextPresenter::new(self, Default::default()).fmt(f)
}
}
impl Deref for LogTextPresenter<'_> {
type Target = Data<Logs>;
fn deref(&self) -> &Self::Target {
self.log
}
}
impl fmt::Display for LogTextPresenter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.options.color.begin_record(f, self.log.severity())?;
self.options.time_format.write_timestamp(f, self.metadata.timestamp)?;
if self.options.show_metadata {
match self.pid() {
Some(pid) => write!(f, "[{pid}]")?,
None => write!(f, "[]")?,
}
match self.tid() {
Some(tid) => write!(f, "[{tid}]")?,
None => write!(f, "[]")?,
}
}
let moniker = if self.options.show_full_moniker {
match &self.moniker {
ExtendedMoniker::ComponentManager => {
Cow::Borrowed(EXTENDED_MONIKER_COMPONENT_MANAGER_STR)
}
ExtendedMoniker::ComponentInstance(instance) => {
if instance.is_root() {
Cow::Borrowed(ROOT_MONIKER_REPR)
} else {
Cow::Owned(instance.to_string())
}
}
}
} else {
self.component_name()
};
write!(f, "[{moniker}]")?;
if self.options.show_tags {
match &self.metadata.tags {
Some(tags) if !tags.is_empty() => {
let mut filtered =
tags.iter().filter(|tag| *tag != moniker.as_ref()).peekable();
if filtered.peek().is_some() {
write!(f, "[{}]", filtered.join(","))?;
}
}
_ => {}
}
}
write!(f, " {}:", self.metadata.severity)?;
if self.options.show_file {
match (&self.metadata.file, &self.metadata.line) {
(Some(file), Some(line)) => write!(f, " [{file}({line})]")?,
(Some(file), None) => write!(f, " [{file}]")?,
_ => (),
}
}
if let Some(msg) = self.msg() {
write!(f, " {msg}")?;
} else {
write!(f, " <missing message>")?;
}
for kvp in self.payload_keys_strings() {
write!(f, " {}", kvp)?;
}
let dropped = self.log.dropped_logs().unwrap_or_default();
let rolled = self.log.rolled_out_logs().unwrap_or_default();
if dropped != 0 || rolled != 0 {
self.options.color.begin_lost_message_counts(f)?;
if dropped != 0 {
write!(f, " [dropped={dropped}]")?;
}
if rolled != 0 {
write!(f, " [rolled={rolled}]")?;
}
}
self.options.color.end_record(f)?;
Ok(())
}
}
impl Eq for Data<Logs> {}
impl PartialOrd for Data<Logs> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Data<Logs> {
fn cmp(&self, other: &Self) -> Ordering {
self.metadata.timestamp.cmp(&other.metadata.timestamp)
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
pub enum LogsField {
ProcessId,
ThreadId,
Dropped,
Tag,
Msg,
MsgStructured,
FilePath,
LineNumber,
Other(String),
}
impl fmt::Display for LogsField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogsField::ProcessId => write!(f, "pid"),
LogsField::ThreadId => write!(f, "tid"),
LogsField::Dropped => write!(f, "num_dropped"),
LogsField::Tag => write!(f, "tag"),
LogsField::Msg => write!(f, "message"),
LogsField::MsgStructured => write!(f, "value"),
LogsField::FilePath => write!(f, "file_path"),
LogsField::LineNumber => write!(f, "line_number"),
LogsField::Other(name) => write!(f, "{}", name),
}
}
}
pub const PID_LABEL: &str = "pid";
pub const TID_LABEL: &str = "tid";
pub const DROPPED_LABEL: &str = "num_dropped";
pub const TAG_LABEL: &str = "tag";
pub const MESSAGE_LABEL_STRUCTURED: &str = "value";
pub const MESSAGE_LABEL: &str = "message";
pub const FILE_PATH_LABEL: &str = "file";
pub const LINE_NUMBER_LABEL: &str = "line";
impl AsRef<str> for LogsField {
fn as_ref(&self) -> &str {
match self {
Self::ProcessId => PID_LABEL,
Self::ThreadId => TID_LABEL,
Self::Dropped => DROPPED_LABEL,
Self::Tag => TAG_LABEL,
Self::Msg => MESSAGE_LABEL,
Self::FilePath => FILE_PATH_LABEL,
Self::LineNumber => LINE_NUMBER_LABEL,
Self::MsgStructured => MESSAGE_LABEL_STRUCTURED,
Self::Other(str) => str.as_str(),
}
}
}
impl<T> From<T> for LogsField
where
T: Deref<Target = str>,
{
fn from(s: T) -> Self {
match s.as_ref() {
PID_LABEL => Self::ProcessId,
TID_LABEL => Self::ThreadId,
DROPPED_LABEL => Self::Dropped,
TAG_LABEL => Self::Tag,
MESSAGE_LABEL => Self::Msg,
FILE_PATH_LABEL => Self::FilePath,
LINE_NUMBER_LABEL => Self::LineNumber,
MESSAGE_LABEL_STRUCTURED => Self::MsgStructured,
_ => Self::Other(s.to_string()),
}
}
}
impl FromStr for LogsField {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s))
}
}
#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
pub enum LogError {
#[serde(rename = "dropped_logs")]
DroppedLogs { count: u64 },
#[serde(rename = "rolled_out_logs")]
RolledOutLogs { count: u64 },
#[serde(rename = "parse_record")]
FailedToParseRecord(String),
#[serde(rename = "other")]
Other { message: String },
}
const DROPPED_PAYLOAD_MSG: &str = "Schema failed to fit component budget.";
impl MetadataError for LogError {
fn dropped_payload() -> Self {
Self::Other { message: DROPPED_PAYLOAD_MSG.into() }
}
fn message(&self) -> Option<&str> {
match self {
Self::FailedToParseRecord(msg) => Some(msg.as_str()),
Self::Other { message } => Some(message.as_str()),
_ => None,
}
}
}
#[derive(Debug, PartialEq, Clone, Eq)]
pub struct InspectError {
pub message: String,
}
impl MetadataError for InspectError {
fn dropped_payload() -> Self {
Self { message: "Schema failed to fit component budget.".into() }
}
fn message(&self) -> Option<&str> {
Some(self.message.as_str())
}
}
impl fmt::Display for InspectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Borrow<str> for InspectError {
fn borrow(&self) -> &str {
&self.message
}
}
impl Serialize for InspectError {
fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
self.message.serialize(ser)
}
}
impl<'de> Deserialize<'de> for InspectError {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let message = String::deserialize(de)?;
Ok(Self { message })
}
}
#[cfg(test)]
mod tests {
use super::*;
use diagnostics_hierarchy::hierarchy;
use selectors::FastError;
use serde_json::json;
const TEST_URL: &str = "fuchsia-pkg://test";
#[fuchsia::test]
fn test_canonical_json_inspect_formatting() {
let mut hierarchy = hierarchy! {
root: {
x: "foo",
}
};
hierarchy.sort();
let json_schema = InspectDataBuilder::new(
"a/b/c/d".try_into().unwrap(),
TEST_URL,
Timestamp::from_nanos(123456i64),
)
.with_hierarchy(hierarchy)
.with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
.build();
let result_json =
serde_json::to_value(&json_schema).expect("serialization should succeed.");
let expected_json = json!({
"moniker": "a/b/c/d",
"version": 1,
"data_source": "Inspect",
"payload": {
"root": {
"x": "foo"
}
},
"metadata": {
"component_url": TEST_URL,
"filename": "test_file_plz_ignore.inspect",
"timestamp": 123456,
}
});
pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
}
#[fuchsia::test]
fn test_errorful_json_inspect_formatting() {
let json_schema = InspectDataBuilder::new(
"a/b/c/d".try_into().unwrap(),
TEST_URL,
Timestamp::from_nanos(123456i64),
)
.with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
.with_errors(vec![InspectError { message: "too much fun being had.".to_string() }])
.build();
let result_json =
serde_json::to_value(&json_schema).expect("serialization should succeed.");
let expected_json = json!({
"moniker": "a/b/c/d",
"version": 1,
"data_source": "Inspect",
"payload": null,
"metadata": {
"component_url": TEST_URL,
"errors": ["too much fun being had."],
"filename": "test_file_plz_ignore.inspect",
"timestamp": 123456,
}
});
pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
}
fn parse_selectors(strings: Vec<&str>) -> Vec<Selector> {
strings
.iter()
.map(|s| match selectors::parse_selector::<FastError>(s) {
Ok(selector) => selector,
Err(e) => panic!("Couldn't parse selector {s}: {e}"),
})
.collect::<Vec<_>>()
}
#[fuchsia::test]
fn test_filter_returns_none_on_empty_hierarchy() {
let data = InspectDataBuilder::new(
"a/b/c/d".try_into().unwrap(),
TEST_URL,
Timestamp::from_nanos(123456i64),
)
.build();
let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
}
#[fuchsia::test]
fn test_filter_returns_none_on_selector_mismatch() {
let mut hierarchy = hierarchy! {
root: {
x: "foo",
}
};
hierarchy.sort();
let data = InspectDataBuilder::new(
"b/c/d/e".try_into().unwrap(),
TEST_URL,
Timestamp::from_nanos(123456i64),
)
.with_hierarchy(hierarchy)
.build();
let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
assert_eq!(data.filter(&selectors).expect("Filter OK"), None);
}
#[fuchsia::test]
fn test_filter_returns_none_on_data_mismatch() {
let mut hierarchy = hierarchy! {
root: {
x: "foo",
}
};
hierarchy.sort();
let data = InspectDataBuilder::new(
"a/b/c/d".try_into().unwrap(),
TEST_URL,
Timestamp::from_nanos(123456i64),
)
.with_hierarchy(hierarchy)
.build();
let selectors = parse_selectors(vec!["a/b/c/d:foo"]);
assert_eq!(data.filter(&selectors).expect("FIlter OK"), None);
}
#[fuchsia::test]
fn test_filter_returns_matching_data() {
let mut hierarchy = hierarchy! {
root: {
x: "foo",
y: "bar",
}
};
hierarchy.sort();
let data = InspectDataBuilder::new(
"a/b/c/d".try_into().unwrap(),
TEST_URL,
Timestamp::from_nanos(123456i64),
)
.with_name(InspectHandleName::filename("test_file_plz_ignore.inspect"))
.with_hierarchy(hierarchy)
.build();
let selectors = parse_selectors(vec!["a/b/c/d:root:x"]);
let expected_json = json!({
"moniker": "a/b/c/d",
"version": 1,
"data_source": "Inspect",
"payload": {
"root": {
"x": "foo"
}
},
"metadata": {
"component_url": TEST_URL,
"filename": "test_file_plz_ignore.inspect",
"timestamp": 123456,
}
});
let result_json = serde_json::to_value(data.filter(&selectors).expect("Filter Ok"))
.expect("serialization should succeed.");
pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
}
#[fuchsia::test]
fn default_builder_test() {
let builder = LogsDataBuilder::new(BuilderArgs {
component_url: Some("url".into()),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
timestamp: Timestamp::from_nanos(0),
});
let expected_json = json!({
"moniker": "moniker",
"version": 1,
"data_source": "Logs",
"payload": {
"root":
{
"message":{}
}
},
"metadata": {
"component_url": "url",
"severity": "INFO",
"tags": [],
"timestamp": 0,
}
});
let result_json =
serde_json::to_value(builder.build()).expect("serialization should succeed.");
pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
}
#[fuchsia::test]
fn regular_message_test() {
let builder = LogsDataBuilder::new(BuilderArgs {
component_url: Some("url".into()),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
timestamp: Timestamp::from_nanos(0),
})
.set_message("app")
.set_file("test file.cc")
.set_line(420)
.set_pid(1001)
.set_tid(200)
.set_dropped(2)
.add_tag("You're")
.add_tag("IT!")
.add_key(LogsProperty::String(LogsField::Other("key".to_string()), "value".to_string()));
let expected_json = json!({
"moniker": "moniker",
"version": 1,
"data_source": "Logs",
"payload": {
"root":
{
"keys":{
"key":"value"
},
"message":{
"value":"app"
}
}
},
"metadata": {
"errors": [],
"component_url": "url",
"errors": [{"dropped_logs":{"count":2}}],
"file": "test file.cc",
"line": 420,
"pid": 1001,
"severity": "INFO",
"tags": ["You're", "IT!"],
"tid": 200,
"timestamp": 0,
}
});
let result_json =
serde_json::to_value(builder.build()).expect("serialization should succeed.");
pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
}
#[fuchsia::test]
fn display_for_logs() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
format!("{}", data)
)
}
#[fuchsia::test]
fn display_for_logs_with_duplicate_moniker() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("moniker")
.add_tag("bar")
.add_tag("moniker")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][123][456][moniker][bar] INFO: [some_file.cc(420)] some message test=property value=test",
format!("{}", data)
)
}
#[fuchsia::test]
fn display_for_logs_with_duplicate_moniker_and_no_other_tags() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("moniker")
.add_tag("moniker")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
format!("{}", data)
)
}
#[fuchsia::test]
fn display_for_logs_partial_moniker() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("test/moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
show_full_moniker: false,
..Default::default()
}))
)
}
#[fuchsia::test]
fn display_for_logs_exclude_metadata() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
show_metadata: false,
..Default::default()
}))
)
}
#[fuchsia::test]
fn display_for_logs_exclude_tags() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][123][456][moniker] INFO: [some_file.cc(420)] some message test=property value=test",
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
show_tags: false,
..Default::default()
}))
)
}
#[fuchsia::test]
fn display_for_logs_exclude_file() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][123][456][moniker][foo,bar] INFO: some message test=property value=test",
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
show_file: false,
..Default::default()
}))
)
}
#[fuchsia::test]
fn display_for_logs_include_color_by_severity() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Error,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
format!("{}[00012.345678][123][456][moniker][foo,bar] ERROR: [some_file.cc(420)] some message test=property value=test{}", color::Fg(color::Red), style::Reset),
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
color: LogTextColor::BySeverity,
..Default::default()
}))
)
}
#[fuchsia::test]
fn display_for_logs_highlight_line() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
format!("{}[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{}", color::Fg(color::LightYellow), style::Reset),
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
color: LogTextColor::Highlight,
..Default::default()
}))
)
}
#[fuchsia::test]
fn display_for_logs_with_wall_time() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[1970-01-01 00:00:12.345][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 1 },
..Default::default()
}))
);
assert_eq!(
"[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test",
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
time_format: LogTimeDisplayFormat::WallTime { tz: Timezone::Utc, offset: 0 },
..Default::default()
})),
"should fall back to monotonic if offset is 0"
);
}
#[fuchsia::test]
fn display_for_logs_with_dropped_count() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_dropped(5)
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5]",
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
);
assert_eq!(
format!("[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5]{}", color::Fg(color::Yellow), style::Reset),
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
color: LogTextColor::BySeverity,
..Default::default()
})),
);
}
#[fuchsia::test]
fn display_for_logs_with_rolled_count() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_rolled_out(10)
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [rolled=10]",
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
);
assert_eq!(
format!("[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [rolled=10]{}", color::Fg(color::Yellow), style::Reset),
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
color: LogTextColor::BySeverity,
..Default::default()
})),
);
}
#[fuchsia::test]
fn display_for_logs_with_dropped_and_rolled_counts() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_dropped(5)
.set_rolled_out(10)
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.set_file("some_file.cc".to_string())
.set_line(420)
.add_tag("foo")
.add_tag("bar")
.add_key(LogsProperty::String(LogsField::Other("test".to_string()), "property".to_string()))
.add_key(LogsProperty::String(LogsField::MsgStructured, "test".to_string()))
.build();
assert_eq!(
"[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test [dropped=5] [rolled=10]",
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions::default())),
);
assert_eq!(
format!("[00012.345678][123][456][moniker][foo,bar] INFO: [some_file.cc(420)] some message test=property value=test{} [dropped=5] [rolled=10]{}", color::Fg(color::Yellow), style::Reset),
format!("{}", LogTextPresenter::new(&data, LogTextDisplayOptions {
color: LogTextColor::BySeverity,
..Default::default()
})),
);
}
#[fuchsia::test]
fn display_for_logs_no_tags() {
let data = LogsDataBuilder::new(BuilderArgs {
timestamp: Timestamp::from_nanos(12345678000i64),
component_url: Some(FlyStr::from("fake-url")),
moniker: ExtendedMoniker::parse_str("moniker").unwrap(),
severity: Severity::Info,
})
.set_pid(123)
.set_tid(456)
.set_message("some message".to_string())
.build();
assert_eq!("[00012.345678][123][456][moniker] INFO: some message", format!("{}", data))
}
#[fuchsia::test]
fn size_bytes_deserialize_backwards_compatibility() {
let original_json = json!({
"moniker": "a/b",
"version": 1,
"data_source": "Logs",
"payload": {
"root": {
"message":{}
}
},
"metadata": {
"component_url": "url",
"severity": "INFO",
"tags": [],
"timestamp": 123,
}
});
let expected_data = LogsDataBuilder::new(BuilderArgs {
component_url: Some("url".into()),
moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
severity: Severity::Info,
timestamp: Timestamp::from_nanos(123),
})
.build();
let original_data: LogsData = serde_json::from_value(original_json).unwrap();
assert_eq!(original_data, expected_data);
assert_eq!(original_data.metadata.size_bytes, None);
}
#[fuchsia::test]
fn dropped_deserialize_backwards_compatibility() {
let original_json = json!({
"moniker": "a/b",
"version": 1,
"data_source": "Logs",
"payload": {
"root": {
"message":{}
}
},
"metadata": {
"dropped": 0,
"component_url": "url",
"severity": "INFO",
"tags": [],
"timestamp": 123,
}
});
let expected_data = LogsDataBuilder::new(BuilderArgs {
component_url: Some("url".into()),
moniker: ExtendedMoniker::parse_str("a/b").unwrap(),
severity: Severity::Info,
timestamp: Timestamp::from_nanos(123),
})
.build();
let original_data: LogsData = serde_json::from_value(original_json).unwrap();
assert_eq!(original_data, expected_data);
assert_eq!(original_data.metadata.dropped, None);
}
#[fuchsia::test]
fn severity_aliases() {
assert_eq!(Severity::from_str("warn").unwrap(), Severity::Warn);
assert_eq!(Severity::from_str("warning").unwrap(), Severity::Warn);
}
}