use schemars::JsonSchema;
use serde::Serialize;
use shep_core::barks::{Bark, SinkOutcome};
use shep_core::protocol::{DogSource, ExitInfo, Lamb, ProcessInfo};
use crate::dog::metrics::HostReading;
#[derive(Debug, Serialize, JsonSchema)]
pub struct FlockListing {
pub flock: Vec<SheepRow>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct BarkListing {
pub barks: Vec<BarkRow>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct SheepRow {
pub id: u32,
pub name: String,
pub status: String,
pub pid: Option<u32>,
pub restarts: u32,
pub uptime_ms: u64,
pub fold: Option<String>,
pub out_file: Option<String>,
pub err_file: Option<String>,
pub cpu_percent: Option<f32>,
pub memory_bytes: Option<u64>,
pub dog: Option<DogRow>,
pub lambs: Option<Vec<LambRow>>,
pub last_exit: Option<ExitInfoRow>,
pub smit: Option<String>,
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DogRow {
BuiltIn,
Adopted {
path: String,
},
Unknown,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct LambRow {
pub pid: u32,
pub name: String,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ExitInfoRow {
pub code: Option<i32>,
pub signal: Option<i32>,
}
impl From<&ProcessInfo> for SheepRow {
fn from(info: &ProcessInfo) -> Self {
Self {
id: info.id,
name: info.name.clone(),
status: info.status.to_string(),
pid: info.pid,
restarts: info.restarts,
uptime_ms: info.uptime_ms,
fold: info.fold.clone(),
out_file: info.out_file.clone(),
err_file: info.err_file.clone(),
cpu_percent: info.cpu_percent,
memory_bytes: info.memory_bytes,
dog: info.dog.as_ref().map(DogRow::from),
lambs: info
.lambs
.as_ref()
.map(|lambs| lambs.iter().map(LambRow::from).collect()),
last_exit: info.last_exit.as_ref().map(ExitInfoRow::from),
smit: info.smit.clone(),
}
}
}
impl From<&ExitInfo> for ExitInfoRow {
fn from(exit: &ExitInfo) -> Self {
Self {
code: exit.code,
signal: exit.signal,
}
}
}
impl From<&DogSource> for DogRow {
fn from(source: &DogSource) -> Self {
match source {
DogSource::BuiltIn => Self::BuiltIn,
DogSource::Adopted { path } => Self::Adopted { path: path.clone() },
_ => Self::Unknown,
}
}
}
impl From<&Lamb> for LambRow {
fn from(lamb: &Lamb) -> Self {
Self {
pid: lamb.pid,
name: lamb.name.clone(),
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct BarkRow {
pub at_ms: u64,
pub rule: String,
pub subject: String,
pub message: String,
pub sinks: Vec<SinkOutcomeRow>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct SinkOutcomeRow {
pub sink: String,
pub error: Option<String>,
}
impl From<&Bark> for BarkRow {
fn from(bark: &Bark) -> Self {
Self {
at_ms: bark.at_ms,
rule: bark.rule.clone(),
subject: bark.subject.clone(),
message: bark.message.clone(),
sinks: bark.sinks.iter().map(SinkOutcomeRow::from).collect(),
}
}
}
impl From<&SinkOutcome> for SinkOutcomeRow {
fn from(outcome: &SinkOutcome) -> Self {
Self {
sink: outcome.sink.clone(),
error: outcome.error.clone(),
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct MetricsReading {
pub daemon_version: String,
pub daemon_pid: u32,
pub flock: Vec<SheepRow>,
pub host: Option<HostRow>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct HostRow {
pub memory_total_bytes: u64,
pub memory_used_bytes: u64,
pub processes: u64,
pub uptime_seconds: u64,
}
impl From<&HostReading> for HostRow {
fn from(host: &HostReading) -> Self {
Self {
memory_total_bytes: host.memory_total_bytes,
memory_used_bytes: host.memory_used_bytes,
processes: host.processes as u64,
uptime_seconds: host.uptime_seconds,
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct BleatTail {
pub name: String,
pub id: u32,
pub out: Vec<String>,
pub err: Vec<String>,
pub truncated: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use shep_core::protocol::{DogSource, Lamb, ProcessInfo};
use shep_core::status::ProcStatus;
#[test]
fn a_sheep_row_serializes_exactly_as_process_info_does() {
let info = ProcessInfo::builder(7, "api", ProcStatus::WaitingRestart)
.pid(Some(4242))
.restarts(3)
.uptime_ms(61_000)
.fold(Some("web".to_string()))
.out_file(Some("/tmp/api-out.log".to_string()))
.err_file(Some("/tmp/api-err.log".to_string()))
.cpu_percent(Some(12.5))
.memory_bytes(Some(1024 * 1024))
.dog(Some(DogSource::Adopted {
path: "/usr/local/bin/dog".to_string(),
}))
.lambs(Some(vec![Lamb::new(4243, "node")]))
.build();
assert_eq!(
serde_json::to_value(SheepRow::from(&info)).unwrap(),
serde_json::to_value(&info).unwrap(),
"whistle and `--format json` must describe a sheep identically"
);
}
#[test]
fn an_empty_sheep_row_serializes_exactly_as_process_info_does_too() {
let info = ProcessInfo::builder(1, "idle", ProcStatus::Stopped).build();
assert_eq!(
serde_json::to_value(SheepRow::from(&info)).unwrap(),
serde_json::to_value(&info).unwrap()
);
}
#[test]
fn the_generated_schema_names_every_field_the_row_carries() {
let schema = serde_json::to_value(schemars::schema_for!(SheepRow)).unwrap();
let properties = schema["properties"].as_object().expect("an object schema");
let info = ProcessInfo::builder(1, "idle", ProcStatus::Stopped).build();
let emitted = serde_json::to_value(&info).unwrap();
for key in emitted.as_object().unwrap().keys() {
assert!(
properties.contains_key(key),
"the schema is missing `{key}`, which the tool returns"
);
}
}
#[test]
fn every_declared_tool_shape_is_object_rooted() {
for (label, schema) in [
("FlockListing", schemars::schema_for!(FlockListing)),
("BarkListing", schemars::schema_for!(BarkListing)),
("MetricsReading", schemars::schema_for!(MetricsReading)),
("BleatTail", schemars::schema_for!(BleatTail)),
] {
let value = serde_json::to_value(schema).unwrap();
assert_eq!(
value["type"], "object",
"{label} is a tool's declared output and must be object-rooted"
);
}
}
#[test]
fn a_bark_row_serializes_exactly_as_a_bark_does() {
let bark = Bark {
at_ms: 1_700_000_000_000,
rule: "restart-loop".to_string(),
subject: "api".to_string(),
message: "api restarted 5 times in 60s".to_string(),
sinks: vec![
SinkOutcome {
sink: "ops-slack".to_string(),
error: None,
},
SinkOutcome {
sink: "pager".to_string(),
error: Some("502 from the webhook".to_string()),
},
],
};
assert_eq!(
serde_json::to_value(BarkRow::from(&bark)).unwrap(),
serde_json::to_value(&bark).unwrap()
);
}
}