Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion crates/core/src/fixtures.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,46 @@
//! The full sample report, `tests/fixtures/report-full.json`, for other crates' tests.
//! Sample data for other crates' tests: the full sample report,
//! `tests/fixtures/report-full.json`, and views with only what identifies them. A test sets
//! what it asserts on (`AppSummary { status: Warn, ..app("x/shop") }`); a field added later
//! takes its default here, so no test has to change for it.

use crate::report::Report;
use crate::rules::{IncidentCode, Severity};
use crate::view::{AppSummary, HostOverview, IncidentView, WorkloadSummary};
use serde_json::json;

pub fn full_report() -> Report {
serde_json::from_str(include_str!("../tests/fixtures/report-full.json")).expect("valid fixture")
}

fn from<T: serde::de::DeserializeOwned>(value: serde_json::Value) -> T {
serde_json::from_value(value).expect("valid fixture")
}

/// An ok application named after its key's label (`shop` for `x/shop`).
pub fn app(key: &str) -> AppSummary {
let name = key.rsplit('/').next().unwrap_or(key);
from(json!({ "key": key, "name": name, "status": "ok" }))
}

/// An ok, running workload: `host/project/service`.
pub fn workload(key: &str) -> WorkloadSummary {
let mut parts = key.splitn(3, '/');
let (host, project, service) = (parts.next(), parts.next(), parts.next());
from(json!({
"key": { "host": host, "project": project, "service": service },
"status": "ok", "run": "running"
}))
}

/// An ok host that reported a moment ago.
pub fn host_overview(id: &str) -> HostOverview {
from(json!({ "id": id, "status": "ok", "last_report_ago": "5s" }))
}

/// An open, unmuted incident with no detail and no application.
pub fn incident(subject: &str, code: IncidentCode, severity: Severity) -> IncidentView {
from(json!({
"subject": subject, "code": code, "severity": severity, "detail": "",
"muted": false
}))
}
19 changes: 3 additions & 16 deletions crates/core/tests/judge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,22 +405,9 @@ fn a_healthcheck_that_cannot_run_is_hygiene_not_unhealthy() {
}

fn incident(subject: Subject, severity: Severity, muted: bool) -> IncidentView {
IncidentView {
subject,
code: IncidentCode::DiskFilling,
severity,
detail: String::new(),
opened_at: None,
since: None,
open_for: None,
resolved_at: None,
muted,
mute_reason: None,
links: BTreeMap::new(),
app: None,
observed_since: None,
workload: None,
}
let i =
skym_core::fixtures::incident(&subject.to_string(), IncidentCode::DiskFilling, severity);
IncidentView { muted, ..i }
}

#[test]
Expand Down
20 changes: 6 additions & 14 deletions crates/server/src/api/apps.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! Applications: workloads grouped by application, described by the configuration. Pure.

use super::views::{links, status};
use super::views::{self, links, status};
use crate::config::{AppConfig, HostEntry};
use crate::store::hosts::WorkloadRow;
use jiff::Timestamp;
use skym_core::model::{Event, EventKind, RunState};
use skym_core::rules::IncidentCode;
use skym_core::subject::{AppKey, Subject, WorkloadKey, encode};
use skym_core::subject::{AppKey, Subject, WorkloadKey};
use skym_core::view::{
AppSummary, Deploy, EndpointOverview, IncidentView, Status, WorkloadSummary, workload_summary,
AppSummary, Deploy, EndpointOverview, IncidentView, Status, WorkloadSummary,
};
use std::collections::BTreeMap;

Expand Down Expand Up @@ -107,23 +107,15 @@ fn summary(
tags,
endpoints,
incidents,
links: BTreeMap::from([
("app".to_string(), key.path()),
("host".to_string(), format!("/api/hosts/{}", encode(&key.host))),
]),
links: links(&Subject::App(key.clone())),
key,
}
}

