1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use soma_fleet::{HostId, HostRecord, TopologyRevision};
4use tokio_util::sync::CancellationToken;
5
6use crate::{InfraError, InfraResult};
7
8const MAX_LOG_LINES: u32 = 5000;
9const MAX_GREP_CHARS: usize = 1024;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
13pub struct DockerUsageCategory {
14 pub count: u64,
16 pub size_bytes: u64,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct DockerDiskUsage {
23 pub host: HostId,
25 pub topology_revision: TopologyRevision,
27 pub layers_size_bytes: u64,
29 pub images: DockerUsageCategory,
31 pub containers: DockerUsageCategory,
33 pub volumes: DockerUsageCategory,
35 pub build_cache: DockerUsageCategory,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
41#[serde(rename_all = "snake_case")]
42pub enum DockerLogStream {
43 Stdout,
45 Stderr,
47 #[default]
49 Both,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct ContainerLogOptions {
55 lines: u32,
56 stream: DockerLogStream,
57 since_unix_seconds: Option<i64>,
58 until_unix_seconds: Option<i64>,
59 grep: Option<String>,
60}
61
62impl Default for ContainerLogOptions {
63 fn default() -> Self {
64 Self {
65 lines: 100,
66 stream: DockerLogStream::Both,
67 since_unix_seconds: None,
68 until_unix_seconds: None,
69 grep: None,
70 }
71 }
72}
73
74impl ContainerLogOptions {
75 pub fn with_lines(mut self, lines: u32) -> InfraResult<Self> {
77 if lines == 0 || lines > MAX_LOG_LINES {
78 return Err(InfraError::InvalidRequest {
79 domain: "docker",
80 message: format!("container log lines must be 1-{MAX_LOG_LINES}"),
81 });
82 }
83 self.lines = lines;
84 Ok(self)
85 }
86
87 #[must_use]
89 pub const fn with_stream(mut self, stream: DockerLogStream) -> Self {
90 self.stream = stream;
91 self
92 }
93
94 pub fn with_since(mut self, seconds: i64) -> InfraResult<Self> {
96 self.since_unix_seconds = Some(seconds);
97 self.validate_time_order()?;
98 Ok(self)
99 }
100
101 pub fn with_until(mut self, seconds: i64) -> InfraResult<Self> {
103 self.until_unix_seconds = Some(seconds);
104 self.validate_time_order()?;
105 Ok(self)
106 }
107
108 pub fn with_grep(mut self, grep: impl Into<String>) -> InfraResult<Self> {
110 let grep = grep.into();
111 let count = grep.chars().count();
112 if count == 0 || count > MAX_GREP_CHARS || grep.chars().any(char::is_control) {
113 return Err(InfraError::InvalidRequest {
114 domain: "docker",
115 message: format!(
116 "container log grep must be 1-{MAX_GREP_CHARS} printable characters"
117 ),
118 });
119 }
120 self.grep = Some(grep);
121 Ok(self)
122 }
123
124 #[must_use]
126 pub const fn lines(&self) -> u32 {
127 self.lines
128 }
129
130 #[must_use]
132 pub const fn stream(&self) -> DockerLogStream {
133 self.stream
134 }
135
136 #[must_use]
138 pub const fn since_unix_seconds(&self) -> Option<i64> {
139 self.since_unix_seconds
140 }
141
142 #[must_use]
144 pub const fn until_unix_seconds(&self) -> Option<i64> {
145 self.until_unix_seconds
146 }
147
148 #[must_use]
150 pub fn grep(&self) -> Option<&str> {
151 self.grep.as_deref()
152 }
153
154 fn validate_time_order(&self) -> InfraResult<()> {
155 if matches!(
156 (self.since_unix_seconds, self.until_unix_seconds),
157 (Some(since), Some(until)) if since > until
158 ) {
159 Err(InfraError::InvalidRequest {
160 domain: "docker",
161 message: "container log since must not exceed until".into(),
162 })
163 } else {
164 Ok(())
165 }
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct ContainerLogs {
172 pub host: HostId,
174 pub topology_revision: TopologyRevision,
176 pub container: String,
178 pub lines: Vec<String>,
180 pub truncated: bool,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct ContainerStatsSnapshot {
187 pub host: HostId,
189 pub topology_revision: TopologyRevision,
191 pub container: String,
193 pub read_at: Option<String>,
195 pub pids_current: u64,
197 pub memory_usage_bytes: u64,
199 pub memory_limit_bytes: u64,
201 pub cpu_total_usage: u64,
203 pub system_cpu_usage: u64,
205 pub online_cpus: u64,
207 pub network_rx_bytes: u64,
209 pub network_tx_bytes: u64,
211 pub block_read_bytes: u64,
213 pub block_write_bytes: u64,
215}
216
217#[async_trait]
219pub trait DockerTelemetryReader: Send + Sync {
220 async fn disk_usage(
222 &self,
223 host: &HostRecord,
224 cancellation: &CancellationToken,
225 ) -> InfraResult<DockerDiskUsage>;
226
227 async fn container_logs(
229 &self,
230 host: &HostRecord,
231 container: &str,
232 options: &ContainerLogOptions,
233 cancellation: &CancellationToken,
234 ) -> InfraResult<ContainerLogs>;
235
236 async fn container_stats(
238 &self,
239 host: &HostRecord,
240 container: &str,
241 cancellation: &CancellationToken,
242 ) -> InfraResult<ContainerStatsSnapshot>;
243}
244
245#[cfg(test)]
246#[path = "docker_telemetry_tests.rs"]
247mod tests;