use std::collections::VecDeque;
use crate::docker::StatsSample;
pub const MAX_LOG_LINES: usize = 2_000;
pub const MAX_SAMPLES: usize = 60;
pub struct Live {
pub container_id: String,
pub logs: LogBuffer,
pub stats: VecDeque<StatsSample>,
}
impl Live {
pub fn new(container_id: String) -> Self {
Self {
container_id,
logs: LogBuffer::default(),
stats: VecDeque::with_capacity(MAX_SAMPLES),
}
}
pub fn push_stats(&mut self, sample: StatsSample) {
if self.stats.len() == MAX_SAMPLES {
self.stats.pop_front();
}
self.stats.push_back(sample);
}
}
#[derive(Default)]
pub struct LogBuffer {
lines: VecDeque<String>,
open: bool,
}
impl LogBuffer {
pub fn push(&mut self, chunk: &str) {
let mut pieces = chunk.split('\n');
if let Some(first) = pieces.next() {
match self.lines.back_mut() {
Some(last) if self.open => last.push_str(first.trim_end_matches('\r')),
_ => self
.lines
.push_back(first.trim_end_matches('\r').to_owned()),
}
}
for piece in pieces {
self.lines
.push_back(piece.trim_end_matches('\r').to_owned());
}
self.open = !chunk.ends_with('\n');
if !self.open && self.lines.back().is_some_and(String::is_empty) {
self.lines.pop_back();
}
while self.lines.len() > MAX_LOG_LINES {
self.lines.pop_front();
}
}
pub fn lines(&self) -> impl Iterator<Item = &str> {
self.lines.iter().map(String::as_str)
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(buf: &LogBuffer) -> Vec<&str> {
buf.lines().collect()
}
#[test]
fn joins_lines_split_across_chunks() {
let mut buf = LogBuffer::default();
buf.push("hello wo");
buf.push("rld\nsecond\n");
buf.push("third\r\n");
assert_eq!(lines(&buf), vec!["hello world", "second", "third"]);
}
#[test]
fn keeps_only_the_newest_lines() {
let mut buf = LogBuffer::default();
for i in 0..MAX_LOG_LINES + 10 {
buf.push(&format!("line {i}\n"));
}
assert_eq!(buf.len(), MAX_LOG_LINES);
assert_eq!(buf.lines().next(), Some("line 10"));
}
#[test]
fn keeps_a_minute_of_stats() {
let mut live = Live::new("x".into());
for _ in 0..MAX_SAMPLES + 5 {
live.push_stats(StatsSample::default());
}
assert_eq!(live.stats.len(), MAX_SAMPLES);
}
}