/// Its workloads with their own status, those with problems first.
fn workload_summaries(rows: &[&WorkloadRow], incidents: &[IncidentView]) -> Vec<WorkloadSummary> {
let mut list: Vec<WorkloadSummary> = rows
.iter()
.map(|w| {
let links = links(&Subject::Workload(w.key.clone()));
workload_summary(w.key.clone(), w.facts.as_ref(), &w.state, incidents, links)
})
.collect();
let mut list: Vec<WorkloadSummary> =
rows.iter().map(|w| views::summary(w, incidents)).collect();
list.sort_by(|a, b| b.status.cmp(&a.status).then_with(|| a.key.cmp(&b.key)));
list
}
Expand Down
21 changes: 3 additions & 18 deletions crates/server/src/api/apps_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,17 @@ fn row(subject: &str, run: RunState) -> WorkloadRow {

fn incident(subject: &str, code: IncidentCode, severity: Severity) -> IncidentView {
IncidentView {
subject: subject.parse().unwrap(),
code,
severity,
detail: "d".into(),
opened_at: Some(at(0)),
since: None,
open_for: None,
resolved_at: None,
muted: false,
mute_reason: None,
links: BTreeMap::new(),
app: None,
observed_since: None,
workload: None,
..skym_core::fixtures::incident(subject, code, severity)
}
}

fn configured(id: &str, name: Option<&str>) -> AppConfig {
AppConfig {
id: id.parse().unwrap(),
name: name.map(String::from),
env: Some("prod".into()),
note: Some("the shop".into()),
tags: vec![],
probes: vec![],
..crate::config::app_config(id, Some("prod"))
}
}

Expand Down Expand Up @@ -225,9 +211,8 @@ fn an_app_lists_its_workloads_deploys_and_recent_exceptions() {
fn apps_carry_their_hosts_tags_and_their_own() {
let host = |id: &str, tags: &[&str]| HostEntry {
id: id.into(),
customer: None,
token_sha256: String::new(),
tags: tags.iter().map(|t| t.to_string()).collect(),
..HostEntry::default()
};
let hosts = [host("x", &["acme", "cn"]), host("y", &["other"])];
let rows = [
Expand Down
13 changes: 3 additions & 10 deletions crates/server/src/api/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use skym_core::subject::{AppKey, Subject, WorkloadKey};
use skym_core::time::parse_since;
use skym_core::view::{
AppList, AppView, ExceptionList, HostList, HostView, IncidentList, IncidentView, Overview,
Timeline, WorkloadView, workload_summary,
Timeline, WorkloadView,
};

type ApiResult<T> = Result<Json<T>, ApiError>;
Expand Down Expand Up @@ -262,14 +262,7 @@ async fn one_app(s: AppState, key: AppKey) -> ApiResult<AppView> {
.into_iter()
.find(|a| a.key == key)
.ok_or_else(|| ApiError::not_found(format!("app {key} is unknown")))?;
let workloads = snap
.workloads
.into_iter()
.filter(|w| key.contains(&w.key))
.map(|w| {
let links = views::links(&Subject::Workload(w.key.clone()));
workload_summary(w.key, w.facts.as_ref(), &w.state, &app.incidents, links)
})
.collect();
// The same workloads as the application's own, problems first.
let workloads = app.workloads.clone();
Ok(Json(AppView { app, workloads }))
}
16 changes: 4 additions & 12 deletions crates/server/src/api/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
//! problems the same way.

use super::apps::History;
use super::views::{self, Context, incident_view, links};
use super::views::{self, Context, incident_view};
use super::{ApiError, AppState, apps};
use crate::lifecycle::Incident;
use crate::store::hosts::{HostRow, WorkloadRow};
use crate::store::probes::ProbeRow;
use crate::store::{history, hosts, incidents, probes};
use jiff::{SignedDuration, Timestamp};
use skym_core::subject::{HostId, Subject, WorkloadKey};
use skym_core::view::{
AppSummary, EndpointOverview, IncidentView, WorkloadSummary, workload_summary,
};
use skym_core::view::{AppSummary, EndpointOverview, IncidentView, WorkloadSummary};
use std::collections::BTreeMap;

/// Generous bound on rows read for one response; lists are cut to `limit` afterwards.
Expand Down Expand Up @@ -81,14 +79,8 @@ impl Snapshot {
};
let open: Vec<IncidentView> = self.open.iter().map(|i| incident_view(i, &cx)).collect();
// Each workload once, its status from every open incident (whatever was asked for).
let workloads: BTreeMap<&WorkloadKey, WorkloadSummary> = self
.workloads
.iter()
.map(|w| {
let links = links(&Subject::Workload(w.key.clone()));
(&w.key, workload_summary(w.key.clone(), w.facts.as_ref(), &w.state, &open, links))
})
.collect();
let workloads: BTreeMap<&WorkloadKey, WorkloadSummary> =
self.workloads.iter().map(|w| (&w.key, views::summary(w, &open))).collect();
// A workload's problem carries the workload, for a reader that looks no further.
incidents
.iter()
Expand Down
16 changes: 8 additions & 8 deletions crates/server/src/api/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use skym_core::subject::{AppKey, HostId, Subject, WorkloadKey, encode};
use skym_core::time::format_duration;
use skym_core::view::{
AppSummary, CustomerOverview, DiskUse, EndpointOverview, HostOverview, HostView, IncidentView,
Overview, Status, Timeline, TimelineEntry, TimelineKind, WorkloadView, rollup,
Overview, Status, Timeline, TimelineEntry, TimelineKind, WorkloadSummary, WorkloadView, rollup,
workload_summary,
};
use std::collections::BTreeMap;
Expand Down Expand Up @@ -81,6 +81,12 @@ pub fn app_of(subject: &Subject, probed: &[Endpoint]) -> Option<AppKey> {
}
}

/// A workload with its links, its status judged by `incidents`.
pub fn summary(w: &WorkloadRow, incidents: &[IncidentView]) -> WorkloadSummary {
let links = links(&Subject::Workload(w.key.clone()));
workload_summary(w.key.clone(), w.facts.as_ref(), &w.state, incidents, links)
}

/// Where to look next, so an agent never builds URLs itself.
pub fn links(subject: &Subject) -> BTreeMap<String, String> {
let host_links = |h: &str| {
Expand Down Expand Up @@ -133,13 +139,7 @@ pub fn host(
apps: Vec<AppSummary>,
now: Timestamp,
) -> HostView {
let workloads = workloads
.into_iter()
.map(|w| {
let links = links(&Subject::Workload(w.key.clone()));
workload_summary(w.key, w.facts.as_ref(), &w.state, &incidents, links)
})
.collect();
let workloads = workloads.into_iter().map(|w| summary(&w, &incidents)).collect();
HostView {
status: status(&incidents, row.is_some()),
last_report_ago: row.as_ref().map(|r| format_duration(now.duration_since(r.last_seen))),
Expand Down
16 changes: 2 additions & 14 deletions crates/server/src/api/views_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,22 +168,11 @@ fn row(id: &str, seen: i64) -> HostRow {

fn summary(key: &str, status: Status) -> AppSummary {
AppSummary {
key: key.parse().unwrap(),
name: key.into(),
env: None,
note: None,
configured: false,
status,
services: 1,
running: 1,
last_deployed: None,
endpoints: vec![],
incidents: vec![],
links: BTreeMap::new(),
workloads: vec![],
deploys: vec![],
exceptions_1h: 0,
tags: vec![],
..skym_core::fixtures::app(key)
}
}

Expand All @@ -192,8 +181,7 @@ fn the_overview_lists_problems_flat_and_hosts_by_urgency() {
let host = |id: &str, customer: Option<&str>| HostEntry {
id: id.into(),
customer: customer.map(String::from),
token_sha256: String::new(),
tags: vec![],
..HostEntry::default()
};
let cfg = ServerConfig {
customers: vec![Customer { id: "acme".into(), name: "Acme".into() }],
Expand Down
15 changes: 14 additions & 1 deletion crates/server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub struct Customer {
pub name: String,
}

#[derive(Deserialize, Debug, Clone)]
#[derive(Deserialize, Debug, Clone, Default)]
pub struct HostEntry {
pub id: HostId,
#[serde(default)]
Expand Down Expand Up @@ -129,6 +129,19 @@ pub struct AppProbe {
pub headers: Headers,
}

/// A listed application with nothing but its id and environment, for tests.
#[cfg(test)]
pub(crate) fn app_config(id: &str, env: Option<&str>) -> AppConfig {
AppConfig {
id: id.parse().unwrap(),
name: None,
env: env.map(String::from),
note: None,
tags: vec![],
probes: vec![],
}
}

impl ServerConfig {
/// Every URL to probe: the applications' probes.
pub fn probed(&self) -> Vec<Endpoint> {
Expand Down
11 changes: 1 addition & 10 deletions crates/server/src/findings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,16 +347,7 @@ mod tests {
assert_eq!(both.len(), 2, "down and expiring are separate incidents");
}

fn app(id: &str, env: Option<&str>) -> AppConfig {
AppConfig {
id: id.parse().unwrap(),
name: None,
env: env.map(String::from),
note: None,
tags: vec![],
probes: vec![],
}
}
use crate::config::app_config as app;

#[test]
fn container_apps_go_missing_only_from_a_complete_listing() {
Expand Down
14 changes: 4 additions & 10 deletions crates/server/tests/api.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//! The HTTP API end to end, against an in-memory database.

mod common;

use axum::Router;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use common::{app_config, down};
use flate2::{Compression, write::GzEncoder};
use http_body_util::BodyExt;
use jiff::{SignedDuration, Timestamp};
use serde_json::Value;
use skym_core::model::RunState;
use skym_core::report::Report;
use skym_server::api::{AppState, router};
use skym_server::config::{
Expand Down Expand Up @@ -256,13 +258,6 @@ async fn a_silent_host_loses_its_heartbeat_and_a_report_restores_it() {
assert_eq!(resolved["incidents"][0]["code"], "HEARTBEAT_LOST");
}

/// Workload `i` of the report exited with an error.
fn down(mut r: Report, i: usize) -> Report {
r.workloads[i].state.run = RunState::Exited;
r.workloads[i].state.exit_code = Some(1);
r
}

#[tokio::test]
async fn views_scope_incidents_and_ignore_muted_ones() {
let mute = Mute {
Expand Down Expand Up @@ -441,16 +436,15 @@ async fn reports_from_a_clock_slightly_ahead_are_visible_at_once() {
#[tokio::test]
async fn applications_are_listed_described_and_missed() {
let shop = AppConfig {
id: "x/shop".parse().unwrap(),
name: Some("Shop".into()),
env: Some("prod".into()),
note: Some("the web shop".into()),
tags: vec!["billing".into()],
probes: vec![AppProbe {
url: "https://shop.example.com/".into(),
expect: vec![],
headers: Headers([("Authorization".to_string(), "Bearer s3cret".to_string())].into()),
}],
..app_config("x/shop", Some("prod"))
};
let (app, state) = app_configured(vec![], vec![shop]);
let listed = |mins_ago| Report { containers_listed: true, ..report(mins_ago) };
Expand Down
Loading
Loading