blob: 20ecad60836576891c5ce613bd763c2ee8cfe86a [file]
// 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::{anyhow, Context, Result},
async_std::{
fs::{create_dir_all, read_dir, remove_dir_all, remove_file, File, OpenOptions},
io::{BufReader, Lines},
path::PathBuf,
prelude::*,
stream::{Stream, StreamExt},
sync::{Arc, RwLock, RwLockWriteGuard},
task::Poll,
},
async_trait::async_trait,
diagnostics_data::Timestamp,
ffx_config::get,
ffx_log_data::LogEntry,
fidl_fuchsia_developer_bridge::StreamMode,
futures::{FutureExt, TryStreamExt},
std::convert::TryInto,
std::io::ErrorKind,
std::iter::Iterator,
std::pin::Pin,
};
const CACHE_DIRECTORY_CONFIG: &str = "proactive_log.cache_directory";
const MAX_LOG_SIZE_CONFIG: &str = "proactive_log.max_log_size_bytes";
const MAX_SESSION_SIZE_CONFIG: &str = "proactive_log.max_session_size_bytes";
const MAX_SESSIONS_CONFIG: &str = "proactive_log.max_sessions_per_target";
struct LogFileEntries {
lines: Lines<BufReader<Box<File>>>,
}
impl LogFileEntries {
fn new(lines: Lines<BufReader<Box<File>>>) -> Self {
Self { lines }
}
}
impl Stream for LogFileEntries {
type Item = Result<LogEntry>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut futures::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.lines).poll_next(cx).map(|line_opt| {
line_opt.map(|line| match line {
Ok(line) => serde_json::from_str(&line).map_err(|e| anyhow!(e)),
Err(e) => Err(anyhow!(e)),
})
})
}
}
async fn sort_directory(parent: &PathBuf) -> Result<Vec<PathBuf>> {
let mut reader = read_dir(parent.clone()).await?;
let mut result = vec![];
while let Some(ent) = reader.try_next().await? {
let fname_num: u64 = match String::from(ent.file_name().to_string_lossy()).parse() {
Ok(name) => name,
Err(_) => continue,
};
result.push((fname_num, ent));
}
result.sort_by_key(|tup| tup.0);
Ok(result.iter().map(|tup| tup.1.path()).collect())
}
#[derive(Debug)]
struct LogFile {
path: PathBuf,
parent: TargetSessionDirectory,
file: Option<Box<File>>,
}
impl LogFile {
fn new(path: PathBuf, parent: TargetSessionDirectory) -> Self {
Self { path, parent, file: None }
}
fn from_file(path: PathBuf, parent: TargetSessionDirectory, file: Box<File>) -> Self {
Self { path, parent, file: Some(file) }
}
async fn create(parent: TargetSessionDirectory) -> Result<Self> {
let fname = match parent.latest_file().await? {
Some(f) => f.parsed_name()? + 1,
None => 1,
};
let mut file_path = parent.to_path_buf();
file_path.push(fname.to_string());
let mut options = OpenOptions::new();
options.create(true).write(true);
let f = options
.open(file_path.clone())
.await
.context("opening output file")
.map(|f| Box::new(f))?;
Ok(Self::from_file(file_path, parent.clone(), f))
}
fn parsed_name(&self) -> Result<u64> {
Ok(String::from(
self.path
.file_name()
.ok_or(anyhow!("invalid path name {:?}", &self.path.to_str()))?
.to_string_lossy(),
)
.parse()?)
}
async fn get_file(&mut self) -> Result<&mut Box<File>> {
if self.file.is_none() {
let mut options = OpenOptions::new();
options.append(true);
self.file = Some(
options
.open(self.path.clone())
.await
.context("opening output file")
.map(|f| Box::new(f))?,
);
}
Ok(self.file.as_mut().unwrap())
}
/// Utility function for immediately opening the underlying file
/// for this LogFile.
pub async fn force_open(&mut self) -> Result<()> {
self.get_file().await.map(|_| ())
}
async fn bytes_in_file(&mut self) -> Result<usize> {
Ok(self.get_file().await?.metadata().await?.len().try_into()?)
}
async fn remove(&mut self) -> Result<()> {
remove_file(self.path.clone()).await?;
self.file = None;
Ok(())
}
async fn write_entries(&mut self, entries: Vec<&'_ Vec<u8>>) -> Result<()> {
let f = self.get_file().await?;
for entry in entries.iter() {
f.write_all(entry.as_slice()).await?;
}
f.sync_data().await?;
Ok(())
}
async fn stream_entries(&self) -> Result<LogFileEntries> {
Ok(LogFileEntries::new(
BufReader::new(Box::new(File::open(self.path.clone()).await?)).lines(),
))
}
}
#[derive(Clone, Debug)]
pub struct TargetLogDirectory {
root: PathBuf,
}
impl TargetLogDirectory {
async fn new(nodename: String) -> Result<Self> {
let mut root = get::<std::path::PathBuf, &str>(CACHE_DIRECTORY_CONFIG).await?;
root.push(nodename);
Ok(Self { root: root.into() })
}
#[cfg(test)]
fn new_with_root(root_dir: PathBuf, nodename: String) -> Self {
let mut root = root_dir.clone();
root.push(nodename);
Self { root }
}
fn with_session(&self, timestamp_millis: usize) -> TargetSessionDirectory {
TargetSessionDirectory::new(self.clone(), timestamp_millis)
}
pub async fn clean_sessions(&self, max_sessions: usize) -> Result<()> {
let entries = sort_directory(&self.root).await?;
if entries.len() > max_sessions {
let end = entries.len() - max_sessions;
let entries_to_remove = &entries[..end];
for path in entries_to_remove.iter() {
log::info!("[logger] garbage collecting log directory: {:?}", &path);
remove_dir_all(path).await?;
}
}
Ok(())
}
fn to_path_buf(&self) -> PathBuf {
self.root.clone()
}
}
#[derive(Clone, Debug)]
struct TargetSessionDirectory {
target_root: TargetLogDirectory,
full_path: PathBuf,
}
impl TargetSessionDirectory {
fn new(target_root: TargetLogDirectory, timestamp_millis: usize) -> Self {
let mut pb = target_root.to_path_buf();
pb.push(timestamp_millis.to_string());
Self { target_root, full_path: pb }
}
async fn sort_entries(&self) -> Result<Vec<LogFile>> {
Ok(sort_directory(&self.full_path)
.await?
.iter()
.map(|p| LogFile::new(p.clone(), self.clone()))
.collect())
}
fn parent(&self) -> TargetLogDirectory {
self.target_root.clone()
}
async fn latest_file(&self) -> Result<Option<LogFile>> {
self.sort_entries().await.map(|v| v.into_iter().rev().next())
}
async fn create_file(&self) -> Result<LogFile> {
LogFile::create(self.clone()).await
}
fn to_path_buf(&self) -> PathBuf {
self.full_path.clone()
}
}
struct CachedSessionStream {
file_iter: Option<LogFileEntries>,
chunks: Vec<LogFile>,
index: usize,
end_timestamp: Option<Timestamp>,
finished: bool,
}
impl CachedSessionStream {
async fn new(
session_dir: TargetSessionDirectory,
end_timestamp: Option<Timestamp>,
stream_mode: StreamMode,
) -> Result<Self> {
let mut entries = session_dir.sort_entries().await?;
if stream_mode == StreamMode::SnapshotRecentThenSubscribe && entries.len() > 2 {
entries.drain(0..entries.len() - 2);
}
// Force every LogFile to cache its underlying FS file. This allows to continue
// streaming data even if the files get garbage collected during iteration.
futures::future::join_all(entries.iter_mut().map(|e| e.force_open())).await;
Ok(Self { chunks: entries, index: 0, file_iter: None, finished: false, end_timestamp })
}
pub async fn iter(&mut self) -> Result<Option<Result<LogEntry>>> {
if self.finished {
return Ok(None);
}
loop {
if self.file_iter.is_some() {
let val = self.file_iter.as_mut().unwrap().next().await;
if val.is_some() {
let entry = val.unwrap();
if !entry.is_ok() {
return Ok(Some(entry));
}
let entry = entry.unwrap();
if let Some(ts) = self.end_timestamp {
if entry.timestamp > ts {
self.finished = true;
return Ok(None);
}
}
return Ok(Some(Ok(entry)));
} else {
self.file_iter = None;
self.index += 1;
}
}
if let Some(chunk) = self.chunks.get(self.index) {
let iterator = chunk.stream_entries().await?;
self.file_iter = Some(iterator);
continue;
} else {
self.finished = true;
return Ok(None);
}
}
}
}
pub struct SessionStream {
cache_stream: CachedSessionStream,
cache_read_finished: bool,
// We hold onto the Sender in order to create the receiver only at the point
// that we've finished iterating through the on-disk cache. This prevents
// duplicating any log entries.
read_stream: Arc<mpmc::Sender<LogEntry>>,
read_receiver: Option<mpmc::Receiver<LogEntry>>,
stream_mode: StreamMode,
}
impl SessionStream {
async fn new(
session_dir: TargetSessionDirectory,
end_timestamp: Option<Timestamp>,
stream_mode: StreamMode,
read_stream: Arc<mpmc::Sender<LogEntry>>,
) -> Result<Self> {
// In the case of a Subscribe only stream_mode, we can create the receiver immediately
// because we have no risk of duplicating on-disk log entries.
let receiver = if stream_mode == StreamMode::Subscribe {
Some(read_stream.new_receiver())
} else {
None
};
Ok(Self {
cache_stream: CachedSessionStream::new(session_dir, end_timestamp, stream_mode).await?,
cache_read_finished: false,
read_receiver: receiver,
read_stream,
stream_mode: stream_mode,
})
}
pub async fn iter(&mut self) -> Result<Option<Result<LogEntry>>> {
if self.stream_mode != StreamMode::Subscribe && !self.cache_read_finished {
let res = self.cache_stream.iter().await;
if let Ok(opt) = res {
if opt.is_some() {
return Ok(opt);
} else {
self.cache_read_finished = true;
}
} else {
return res;
}
}
if self.stream_mode != StreamMode::SnapshotAll {
if self.read_receiver.is_none() {
self.read_receiver = Some(self.read_stream.new_receiver());
}
let res = self
.read_receiver
.as_mut()
.unwrap()
.next()
.map(|log_opt| Ok(log_opt.map(|l| Ok(l))))
.await;
return res;
} else {
return Ok(None);
}
}
}
#[derive(Default)]
struct DiagnosticsStreamerInner {
output_dir: Option<TargetSessionDirectory>,
current_file: Option<LogFile>,
max_file_size_bytes: usize,
max_session_size_bytes: usize,
max_num_sessions: usize,
read_stream: Arc<mpmc::Sender<LogEntry>>,
}
impl Clone for DiagnosticsStreamerInner {
fn clone(&self) -> Self {
Self {
output_dir: self.output_dir.clone(),
current_file: None,
max_file_size_bytes: self.max_file_size_bytes,
max_session_size_bytes: self.max_session_size_bytes,
max_num_sessions: self.max_num_sessions,
read_stream: self.read_stream.clone(),
}
}
}
pub struct DiagnosticsStreamer {
inner: RwLock<DiagnosticsStreamerInner>,
}
impl Clone for DiagnosticsStreamer {
fn clone(&self) -> Self {
let inner = futures::executor::block_on(self.inner.read());
Self { inner: RwLock::new(inner.clone()) }
}
}
impl Default for DiagnosticsStreamer {
fn default() -> Self {
Self { inner: RwLock::new(DiagnosticsStreamerInner::default()) }
}
}
#[async_trait]
pub trait GenericDiagnosticsStreamer {
async fn setup_stream(
&self,
target_nodename: String,
target_boot_time_nanos: u64,
) -> Result<()>;
async fn append_logs(&self, entries: Vec<LogEntry>) -> Result<()>;
async fn read_most_recent_timestamp(&self) -> Result<Option<Timestamp>>;
async fn clean_sessions_for_target(&self) -> Result<()>;
async fn stream_entries(&self, stream_mode: StreamMode) -> Result<SessionStream>;
}
#[async_trait::async_trait]
impl GenericDiagnosticsStreamer for DiagnosticsStreamer {
async fn setup_stream(
&self,
target_nodename: String,
target_boot_time_nanos: u64,
) -> Result<()> {
self.setup_stream_with_config(
TargetLogDirectory::new(target_nodename).await?,
target_boot_time_nanos,
get::<u64, &str>(MAX_LOG_SIZE_CONFIG).await? as usize,
get::<u64, &str>(MAX_SESSION_SIZE_CONFIG).await? as usize,
get::<u64, &str>(MAX_SESSIONS_CONFIG).await? as usize,
)
.await
}
async fn append_logs(&self, raw_entries: Vec<LogEntry>) -> Result<()> {
let entries = raw_entries
.iter()
.filter_map(|entry| match serde_json::to_string(entry) {
Ok(s) => {
let mut owned = s.clone();
owned.push('\n');
Some(owned.into_bytes())
}
Err(e) => {
log::warn!("failed to serialize LogEntry: {:?}. Log was: {:?}", e, &entry);
None
}
})
.collect::<Vec<_>>();
let mut inner = self.inner.write().await;
let max_file_size = inner.max_file_size_bytes;
let parent = inner.output_dir.as_ref().context("no stream setup")?.clone();
let mut file = match inner.current_file.take() {
Some(f) => f,
None => {
// Continue appending to the latest file for this session if one exists.
let latest_file = parent.latest_file().await?;
match latest_file {
Some(f) => f,
None => parent.create_file().await?,
}
}
};
let mut buf_bytes = file.bytes_in_file().await?;
let mut buf: Vec<&Vec<u8>> = vec![];
for log_bytes in entries.as_slice() {
// We flush the buffer to disk if this log entry would push us over the max file size
// or if this log entry is, alone, larger the max file size (rather than discard the log entry)
if !buf.is_empty()
&& (buf_bytes + log_bytes.len() > max_file_size || log_bytes.len() > max_file_size)
{
file.write_entries(buf).await?;
buf_bytes = 0;
buf = vec![];
file = parent.create_file().await?;
}
buf_bytes += log_bytes.len();
buf.push(log_bytes);
}
if !buf.is_empty() {
// We don't need to create a new file here in the special case that
// the buffer exceeds the max file size, but hasn't been
// flushed yet (i.e. we got a single log entry > max_file_size)
if buf_bytes > max_file_size && file.bytes_in_file().await? != 0 {
file = parent.create_file().await?;
}
file.write_entries(buf).await?;
}
inner.current_file = Some(file);
for entry in raw_entries.iter() {
inner.read_stream.send(entry.clone()).await;
}
self.cleanup_logs(inner).await?;
Ok(())
}
async fn read_most_recent_timestamp(&self) -> Result<Option<Timestamp>> {
let inner = self.inner.read().await;
let output_dir = inner.output_dir.as_ref().context("stream not setup")?;
let files = output_dir.sort_entries().await?;
for file in files.iter().rev() {
let entry = file
.stream_entries()
.await?
.filter_map(|l| l.ok())
.map(|l| l.timestamp)
.last()
.await;
if entry.is_some() {
return Ok(entry);
}
}
Ok(None)
}
async fn clean_sessions_for_target(&self) -> Result<()> {
let inner = self.inner.read().await;
inner
.output_dir
.as_ref()
.context("missing output directory")?
.parent()
.clean_sessions(inner.max_num_sessions)
.await
}
async fn stream_entries(&self, stream_mode: StreamMode) -> Result<SessionStream> {
let ts = if stream_mode == StreamMode::SnapshotAll {
Some(self.read_most_recent_timestamp().await?.unwrap_or(Timestamp::from(0u64)))
} else {
None
};
let inner = self.inner.read().await;
let output_dir = inner.output_dir.as_ref().context("stream not setup")?;
SessionStream::new(output_dir.clone(), ts, stream_mode, inner.read_stream.clone()).await
}
}
impl DiagnosticsStreamer {
// This should only be called by tests.
pub(crate) async fn setup_stream_with_config(
&self,
target_root_dir: TargetLogDirectory,
target_boot_time_nanos: u64,
max_file_size_bytes: usize,
max_session_size_bytes: usize,
max_num_sessions: usize,
) -> Result<()> {
// The ticks=>time conversion isn't accurate enough to use units smaller than milliseconds here.
let t = target_boot_time_nanos / 1_000_000;
let session_dir = target_root_dir.with_session(t as usize);
create_dir_all(session_dir.to_path_buf()).await.or_else(|e| match e.kind() {
ErrorKind::AlreadyExists => Ok(()),
_ => Err(e),
})?;
let mut inner = self.inner.write().await;
inner.output_dir.replace(session_dir);
inner.max_file_size_bytes = max_file_size_bytes;
inner.max_session_size_bytes = max_session_size_bytes;
inner.max_num_sessions = max_num_sessions;
Ok(())
}
async fn cleanup_logs(
&self,
inner: RwLockWriteGuard<'_, DiagnosticsStreamerInner>,
) -> Result<()> {
let output_dir = inner.output_dir.as_ref().context("no stream setup")?;
let mut entries = output_dir.sort_entries().await?;
// We approximate the need to garbage collect by multiplying the number of files by
// the max file size, *excluding* the most recent file.
if entries.len() > 1
&& (entries.len() - 1) * inner.max_file_size_bytes > inner.max_session_size_bytes
{
let to_remove = entries.first_mut().unwrap();
log::info!("logger: garbage collecting log file: {:?}", to_remove);
to_remove.remove().await?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use {
super::*,
async_std::{fs::read_to_string, future::timeout},
diagnostics_data::{LogsData, LogsField, Severity},
diagnostics_hierarchy::{DiagnosticsHierarchy, Property},
ffx_log_data::LogData,
std::collections::HashMap,
std::time::Duration,
tempfile::{tempdir, TempDir},
};
const FAKE_DIR_NAME: &str = "fake_logs";
const SMALL_MAX_LOG_SIZE: usize = 10;
const LARGE_MAX_LOG_SIZE: usize = 1_000_000;
const SMALL_MAX_SESSION_SIZE: usize = 11;
const LARGE_MAX_SESSION_SIZE: usize = 1_000_001;
const DEFAULT_MAX_SESSIONS: usize = 1;
const NODENAME: &str = "my-cool-node";
const BOOT_TIME_NANOS: u64 = 123456789000000000;
const BOOT_TIME_MILLIS: u64 = 123456789000;
const TIMESTAMP: u64 = 987654321;
const READ_TIMEOUT_MILLIS: u64 = 100;
async fn collect_logs(path: PathBuf) -> Result<HashMap<String, String>> {
let mut result = HashMap::new();
let mut ent_stream = read_dir(path).await?;
while let Some(f) = ent_stream.next().await {
let f = f?;
result.insert(
String::from(f.file_name().to_string_lossy()),
read_to_string(String::from(f.path().to_str().unwrap())).await?,
);
}
Ok(result)
}
async fn collect_default_logs(dir_path: &PathBuf) -> Result<HashMap<String, String>> {
collect_default_logs_for_session(dir_path, BOOT_TIME_MILLIS).await
}
async fn collect_default_logs_for_session(
dir_path: &PathBuf,
boot_time_millis: u64,
) -> Result<HashMap<String, String>> {
let mut root = dir_path.clone();
root.push(FAKE_DIR_NAME);
root.push(NODENAME);
root.push(boot_time_millis.to_string());
collect_logs(root).await
}
fn make_target_log(msg: String) -> LogsData {
let hierarchy =
DiagnosticsHierarchy::new("root", vec![Property::String(LogsField::Msg, msg)], vec![]);
LogsData::for_logs(
String::from("test/moniker"),
Some(hierarchy),
Timestamp::from(0u64),
String::from("fake-url"),
Severity::Error,
1,
vec![],
)
}
fn make_malformed_log(ts: u64) -> LogEntry {
LogEntry {
data: LogData::MalformedTargetLog("fake log data".to_string()),
version: 1,
timestamp: ts.into(),
}
}
fn make_valid_log(ts: u64, msg: String) -> LogEntry {
LogEntry {
data: LogData::TargetLog(make_target_log(msg)),
version: 1,
timestamp: ts.into(),
}
}
async fn setup_default_streamer_with_temp_and_boot_time(
temp_parent: &TempDir,
boot_time_nanos: u64,
max_log_size: usize,
max_session_size: usize,
max_sessions: usize,
) -> Result<DiagnosticsStreamer> {
let mut root: PathBuf = temp_parent.path().to_path_buf().into();
root.push(FAKE_DIR_NAME.to_string());
let streamer = DiagnosticsStreamer::default();
let target_dir = TargetLogDirectory::new_with_root(root.clone(), NODENAME.to_string());
streamer
.setup_stream_with_config(
target_dir,
boot_time_nanos,
max_log_size,
max_session_size,
max_sessions,
)
.await?;
Ok(streamer)
}
async fn setup_default_streamer_with_temp(
temp_parent: &TempDir,
max_log_size: usize,
max_session_size: usize,
max_sessions: usize,
) -> Result<DiagnosticsStreamer> {
setup_default_streamer_with_temp_and_boot_time(
temp_parent,
BOOT_TIME_NANOS,
max_log_size,
max_session_size,
max_sessions,
)
.await
}
async fn setup_default_streamer(
max_log_size: usize,
max_session_size: usize,
max_sessions: usize,
) -> Result<(TempDir, DiagnosticsStreamer)> {
let temp_parent = tempdir()?;
let streamer = setup_default_streamer_with_temp(
&temp_parent,
max_log_size,
max_session_size,
max_sessions,
)
.await?;
Ok((temp_parent, streamer))
}
async fn verify_logs(mut reader: SessionStream, logs: Vec<LogEntry>) {
for log in logs.iter() {
let item = reader.iter().await.unwrap();
assert!(
item.is_some(),
"expected log {:?} has no corresponding entry in log file",
log
);
assert_eq!(item.unwrap().unwrap(), *log);
}
assert!(reader.iter().await.unwrap().is_none());
}
async fn verify_log(reader: &mut SessionStream, expected: LogEntry) -> Result<()> {
assert_eq!(
timeout(Duration::from_millis(READ_TIMEOUT_MILLIS), reader.iter())
.await??
.context("missing log entry")??,
expected
);
Ok(())
}
async fn verify_times_out(reader: &mut SessionStream) {
let res = timeout(Duration::from_millis(READ_TIMEOUT_MILLIS), reader.iter()).await;
assert!(res.is_err(), "expected timeout, got {:?}", res);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_single_log_exceeds_max_size() -> Result<()> {
let (temp, streamer) = setup_default_streamer(
SMALL_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let root: PathBuf = temp.path().to_path_buf().into();
let log = make_valid_log(TIMESTAMP, "log".to_string());
streamer.append_logs(vec![log.clone()]).await?;
let results = collect_default_logs(&root).await.unwrap();
assert_eq!(results.len(), 1, "{:?}", results);
assert_eq!(
serde_json::from_str::<LogEntry>(results.values().next().unwrap()).unwrap(),
log
);
verify_logs(streamer.stream_entries(StreamMode::SnapshotAll).await?, vec![log]).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_two_logs_exceeds_max_size() -> Result<()> {
let (temp, streamer) = setup_default_streamer(
SMALL_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let root: PathBuf = temp.path().to_path_buf().into();
let log = make_valid_log(TIMESTAMP, "log".to_string());
let log2 = make_valid_log(TIMESTAMP, "log2".to_string());
streamer.append_logs(vec![log.clone(), log2.clone()]).await?;
let results = collect_default_logs(&root).await.unwrap();
assert_eq!(results.len(), 2, "{:?}", results);
let mut values = results.values().collect::<Vec<_>>();
values.sort();
assert_eq!(serde_json::from_str::<LogEntry>(values.get(0).unwrap()).unwrap(), log);
assert_eq!(serde_json::from_str::<LogEntry>(values.get(1).unwrap()).unwrap(), log2);
verify_logs(streamer.stream_entries(StreamMode::SnapshotAll).await?, vec![log, log2]).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_two_logs_with_two_calls_exceeds_max_size() -> Result<()> {
let (temp, streamer) = setup_default_streamer(
SMALL_MAX_LOG_SIZE,
SMALL_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let root: PathBuf = temp.path().to_path_buf().into();
let log = make_valid_log(TIMESTAMP, "log".to_string());
let log2 = make_valid_log(TIMESTAMP, "log2".to_string());
streamer.append_logs(vec![log.clone()]).await?;
streamer.append_logs(vec![log2.clone()]).await?;
let results = collect_default_logs(&root).await.unwrap();
assert_eq!(results.len(), 2, "{:?}", results);
let mut values = results.values().collect::<Vec<_>>();
values.sort();
assert_eq!(serde_json::from_str::<LogEntry>(values.get(0).unwrap()).unwrap(), log);
assert_eq!(serde_json::from_str::<LogEntry>(values.get(1).unwrap()).unwrap(), log2);
verify_logs(streamer.stream_entries(StreamMode::SnapshotAll).await?, vec![log, log2]).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_garbage_collection_doesnt_count_latest_file() -> Result<()> {
let (temp, streamer) =
setup_default_streamer(SMALL_MAX_LOG_SIZE, SMALL_MAX_LOG_SIZE, DEFAULT_MAX_SESSIONS)
.await?;
let root: PathBuf = temp.path().to_path_buf().into();
let log = make_valid_log(TIMESTAMP, "log".to_string());
let log2 = make_valid_log(TIMESTAMP, "log2".to_string());
streamer.append_logs(vec![log.clone()]).await?;
streamer.append_logs(vec![log2.clone()]).await?;
let results = collect_default_logs(&root).await.unwrap();
assert_eq!(results.len(), 2, "{:?}", results);
let mut values = results.values().collect::<Vec<_>>();
values.sort();
assert_eq!(serde_json::from_str::<LogEntry>(values.get(0).unwrap()).unwrap(), log);
assert_eq!(serde_json::from_str::<LogEntry>(values.get(1).unwrap()).unwrap(), log2);
verify_logs(streamer.stream_entries(StreamMode::SnapshotAll).await?, vec![log, log2]).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_two_logs_with_two_calls_exceeds_max_session_size() -> Result<()> {
let (temp, streamer) =
setup_default_streamer(SMALL_MAX_LOG_SIZE, SMALL_MAX_LOG_SIZE, DEFAULT_MAX_SESSIONS)
.await?;
let root: PathBuf = temp.path().to_path_buf().into();
let log = make_valid_log(TIMESTAMP, "log".to_string());
let log2 = make_valid_log(TIMESTAMP, "log2".to_string());
let log3 = make_valid_log(TIMESTAMP, "log3".to_string());
streamer.append_logs(vec![log.clone()]).await?;
streamer.append_logs(vec![log2.clone()]).await?;
// This call should delete the first log file.
streamer.append_logs(vec![log3.clone()]).await?;
let results = collect_default_logs(&root).await.unwrap();
assert_eq!(results.len(), 2, "{:?}", results);
let mut values = results.values().collect::<Vec<_>>();
values.sort();
assert_eq!(serde_json::from_str::<LogEntry>(values.get(0).unwrap()).unwrap(), log2);
assert_eq!(serde_json::from_str::<LogEntry>(values.get(1).unwrap()).unwrap(), log3);
verify_logs(streamer.stream_entries(StreamMode::SnapshotAll).await?, vec![log2, log3])
.await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_new_streamer_appends_to_existing_file() -> Result<()> {
let temp = tempdir()?;
let root: PathBuf = temp.path().to_path_buf().into();
let log = make_valid_log(TIMESTAMP, "log1".to_string());
let log2 = make_valid_log(TIMESTAMP + 1, "log2".to_string());
{
let streamer = setup_default_streamer_with_temp(
&temp,
LARGE_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await
.unwrap();
streamer.append_logs(vec![log.clone()]).await.unwrap();
}
let streamer = setup_default_streamer_with_temp(
&temp,
LARGE_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await
.unwrap();
streamer.append_logs(vec![log2.clone()]).await.unwrap();
let results = collect_default_logs(&root).await.unwrap();
assert_eq!(results.len(), 1, "{:?}", results);
let value = results.values().next().unwrap();
let results = value.split("\n").collect::<Vec<_>>();
assert_eq!(results.len(), 3, "{:?}", results);
assert_eq!(serde_json::from_str::<LogEntry>(results.get(0).unwrap()).unwrap(), log);
assert_eq!(serde_json::from_str::<LogEntry>(results.get(1).unwrap()).unwrap(), log2);
assert!(results.get(2).unwrap().is_empty());
verify_logs(streamer.stream_entries(StreamMode::SnapshotAll).await?, vec![log, log2]).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_snapshot_does_not_include_later_writes() -> Result<()> {
let (_temp, streamer) = setup_default_streamer(
SMALL_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let log = make_valid_log(TIMESTAMP, "log1".to_string());
streamer.append_logs(vec![log.clone()]).await?;
let iterator = streamer.stream_entries(StreamMode::SnapshotAll).await?;
let log2 = make_valid_log(TIMESTAMP + 1, "log2".to_string());
streamer.append_logs(vec![log2.clone()]).await?;
verify_logs(iterator, vec![log.clone()]).await;
verify_logs(streamer.stream_entries(StreamMode::SnapshotAll).await?, vec![log, log2]).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_snapshot_returns_nothing_if_no_logs_at_write_time() -> Result<()> {
let (_temp, streamer) = setup_default_streamer(
SMALL_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let mut iterator = streamer.stream_entries(StreamMode::SnapshotAll).await?;
let log = make_valid_log(TIMESTAMP, "log1".to_string());
streamer.append_logs(vec![log.clone()]).await?;
assert!(iterator.iter().await?.is_none());
verify_logs(streamer.stream_entries(StreamMode::SnapshotAll).await?, vec![log]).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_snapshot_subscribe_intermingled_writes_all_disk() -> Result<()> {
let (_temp, streamer) = setup_default_streamer(
LARGE_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let log1 = make_valid_log(TIMESTAMP, "log1".to_string());
let log2 = make_valid_log(TIMESTAMP + 1, "log2".to_string());
let log3 = make_valid_log(TIMESTAMP + 2, "log3".to_string());
let log4 = make_valid_log(TIMESTAMP + 3, "log4".to_string());
streamer.append_logs(vec![log1.clone(), log2.clone()]).await?;
let mut iterator = streamer.stream_entries(StreamMode::SnapshotAllThenSubscribe).await?;
verify_log(&mut iterator, log1.clone()).await.context(format!("{:?}", log1)).unwrap();
streamer.append_logs(vec![log3.clone()]).await?;
verify_log(&mut iterator, log2.clone()).await.context(format!("{:?}", log2)).unwrap();
verify_log(&mut iterator, log3.clone()).await.context(format!("{:?}", log3)).unwrap();
streamer.append_logs(vec![log4.clone()]).await?;
verify_log(&mut iterator, log4.clone()).await.context(format!("{:?}", log4)).unwrap();
verify_times_out(&mut iterator).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_snapshot_subscribe_intermingled_writes() -> Result<()> {
let (_temp, streamer) = setup_default_streamer(
LARGE_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let log1 = make_valid_log(TIMESTAMP, "log1".to_string());
let log2 = make_valid_log(TIMESTAMP + 1, "log2".to_string());
let log3 = make_valid_log(TIMESTAMP + 2, "log3".to_string());
let log4 = make_valid_log(TIMESTAMP + 3, "log4".to_string());
streamer.append_logs(vec![log1.clone(), log2.clone()]).await?;
let mut iterator = streamer.stream_entries(StreamMode::SnapshotAllThenSubscribe).await?;
verify_log(&mut iterator, log1.clone()).await.context(format!("{:?}", log1)).unwrap();
verify_log(&mut iterator, log2.clone()).await.context(format!("{:?}", log2)).unwrap();
verify_times_out(&mut iterator).await;
streamer.append_logs(vec![log3.clone()]).await?;
verify_log(&mut iterator, log3.clone()).await.context(format!("{:?}", log3)).unwrap();
streamer.append_logs(vec![log4.clone()]).await?;
verify_log(&mut iterator, log4.clone()).await.context(format!("{:?}", log4)).unwrap();
verify_times_out(&mut iterator).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_snapshot_recent_includes_only_recent() -> Result<()> {
let (_temp, streamer) = setup_default_streamer(
SMALL_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let log = make_valid_log(TIMESTAMP, "should not be read".to_string());
let log2 = make_valid_log(TIMESTAMP, "log2".to_string());
let log3 = make_valid_log(TIMESTAMP, "log3".to_string());
let log4 = make_valid_log(TIMESTAMP, "log4".to_string());
streamer.append_logs(vec![log.clone(), log.clone(), log2.clone(), log3.clone()]).await?;
// SnapshotRecent will include only the most recent two chunks
let mut iterator = streamer.stream_entries(StreamMode::SnapshotRecentThenSubscribe).await?;
verify_log(&mut iterator, log2.clone()).await.context(format!("{:?}", log2)).unwrap();
verify_log(&mut iterator, log3.clone()).await.context(format!("{:?}", log3)).unwrap();
verify_times_out(&mut iterator).await;
streamer.append_logs(vec![log4.clone()]).await?;
verify_log(&mut iterator, log4.clone()).await.context(format!("{:?}", log4)).unwrap();
verify_times_out(&mut iterator).await;
let mut iterator = streamer.stream_entries(StreamMode::SnapshotRecentThenSubscribe).await?;
verify_log(&mut iterator, log3.clone()).await.context(format!("{:?}", log3)).unwrap();
verify_log(&mut iterator, log4.clone()).await.context(format!("{:?}", log4)).unwrap();
verify_times_out(&mut iterator).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_subscribe_ignores_on_disk_logs() -> Result<()> {
let (_temp, streamer) = setup_default_streamer(
LARGE_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let log1 = make_valid_log(TIMESTAMP, "log1".to_string());
let log2 = make_valid_log(TIMESTAMP + 1, "log2".to_string());
let log3 = make_valid_log(TIMESTAMP + 2, "log3".to_string());
let log4 = make_valid_log(TIMESTAMP + 3, "log4".to_string());
streamer.append_logs(vec![log1.clone(), log2.clone()]).await?;
let mut iterator = streamer.stream_entries(StreamMode::Subscribe).await?;
streamer.append_logs(vec![log3.clone()]).await?;
verify_log(&mut iterator, log3.clone()).await.context(format!("{:?}", log3)).unwrap();
streamer.append_logs(vec![log4.clone()]).await?;
verify_log(&mut iterator, log4.clone()).await.context(format!("{:?}", log4)).unwrap();
verify_times_out(&mut iterator).await;
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_write_no_setup_call_errors() -> Result<()> {
let streamer = DiagnosticsStreamer::default();
let log = make_malformed_log(TIMESTAMP);
assert!(streamer.append_logs(vec![log]).await.is_err());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_read_no_setup_call_errors() -> Result<()> {
let streamer = DiagnosticsStreamer::default();
assert!(streamer.stream_entries(StreamMode::SnapshotAll).await.is_err());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_simple_read() -> Result<()> {
let (_temp, streamer) = setup_default_streamer(
SMALL_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
let early_log = make_valid_log(TIMESTAMP - 1, String::default());
let log = make_valid_log(TIMESTAMP, String::default());
streamer.append_logs(vec![early_log, log]).await?;
assert_eq!(streamer.read_most_recent_timestamp().await?.unwrap(), TIMESTAMP.into());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_read_timestamp_returns_none_if_no_logs() -> Result<()> {
let (_temp, streamer) = setup_default_streamer(
SMALL_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await?;
assert!(streamer.read_most_recent_timestamp().await?.is_none());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_cleans_old_sessions() -> Result<()> {
let temp = tempdir()?;
let root: PathBuf = temp.path().to_path_buf().into();
let log = make_valid_log(TIMESTAMP, "log".to_string());
let log2 = make_valid_log(TIMESTAMP, "log2".to_string());
let log3 = make_valid_log(TIMESTAMP, "log3".to_string());
{
let streamer = setup_default_streamer_with_temp_and_boot_time(
&temp,
BOOT_TIME_NANOS,
LARGE_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await
.unwrap();
streamer.append_logs(vec![log.clone()]).await.unwrap();
}
{
let streamer = setup_default_streamer_with_temp_and_boot_time(
&temp,
BOOT_TIME_NANOS + 2_000_000,
LARGE_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await
.unwrap();
streamer.append_logs(vec![log2.clone()]).await.unwrap();
}
let streamer = setup_default_streamer_with_temp_and_boot_time(
&temp,
BOOT_TIME_NANOS + 3_000_000,
LARGE_MAX_LOG_SIZE,
LARGE_MAX_SESSION_SIZE,
DEFAULT_MAX_SESSIONS,
)
.await
.unwrap();
streamer.append_logs(vec![log3.clone()]).await.unwrap();
streamer.clean_sessions_for_target().await?;
let mut session1 = root.clone();
session1.push(FAKE_DIR_NAME);
session1.push(NODENAME);
session1.push(BOOT_TIME_MILLIS.to_string());
assert!(!session1.exists().await);
let mut session2 = root.clone();
session2.push(FAKE_DIR_NAME);
session2.push(NODENAME);
session2.push((BOOT_TIME_MILLIS + 2).to_string());
assert!(!session2.exists().await);
let results = collect_default_logs_for_session(&root, BOOT_TIME_MILLIS + 3).await.unwrap();
assert_eq!(results.len(), 1, "{:?}", results);
let mut values = results.values().collect::<Vec<_>>();
values.sort();
assert_eq!(
serde_json::from_str::<LogEntry>(values.get(0).unwrap()).unwrap(),
log3,
"{:?}",
results
);
verify_logs(streamer.stream_entries(StreamMode::SnapshotAll).await?, vec![log3]).await;
Ok(())
}
}