| // 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) |
| } |
| } |