blob: 4095a649792a433480a31b9c9556f4c4799b66cf [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 fidl_fuchsia_diagnostics::{DataType, Severity as FidlSeverity};
use fuchsia_inspect_node_hierarchy::{NodeHierarchy, Property};
use serde::{
self,
de::{DeserializeOwned, Deserializer},
Deserialize, Serialize, Serializer,
};
use std::{
borrow::Borrow,
fmt,
hash::Hash,
ops::{Deref, DerefMut},
str::FromStr,
};
pub use fuchsia_inspect_node_hierarchy::{assert_data_tree, tree_assertion};
const SCHEMA_VERSION: u64 = 1;
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
pub enum DataSource {
Unknown,
Inspect,
LifecycleEvent,
Logs,
}
impl Default for DataSource {
fn default() -> Self {
DataSource::Unknown
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
pub enum LifecycleType {
Started,
Stopped,
Running,
DiagnosticsReady,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
#[serde(untagged)]
pub enum Metadata {
Empty,
Inspect(InspectMetadata),
LifecycleEvent(LifecycleEventMetadata),
Logs(LogsMetadata),
}
impl Default for Metadata {
fn default() -> Self {
Metadata::Empty
}
}
/// A trait implemented by marker types which denote "kinds" of diagnostics data.
pub trait DiagnosticsData {
/// The type of metadata included in results of this type.
type Metadata: DeserializeOwned + Serialize + Clone + Send + 'static;
/// The type of key used for indexing node hierarchies in the payload.
type Key: AsRef<str> + Clone + DeserializeOwned + Eq + FromStr + Hash + Send + 'static;
/// The type of error returned in this metadata.
type Error: Clone;
/// Used to query for this kind of metadata in the ArchiveAccessor.
const DATA_TYPE: DataType;
/// Returns the component URL which generated this value.
fn component_url(metadata: &Self::Metadata) -> &str;
/// Returns the timestamp at which this value was recorded.
fn timestamp(metadata: &Self::Metadata) -> Timestamp;
/// Returns the errors recorded with this value, if any.
fn errors(metadata: &Self::Metadata) -> &Option<Vec<Self::Error>>;
/// Returns whether any errors are recorded on this value.
fn has_errors(metadata: &Self::Metadata) -> bool {
Self::errors(metadata).as_ref().map(|e| !e.is_empty()).unwrap_or_default()
}
}
/// Lifecycle events track the start, stop, and diagnostics directory readiness of components.
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct Lifecycle;
impl DiagnosticsData for Lifecycle {
type Metadata = LifecycleEventMetadata;
type Key = String;
type Error = Error;
const DATA_TYPE: DataType = DataType::Lifecycle;
fn component_url(metadata: &Self::Metadata) -> &str {
&metadata.component_url
}
fn timestamp(metadata: &Self::Metadata) -> Timestamp {
metadata.timestamp
}
fn errors(metadata: &Self::Metadata) -> &Option<Vec<Self::Error>> {
&metadata.errors
}
}
/// Inspect carries snapshots of metrics trees hosted by components.
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct Inspect;
impl DiagnosticsData for Inspect {
type Metadata = InspectMetadata;
type Key = String;
type Error = Error;
const DATA_TYPE: DataType = DataType::Inspect;
fn component_url(metadata: &Self::Metadata) -> &str {
&metadata.component_url
}
fn timestamp(metadata: &Self::Metadata) -> Timestamp {
metadata.timestamp
}
fn errors(metadata: &Self::Metadata) -> &Option<Vec<Self::Error>> {
&metadata.errors
}
}
/// Logs carry streams of structured events from components.
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct Logs;
impl DiagnosticsData for Logs {
type Metadata = LogsMetadata;
type Key = LogsField;
type Error = LogError;
const DATA_TYPE: DataType = DataType::Logs;
fn component_url(metadata: &Self::Metadata) -> &str {
&metadata.component_url
}
fn timestamp(metadata: &Self::Metadata) -> Timestamp {
metadata.timestamp
}
fn errors(metadata: &Self::Metadata) -> &Option<Vec<Self::Error>> {
&metadata.errors
}
}
/// Wraps a time for serialization and deserialization purposes.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Timestamp(u64);
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<usize> for Timestamp {
fn from(nanos: usize) -> Timestamp {
Timestamp(nanos as u64)
}
}
impl From<u64> for Timestamp {
fn from(nanos: u64) -> Timestamp {
Timestamp(nanos)
}
}
impl From<i64> for Timestamp {
fn from(nanos: i64) -> Timestamp {
Timestamp(nanos as u64)
}
}
impl Into<i64> for Timestamp {
fn into(self) -> i64 {
self.0 as _
}
}
#[cfg(target_os = "fuchsia")]
mod zircon {
use super::*;
use fuchsia_zircon as zx;
impl From<zx::Time> for Timestamp {
fn from(t: zx::Time) -> Timestamp {
Timestamp(t.into_nanos() as u64)
}
}
impl Into<zx::Time> for Timestamp {
fn into(self) -> zx::Time {
zx::Time::from_nanos(self.0 as i64)
}
}
}
impl Deref for Timestamp {
type Target = u64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Timestamp {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct LifecycleEventMetadata {
/// Optional vector of errors encountered by platform.
pub errors: Option<Vec<Error>>,
/// Type of lifecycle event being encoded in the payload.
pub lifecycle_event_type: LifecycleType,
/// The url with which the component was launched.
pub component_url: String,
/// Monotonic time in nanos.
pub timestamp: Timestamp,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct InspectMetadata {
/// Optional vector of errors encountered by platform.
pub errors: Option<Vec<Error>>,
/// Name of diagnostics file producing data.
pub filename: String,
/// The url with which the component was launched.
pub component_url: String,
/// Monotonic time in nanos.
pub timestamp: Timestamp,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct LogsMetadata {
// TODO(fxbug.dev/58369) figure out exact spelling of pid/tid context and severity
/// Optional vector of errors encountered by platform.
pub errors: Option<Vec<LogError>>,
/// The url with which the component was launched.
pub component_url: String,
/// Monotonic time in nanos.
pub timestamp: Timestamp,
/// Severity of the message.
pub severity: Severity,
/// Size of the original message on the wire, in bytes.
pub size_bytes: usize,
}
/// Severities a log message can have, often called the log's "level".
// NOTE: this is only duplicated because we can't get Serialize/Deserialize on the FIDL type
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub enum Severity {
/// Trace records include detailed information about program execution.
Trace,
/// Debug records include development-facing information about program execution.
Debug,
/// Info records include general information about program execution. (default)
Info,
/// Warning records include information about potentially problematic operations.
Warn,
/// Error records include information about failed operations.
Error,
/// Fatal records convey information about operations which cause a program's termination.
Fatal,
}
impl From<FidlSeverity> for Severity {
fn from(severity: FidlSeverity) -> Self {
match severity {
FidlSeverity::Trace => Severity::Trace,
FidlSeverity::Debug => Severity::Debug,
FidlSeverity::Info => Severity::Info,
FidlSeverity::Warn => Severity::Warn,
FidlSeverity::Error => Severity::Error,
FidlSeverity::Fatal => Severity::Fatal,
}
}
}
/// An instance of diagnostics data with typed metadata and an optional nested payload.
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct Data<D: DiagnosticsData> {
/// The source of the data.
#[serde(default)]
// TODO(fxbug.dev/58033) remove this once the Metadata enum is gone everywhere
pub data_source: DataSource,
/// The metadata for the diagnostics payload.
#[serde(bound(
deserialize = "D::Metadata: DeserializeOwned",
serialize = "D::Metadata: Serialize"
))]
pub metadata: D::Metadata,
/// Moniker of the component that generated the payload.
pub moniker: String,
/// Payload containing diagnostics data, if the payload exists, else None.
pub payload: Option<NodeHierarchy<D::Key>>,
/// Schema version.
#[serde(default)]
pub version: u64,
}
pub type InspectData = Data<Inspect>;
pub type LifecycleData = Data<Lifecycle>;
pub type LogsData = Data<Logs>;
pub type LogsHierarchy = NodeHierarchy<LogsField>;
pub type LogsProperty = Property<LogsField>;
impl Data<Lifecycle> {
/// Creates a new data instance for a lifecycle event.
pub fn for_lifecycle_event(
moniker: impl Into<String>,
lifecycle_event_type: LifecycleType,
payload: Option<NodeHierarchy>,
component_url: impl Into<String>,
timestamp: impl Into<Timestamp>,
errors: Vec<Error>,
) -> LifecycleData {
let errors_opt = if errors.is_empty() { None } else { Some(errors) };
Data {
moniker: moniker.into(),
version: SCHEMA_VERSION,
data_source: DataSource::LifecycleEvent,
payload,
metadata: LifecycleEventMetadata {
timestamp: timestamp.into(),
component_url: component_url.into(),
lifecycle_event_type,
errors: errors_opt,
},
}
}
}
impl Data<Inspect> {
/// Creates a new data instance for inspect.
pub fn for_inspect(
moniker: impl Into<String>,
inspect_hierarchy: Option<NodeHierarchy>,
timestamp_nanos: impl Into<Timestamp>,
component_url: impl Into<String>,
filename: impl Into<String>,
errors: Vec<Error>,
) -> InspectData {
let errors_opt = if errors.is_empty() { None } else { Some(errors) };
Data {
moniker: moniker.into(),
version: SCHEMA_VERSION,
data_source: DataSource::Inspect,
payload: inspect_hierarchy,
metadata: InspectMetadata {
timestamp: timestamp_nanos.into(),
component_url: component_url.into(),
filename: filename.into(),
errors: errors_opt,
},
}
}
}
impl Data<Logs> {
/// Creates a new data instance for logs.
pub fn for_logs(
moniker: impl Into<String>,
payload: Option<LogsHierarchy>,
timestamp_nanos: impl Into<Timestamp>,
component_url: impl Into<String>,
severity: impl Into<Severity>,
size_bytes: usize,
errors: Vec<LogError>,
) -> Self {
let errors = if errors.is_empty() { None } else { Some(errors) };
Data {
moniker: moniker.into(),
version: SCHEMA_VERSION,
data_source: DataSource::Logs,
payload,
metadata: LogsMetadata {
timestamp: timestamp_nanos.into(),
component_url: component_url.into(),
severity: severity.into(),
size_bytes,
errors,
},
}
}
/// Returns the string log associated with the message, if one exists.
pub fn msg(&self) -> Option<&str> {
self.payload
.as_ref()
.map(|p| {
p.properties
.iter()
.filter_map(|property| match property {
LogsProperty::String(LogsField::Msg, msg) => Some(msg.as_str()),
_ => None,
})
.next()
})
.flatten()
}
}
/// An enum containing well known argument names passed through logs, as well
/// as an `Other` variant for any other argument names.
///
/// This contains the fields of logs sent as a [`LogMessage`].
///
/// [`LogMessage`]: https://fuchsia.dev/reference/fidl/fuchsia.logger#LogMessage
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
pub enum LogsField {
ProcessId,
ThreadId,
Dropped,
Tag,
Verbosity,
Msg,
Other(String),
}
// TODO(fxbug.dev/50519) - ensure that strings reported here align with naming
// decisions made for the structured log format sent by other components.
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: &str = "message";
pub const VERBOSITY_LABEL: &str = "verbosity";
impl LogsField {
pub fn is_legacy(&self) -> bool {
matches!(
self,
LogsField::ProcessId
| LogsField::ThreadId
| LogsField::Dropped
| LogsField::Tag
| LogsField::Msg
| LogsField::Verbosity
)
}
}
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::Verbosity => VERBOSITY_LABEL,
Self::Other(str) => str.as_str(),
}
}
}
impl<T> From<T> for LogsField
where
// Deref instead of AsRef b/c LogsField: AsRef<str> so this conflicts with concrete From<Self>
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,
VERBOSITY_LABEL => Self::Verbosity,
TAG_LABEL => Self::Tag,
MESSAGE_LABEL => Self::Msg,
_ => 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 {
DroppedLogs { count: u64 },
Other(Error),
}
#[derive(Debug, PartialEq, Clone, Eq)]
pub struct Error {
pub message: String,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Borrow<str> for Error {
fn borrow(&self) -> &str {
&self.message
}
}
impl Serialize for Error {
fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
self.message.serialize(ser)
}
}
impl<'de> Deserialize<'de> for Error {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let message = String::deserialize(de)?;
Ok(Error { message })
}
}
impl Metadata {
pub fn inspect(&self) -> Option<&InspectMetadata> {
match self {
Metadata::Inspect(m) => Some(m),
_ => None,
}
}
pub fn lifecycle_event(&self) -> Option<&LifecycleEventMetadata> {
match self {
Metadata::LifecycleEvent(m) => Some(m),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fuchsia_inspect_node_hierarchy::Property;
use pretty_assertions;
use serde_json::json;
const TEST_URL: &'static str = "fuchsia-pkg://test";
#[test]
fn test_canonical_json_inspect_formatting() {
let mut hierarchy = NodeHierarchy::new(
"root",
vec![Property::String("x".to_string(), "foo".to_string())],
vec![],
);
hierarchy.sort();
let json_schema = Data::for_inspect(
"a/b/c/d",
Some(hierarchy),
123456u64,
TEST_URL,
"test_file_plz_ignore.inspect",
Vec::new(),
);
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,
"errors": null,
"filename": "test_file_plz_ignore.inspect",
"timestamp": 123456,
}
});
pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
}
#[test]
fn test_errorful_json_inspect_formatting() {
let json_schema = Data::for_inspect(
"a/b/c/d",
None,
123456u64,
TEST_URL,
"test_file_plz_ignore.inspect",
vec![Error { message: "too much fun being had.".to_string() }],
);
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.");
}
#[test]
fn test_canonical_json_lifecycle_event_formatting() {
let json_schema = Data::for_lifecycle_event(
"a/b/c/d",
LifecycleType::DiagnosticsReady,
None,
TEST_URL,
123456u64,
Vec::new(),
);
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": "LifecycleEvent",
"payload": null,
"metadata": {
"component_url": TEST_URL,
"errors": null,
"lifecycle_event_type": "DiagnosticsReady",
"timestamp": 123456,
}
});
pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
}
#[test]
fn test_errorful_json_lifecycle_event_formatting() {
let json_schema = Data::for_lifecycle_event(
"a/b/c/d",
LifecycleType::DiagnosticsReady,
None,
TEST_URL,
123456u64,
vec![Error { message: "too much fun being had.".to_string() }],
);
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": "LifecycleEvent",
"payload": null,
"metadata": {
"errors": ["too much fun being had."],
"lifecycle_event_type": "DiagnosticsReady",
"component_url": TEST_URL,
"timestamp": 123456,
}
});
pretty_assertions::assert_eq!(result_json, expected_json, "golden diff failed.");
}
}