Skip to main content

olai_stack_runtime/
events.rs

1//! Container events, parsed from `docker events --format '{{json .}}'`.
2
3use serde::{Deserialize, Serialize};
4
5use crate::status::Health;
6
7/// The label Compose puts on every container with its project's name.
8pub const PROJECT_LABEL: &str = "com.docker.compose.project";
9/// The label Compose puts on every container with its service's name.
10pub const SERVICE_LABEL: &str = "com.docker.compose.service";
11
12/// What happened to a container.
13#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum EventAction {
16    /// The container was created.
17    Create,
18    /// It started.
19    Start,
20    /// It was asked to stop.
21    Stop,
22    /// It was sent a signal.
23    Kill,
24    /// Its process exited.
25    Die {
26        /// The exit code, when Docker reported one.
27        exit_code: Option<i32>,
28    },
29    /// It was removed.
30    Destroy,
31    /// Its healthcheck changed state.
32    Health(Health),
33    /// Something else (`pause`, `restart`, `oom`, …), by Docker's name.
34    Other(String),
35}
36
37/// One event about a container of a Compose project.
38#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
39pub struct EngineEvent {
40    /// The project the container belongs to.
41    pub project: String,
42    /// The compose service.
43    pub service: String,
44    /// The container's name.
45    pub container: String,
46    /// What happened.
47    pub action: EventAction,
48    /// When, in nanoseconds since the Unix epoch.
49    pub time_nanos: i64,
50}
51
52/// Parse one line of `docker events --format '{{json .}}'`. `Ok(None)` for an event that isn't
53/// about a Compose container, and for the `exec_*` events every healthcheck probe raises.
54pub fn parse_event(line: &str) -> Result<Option<EngineEvent>, serde_json::Error> {
55    #[derive(Deserialize)]
56    struct Actor {
57        #[serde(rename = "Attributes", default)]
58        attributes: std::collections::BTreeMap<String, String>,
59    }
60    #[derive(Deserialize)]
61    struct Raw {
62        #[serde(rename = "Type", default)]
63        kind: String,
64        #[serde(rename = "Action", default)]
65        action: String,
66        #[serde(rename = "Actor")]
67        actor: Actor,
68        #[serde(rename = "timeNano", default)]
69        time_nano: i64,
70    }
71    let raw: Raw = serde_json::from_str(line)?;
72    if raw.kind != "container" {
73        return Ok(None);
74    }
75    let attrs = &raw.actor.attributes;
76    let (Some(project), Some(service)) = (attrs.get(PROJECT_LABEL), attrs.get(SERVICE_LABEL))
77    else {
78        return Ok(None);
79    };
80    let action = match raw.action.split_once(':') {
81        Some(("health_status", health)) => EventAction::Health(Health::parse(health)),
82        Some((name, _)) if name.starts_with("exec_") => return Ok(None),
83        _ => match raw.action.as_str() {
84            "create" => EventAction::Create,
85            "start" => EventAction::Start,
86            "stop" => EventAction::Stop,
87            "kill" => EventAction::Kill,
88            "die" => EventAction::Die {
89                exit_code: attrs.get("exitCode").and_then(|c| c.parse().ok()),
90            },
91            "destroy" => EventAction::Destroy,
92            other if other.starts_with("exec_") => return Ok(None),
93            other => EventAction::Other(other.to_owned()),
94        },
95    };
96    Ok(Some(EngineEvent {
97        project: project.clone(),
98        service: service.clone(),
99        container: attrs.get("name").cloned().unwrap_or_default(),
100        action,
101        time_nanos: raw.time_nano,
102    }))
103}
104
105/// Which projects' events to watch.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub enum EventFilter {
108    /// One project, by name.
109    Project(String),
110    /// Every project whose name starts with this prefix.
111    Prefix(String),
112    /// Every Compose project.
113    All,
114}
115
116impl EventFilter {
117    /// Whether `event` passes the filter.
118    pub fn matches(&self, event: &EngineEvent) -> bool {
119        match self {
120            EventFilter::Project(name) => event.project == *name,
121            EventFilter::Prefix(prefix) => event.project.starts_with(prefix.as_str()),
122            EventFilter::All => true,
123        }
124    }
125}