use schemars::JsonSchema;
use serde::Serialize;
use shep_core::barks::{Bark, SinkOutcome};
use shep_core::protocol::{DogSource, ExitInfo, HostUsage, Lamb, ProcessInfo};
use crate::host_sample::HostReading;
#[derive(Debug, Serialize, JsonSchema)]
pub struct FlockListing {
pub flock: Vec<SheepRow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub host: Option<String>,
}
#[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 depends_on: Vec<String>,
pub out_file: Option<String>,
pub err_file: Option<String>,
pub cpu_percent: Option<f32>,
pub memory_bytes: Option<u64>,
pub cpu_ms: Option<u64>,
pub dog: Option<DogSourceRow>,
pub lambs: Option<Vec<LambRow>>,
pub last_exit: Option<ExitInfoRow>,
pub smit: Option<String>,
pub instance: Option<u32>,
pub handshook: Option<bool>,
pub dog_stale: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pending: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub overridden: Option<Vec<String>>,
pub max_memory: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DogSourceRow {
BuiltIn,
Adopted {
path: String,
#[serde(skip_serializing_if = "core::ops::Not::not")]
channel: bool,
},
Unknown,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct LambRow {
pub pid: u32,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<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(),
depends_on: info.depends_on.clone(),
out_file: info.out_file.clone(),
err_file: info.err_file.clone(),
cpu_percent: info.cpu_percent,
memory_bytes: info.memory_bytes,
cpu_ms: info.cpu_ms,
dog: info.dog.as_ref().map(DogSourceRow::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(),
instance: info.instance,
handshook: info.handshook,
dog_stale: info.dog_stale,
pending: info.pending.clone(),
overridden: info.overridden.clone(),
max_memory: info.max_memory,
disabled: info.disabled,
}
}
}
impl From<&ExitInfo> for ExitInfoRow {
fn from(exit: &ExitInfo) -> Self {
Self {
code: exit.code,
signal: exit.signal,
}
}
}
impl From<&DogSource> for DogSourceRow {
fn from(source: &DogSource) -> Self {
match source {
DogSource::BuiltIn { .. } => Self::BuiltIn,
DogSource::Adopted { path, channel, .. } => Self::Adopted {
path: path.clone(),
channel: *channel,
},
_ => Self::Unknown,
}
}
}
impl From<&Lamb> for LambRow {
fn from(lamb: &Lamb) -> Self {
Self {
pid: lamb.pid,
name: lamb.name.clone(),
label: lamb.label.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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub host_name: Option<String>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct HostRow {
pub memory_total_bytes: u64,
pub memory_used_bytes: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub processes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uptime_seconds: Option<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: Some(host.processes as u64),
uptime_seconds: Some(host.uptime_seconds),
}
}
}
impl From<&HostUsage> for HostRow {
fn from(usage: &HostUsage) -> Self {
Self {
memory_total_bytes: usage.memory_total_bytes,
memory_used_bytes: usage.memory_used_bytes,
processes: None,
uptime_seconds: None,
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct BleatTail {
pub name: String,
pub id: u32,
pub out: Vec<String>,
pub err: Vec<String>,
pub truncated: bool,
pub notes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub host: Option<String>,
}
#[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))
.max_memory(Some(64 * 1024 * 1024))
.cpu_ms(Some(5_678))
.dog(Some(DogSource::Adopted {
grants: None,
path: "/usr/local/bin/dog".to_string(),
channel: true,
events: Vec::new(),
}))
.lambs(Some(vec![
Lamb::new(4243, "node"),
Lamb::new(4244, "node").with_label("worker 1"),
]))
.pending(Some(vec!["env".to_string()]))
.overridden(Some(vec!["cwd".to_string()]))
.disabled(Some(true))
.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 every_dog_source_shape_serializes_exactly_as_dog_source_does() {
let adopted = |channel| DogSource::Adopted {
grants: None,
path: "/usr/local/bin/dog".to_string(),
channel,
events: Vec::new(),
};
for source in [
DogSource::BuiltIn {
grants: None,
events: Vec::new(),
},
adopted(false),
adopted(true),
] {
assert_eq!(
serde_json::to_value(DogSourceRow::from(&source)).unwrap(),
serde_json::to_value(&source).unwrap(),
"{source:?}"
);
}
}
#[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(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 {
grants: None,
path: "/usr/local/bin/dog".to_string(),
channel: false,
events: Vec::new(),
}))
.lambs(Some(vec![Lamb::new(4243, "node")]))
.pending(Some(vec!["env".to_string()]))
.overridden(Some(vec!["cwd".to_string()]))
.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()
);
}
}