blob: 1ff049365d5a0ea8516af89d3f950087addfaef0 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::{format_err, Error},
diagnostics_reader::{ArchiveReader, DiagnosticsHierarchy, Inspect, Property},
fuchsia_zircon as zx,
std::{cmp::Reverse, collections::HashMap, fmt, str::FromStr},
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LogSeverity {
TRACE = 0,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
}
impl FromStr for LogSeverity {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"fatal" => Ok(LogSeverity::FATAL),
"error" => Ok(LogSeverity::ERROR),
"warn" | "warning" => Ok(LogSeverity::WARN),
"info" => Ok(LogSeverity::INFO),
"debug" => Ok(LogSeverity::DEBUG),
"trace" => Ok(LogSeverity::TRACE),
_ => Err(format_err!("{} is not a valid log severity", s)),
}
}
}
// Number of log messages broken down by severity for a given component.
pub struct ComponentLogStats {
component_url: String,
short_name: String,
start_time: i64,
log_counts: Vec<u64>,
total_logs: u64,
}
impl ComponentLogStats {
pub fn new(
node: &DiagnosticsHierarchy,
start_times: &HashMap<String, i64>,
) -> ComponentLogStats {
let component_url = node.name.clone();
let short_name = {
let last_slash_index = component_url.rfind("/");
match last_slash_index {
Some(index) => &component_url[index + 1..],
None => component_url.as_str(),
}
.to_string()
};
let start_time = match start_times.get(&short_name) {
Some(s) => *s,
None => 0,
};
let map = node
.properties
.iter()
.map(|x| match x {
Property::Int(name, value) => (name.as_str(), *value as u64),
Property::Uint(name, value) => (name.as_str(), *value),
_ => ("", 0),
})
.collect::<HashMap<_, _>>();
ComponentLogStats {
component_url,
short_name,
start_time,
log_counts: vec![
map["trace_logs"],
map["debug_logs"],
map["info_logs"],
map["warning_logs"],
map["error_logs"],
map["fatal_logs"],
],
total_logs: map["total_logs"],
}
}
pub fn error_rate(&self, now: i64) -> f64 {
let uptime_in_hours = (now - self.start_time) as f64 / 3600.0;
self.get_count(LogSeverity::ERROR) as f64 / uptime_in_hours
}
pub fn get_count(&self, severity: LogSeverity) -> u64 {
self.log_counts[severity as usize]
}
fn get_sort_key(&self) -> (u64, u64, u64, u64, u64) {
(
// Fatal logs are reported separately. They shouldn't affect the order of the output.
self.get_count(LogSeverity::ERROR),
self.get_count(LogSeverity::WARN),
self.get_count(LogSeverity::INFO),
self.get_count(LogSeverity::DEBUG),
self.get_count(LogSeverity::TRACE),
)
}
}
/// Log statistics broken down by component.
pub struct LogStats {
/// The list of log statistics broken down by component.
stats_list: Vec<ComponentLogStats>,
/// The minimum severity log to display.
min_severity: LogSeverity,
}
impl LogStats {
pub async fn new(min_severity: LogSeverity) -> Result<Self, Error> {
let mut response = ArchiveReader::new()
.add_selector("bootstrap/archivist:root/log_stats/by_component/*:*")
.add_selector("bootstrap/archivist:root/event_stats/recent_events/*:*")
.snapshot::<Inspect>()
.await?;
if response.len() != 1 {
return Err(format_err!("Expected one inspect tree, received {}", response.len()));
}
let inspect_root = response.pop().and_then(|r| r.payload).unwrap();
Self::new_with_root(min_severity, &inspect_root).await
}
pub async fn new_with_root(
min_severity: LogSeverity,
inspect_root: &DiagnosticsHierarchy,
) -> Result<Self, Error> {
let start_times = Self::extract_component_start_times(&inspect_root)?;
let stats_node = inspect_root
.get_child_by_path(&vec!["log_stats", "by_component"])
.ok_or(format_err!("Missing log_stats/by_component"))?;
let mut stats_list = stats_node
.children
.iter()
.map(|x| ComponentLogStats::new(x, &start_times))
.collect::<Vec<_>>();
stats_list.sort_by_key(|x| Reverse(x.get_sort_key()));
Ok(Self { stats_list, min_severity })
}
pub fn min_severity(&self) -> LogSeverity {
// Min severity cannot be FATAL.
if self.min_severity == LogSeverity::FATAL {
LogSeverity::ERROR
} else {
self.min_severity.clone()
}
}
pub fn get_by_url(&self, url: &str) -> Option<&ComponentLogStats> {
self.stats_list.iter().filter(|x| x.component_url == url).next()
}
// Extracts the component start times from the inspect hierarchy.
fn extract_component_start_times(
inspect_root: &DiagnosticsHierarchy,
) -> Result<HashMap<String, i64>, Error> {
let mut res = HashMap::new();
let events_node = inspect_root
.get_child_by_path(&vec!["event_stats", "recent_events"])
.ok_or(format_err!("Missing event_stats/recent_events"))?;
for event in &events_node.children {
// Extract the component name from the moniker. This allows us to match against the
// component url from log stats.
let moniker = event
.get_property("moniker")
.and_then(|prop| prop.string())
.ok_or(format_err!("Missing moniker"))?;
let last_slash_index = moniker.rfind("/");
if let Some(i) = last_slash_index {
let last_colon_index = moniker.rfind(":");
if let Some(j) = last_colon_index {
let time = event
.get_property("@time")
.and_then(|prop| prop.uint())
.ok_or(format_err!("Missing @time"))?;
res.insert(moniker[i + 1..j].into(), *time as i64);
}
}
}
Ok(res)
}
}
impl fmt::Display for LogStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut output_str = String::new();
// Number of fatal logs is expected to be zero. If that's not the case, report it here.
for stats in &self.stats_list {
if stats.get_count(LogSeverity::FATAL) != 0 {
output_str.push_str(&format!(
"Found {} fatal log messages for component {}\n",
stats.get_count(LogSeverity::FATAL),
stats.component_url
));
}
}
let min_severity_int = self.min_severity() as usize;
let max_severity_int = LogSeverity::ERROR as usize;
let severity_strs = vec!["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
for i in (min_severity_int..=max_severity_int).rev() {
output_str.push_str(&format!("{:<7}", severity_strs[i]));
}
output_str.push_str(&format!("{:<7}{:<10}{}\n", "Total", "ERROR/h", "Component"));
let now = zx::Time::get_monotonic().into_nanos() / 1_000_000_000;
for stats in &self.stats_list {
for i in (min_severity_int..=max_severity_int).rev() {
output_str.push_str(&format!("{:<7}", stats.log_counts[i]));
}
output_str.push_str(&format!("{:<7}", stats.total_logs));
output_str.push_str(&format!("{:<10.4}", stats.error_rate(now)));
output_str.push_str(&format!("{}\n", stats.short_name));
}
write!(f, "{}", output_str)
}
}