blob: 7a87cb53a27a2b27d966324437cb99792c876565 [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};
use glob::glob;
use persistence_config::{ServiceName, Tag};
use serde::{ser::SerializeMap, Serialize, Serializer};
use serde_json::Value;
use std::{collections::HashMap, fs};
use tracing::{info, warn};
const CURRENT_PATH: &str = "/cache/current";
const PREVIOUS_PATH: &str = "/cache/previous";
pub(crate) struct PersistSchema {
pub timestamps: Timestamps,
pub payload: PersistPayload,
}
pub(crate) enum PersistPayload {
Data(PersistData),
Error(String),
}
pub(crate) struct PersistData {
pub data_length: usize,
pub entries: HashMap<String, Value>,
}
#[derive(Clone, Serialize)]
pub(crate) struct Timestamps {
pub before_monotonic: i64,
pub before_utc: i64,
pub after_monotonic: i64,
pub after_utc: i64,
}
// Keys for JSON per-tag metadata to be persisted and published
const TIMESTAMPS_KEY: &str = "@timestamps";
const SIZE_KEY: &str = "@persist_size";
const ERROR_KEY: &str = ":error";
const ERROR_DESCRIPTION_KEY: &str = "description";
impl Serialize for PersistSchema {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match &self.payload {
PersistPayload::Data(data) => {
let mut s = serializer.serialize_map(Some(data.entries.len() + 2))?;
s.serialize_entry(TIMESTAMPS_KEY, &self.timestamps)?;
s.serialize_entry(SIZE_KEY, &data.data_length)?;
for (k, v) in data.entries.iter() {
s.serialize_entry(k, v)?;
}
s.end()
}
PersistPayload::Error(error) => {
let mut s = serializer.serialize_map(Some(2))?;
s.serialize_entry(TIMESTAMPS_KEY, &self.timestamps)?;
s.serialize_entry(ERROR_KEY, &ErrorHelper(&error))?;
s.end()
}
}
}
}
impl PersistSchema {
pub(crate) fn error(timestamps: Timestamps, description: String) -> Self {
Self { timestamps, payload: PersistPayload::Error(description) }
}
}
struct ErrorHelper<'a>(&'a str);
impl<'a> Serialize for ErrorHelper<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_map(Some(1))?;
s.serialize_entry(ERROR_DESCRIPTION_KEY, self.0)?;
s.end()
}
}
// Throw away stuff from two boots ago. Move stuff in the "current"
// directory to the "previous" directory.
pub fn shuffle_at_boot() {
// These may fail if /cache was wiped. This is WAI and should not signal an error.
fs::remove_dir_all(PREVIOUS_PATH)
.map_err(|e| info!("Could not delete {}: {:?}", PREVIOUS_PATH, e))
.ok();
fs::rename(CURRENT_PATH, PREVIOUS_PATH)
.map_err(|e| info!("Could not move {} to {}: {:?}", CURRENT_PATH, PREVIOUS_PATH, e))
.ok();
}
// Write a VMO's contents to the appropriate file.
pub(crate) fn write(service_name: &ServiceName, tag: &Tag, data: &PersistSchema) {
// /cache/ may be deleted any time. It's OK to try to create CURRENT_PATH if it already exists.
let path = format!("{}/{}", CURRENT_PATH, service_name);
fs::create_dir_all(&path)
.map_err(|e| warn!("Could not create directory {}: {:?}", path, e))
.ok();
let data = match serde_json::to_string(data) {
Ok(data) => data,
Err(e) => {
warn!("Could not serialize data - unexpected error {e}");
return;
}
};
fs::write(&format!("{}/{}", path, tag), data)
.map_err(|e| warn!("Could not write file {}/{}: {:?}", path, tag, e))
.ok();
}
// All the names in the previous-boot directory.
// TODO(https://fxbug.dev/42150693): If this gets big, use Lazy Inspect.
pub(crate) fn remembered_data() -> Result<Vec<(String, Vec<(String, String)>)>, Error> {
// Counter for number of tags successfully retrieved. If no persisted tags were
// retrieved, this method returns an error.
let mut tags_retrieved = 0;
let mut service_entries = Vec::new();
// Create an iterator over all subdirectories of /cache/previous
// which contains persisted data from the last boot.
for service_path in glob(&format!("{}/*", PREVIOUS_PATH))
.map_err(|e| format_err!("Failed to read previous-path glob pattern: {:?}", e))?
{
match service_path {
Err(e) => {
// If our glob pattern was valid, but we encountered glob errors while iterating, just warn
// since there may still be some persisted metrics.
warn!(
"Encountered GlobError; contents could not be read to determine if glob pattern was matched: {:?}",
e
)
}
Ok(path) => {
if let Some(name) = path.file_name() {
let service_name = name.to_string_lossy().to_string();
let mut tag_entries = Vec::new();
for tag_path in
glob(&format!("{}/{}/*", PREVIOUS_PATH, service_name)).map_err(|e| {
format_err!(
"Failed to read previous service persistence pattern: {:?}",
e
)
})?
{
match tag_path {
Ok(path) => {
if let Some(tag_name) = path.file_name() {
let tag_name = tag_name.to_string_lossy().to_string();
match fs::read(path.clone()) {
Ok(text) => {
// TODO(cphoenix): We want to encode failures at retrieving persisted
// metrics in the inspect hierarchy so clients know why their data is
// missing.
match std::str::from_utf8(&text) {
Ok(contents) => {
tags_retrieved += 1;
tag_entries
.push((tag_name, contents.to_owned()));
}
Err(e) => {
warn!(
"Failed to parse persisted bytes at path: {:?} into text: {:?}",
path, e
);
}
}
}
Err(e) => {
warn!(
"Failed to retrieve text persisted at path: {:?}: {:?}",
path, e
);
}
}
}
}
Err(e) => {
// If our glob pattern was valid, but we encountered glob errors while iterating, just warn
// since there may still be some persisted metrics.
warn!(
"Encountered GlobError; contents could not be read to determine if glob pattern was matched: {:?}",
e
)
}
}
}
service_entries.push((service_name, tag_entries));
}
}
};
}
if tags_retrieved == 0 {
info!("No persisted data was successfully retrieved.");
}
Ok(service_entries)
}