1use core::fmt;
2use std::{borrow::Cow, collections::BTreeMap};
3
4use aligned_vec::{AVec, ConstAlign};
5use chrono::{DateTime, Utc};
6use eyre::Context as _;
7use serde::Deserialize;
8use uuid::Uuid;
9
10use crate::{BuildId, DataflowId, daemon_to_daemon::InterDaemonEvent, id::NodeId};
11
12pub use log::Level as LogLevel;
13
14#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
15#[must_use]
16pub struct LogMessage {
17 pub build_id: Option<BuildId>,
18 pub dataflow_id: Option<DataflowId>,
19 pub node_id: Option<NodeId>,
20 pub daemon_id: Option<DaemonId>,
21 pub level: LogLevelOrStdout,
22 pub target: Option<String>,
23 pub module_path: Option<String>,
24 pub file: Option<String>,
25 pub line: Option<u32>,
26 pub message: String,
27 pub timestamp: DateTime<Utc>,
28 pub fields: Option<BTreeMap<String, String>>,
29}
30
31#[derive(Deserialize)]
32pub struct LogMessageHelper {
33 build_id: Option<BuildId>,
34 dataflow_id: Option<DataflowId>,
35 node_id: Option<NodeId>,
36 daemon_id: Option<DaemonId>,
37 level: LogLevelOrStdout,
38 target: Option<String>,
39 module_path: Option<String>,
40 file: Option<String>,
41 line: Option<u32>,
42 message: Option<String>,
43 timestamp: DateTime<Utc>,
44 fields: Option<BTreeMap<String, String>>,
45}
46
47impl From<LogMessageHelper> for LogMessage {
48 fn from(helper: LogMessageHelper) -> Self {
49 let fields = helper.fields.as_ref();
50 LogMessage {
51 build_id: helper.build_id.or(fields
52 .and_then(|f| f.get("build_id").cloned())
53 .and_then(|id| Uuid::parse_str(&id).ok().map(BuildId))),
54 dataflow_id: helper.dataflow_id.or(fields
55 .and_then(|f| f.get("dataflow_id").cloned())
56 .and_then(|id| Uuid::parse_str(&id).ok())),
57 node_id: helper
58 .node_id
59 .or(fields.and_then(|f| f.get("node_id").cloned()).map(NodeId)),
60 daemon_id: helper.daemon_id.or(fields
61 .and_then(|f| f.get("daemon_id").cloned())
62 .and_then(|id| DaemonId::from_display_str(&id))),
63 level: helper.level,
64 target: helper
65 .target
66 .or(fields.and_then(|f| f.get("target").cloned())),
67 module_path: helper
68 .module_path
69 .or(fields.and_then(|f| f.get("module_path").cloned())),
70 file: helper.file.or(fields.and_then(|f| f.get("file").cloned())),
71 line: helper.line.or(fields
72 .and_then(|f| f.get("line").cloned())
73 .and_then(|s| s.parse().ok())),
74 message: helper
75 .message
76 .or(fields.and_then(|f| f.get("message").cloned()))
77 .unwrap_or_default(),
78 fields: helper.fields,
79 timestamp: helper.timestamp,
80 }
81 }
82}
83
84#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord)]
85#[serde(rename_all = "UPPERCASE")]
86pub enum LogLevelOrStdout {
87 #[serde(rename = "stdout")]
88 Stdout,
89 #[serde(untagged)]
90 LogLevel(LogLevel),
91}
92
93impl LogLevelOrStdout {
94 pub fn passes(&self, min: &LogLevelOrStdout) -> bool {
99 match (self, min) {
100 (LogLevelOrStdout::Stdout, LogLevelOrStdout::Stdout) => true,
101 (LogLevelOrStdout::Stdout, _) => false,
102 (LogLevelOrStdout::LogLevel(_), LogLevelOrStdout::Stdout) => true,
103 (LogLevelOrStdout::LogLevel(msg), LogLevelOrStdout::LogLevel(max)) => msg <= max,
104 }
105 }
106}
107
108impl From<LogLevel> for LogLevelOrStdout {
109 fn from(level: LogLevel) -> Self {
110 Self::LogLevel(level)
111 }
112}
113
114#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
115pub struct NodeError {
116 pub timestamp: uhlc::Timestamp,
117 pub cause: NodeErrorCause,
118 pub exit_status: NodeExitStatus,
119}
120
121impl std::fmt::Display for NodeError {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 if let NodeErrorCause::FailedToSpawn(err) = &self.cause {
124 return write!(f, "failed to spawn node: {err}");
125 }
126 match &self.exit_status {
127 NodeExitStatus::Success => write!(f, "<success>"),
128 NodeExitStatus::IoError(err) => write!(f, "I/O error while reading exit status: {err}"),
129 NodeExitStatus::ExitCode(code) => write!(f, "exited with code {code}"),
130 NodeExitStatus::Signal(signal) => {
131 let signal_str: Cow<_> = match signal {
132 1 => "SIGHUP".into(),
133 2 => "SIGINT".into(),
134 3 => "SIGQUIT".into(),
135 4 => "SIGILL".into(),
136 6 => "SIGABRT".into(),
137 8 => "SIGFPE".into(),
138 9 => "SIGKILL".into(),
139 11 => "SIGSEGV".into(),
140 13 => "SIGPIPE".into(),
141 14 => "SIGALRM".into(),
142 15 => "SIGTERM".into(),
143 22 => "SIGABRT".into(),
144 23 => "NSIG".into(),
145 other => other.to_string().into(),
146 };
147 if matches!(self.cause, NodeErrorCause::GraceDuration) {
148 write!(
149 f,
150 "node was killed by dora because it didn't react to a stop message in time ({signal_str})"
151 )
152 } else {
153 write!(f, "exited because of signal {signal_str}")
154 }
155 }
156 NodeExitStatus::Unknown => write!(f, "unknown exit status"),
157 }?;
158
159 match &self.cause {
160 NodeErrorCause::GraceDuration => {} NodeErrorCause::Cascading { caused_by_node } => write!(
162 f,
163 ". This error occurred because node `{caused_by_node}` exited before connecting to dora."
164 )?,
165 NodeErrorCause::FailedToSpawn(_) => unreachable!(), NodeErrorCause::Other { stderr } if stderr.is_empty() => {}
167 NodeErrorCause::Other { stderr } => {
168 let line: &str = "---------------------------------------------------------------------------------\n";
169 let stderr = stderr.trim_end();
170 write!(f, " with stderr output:\n{line}{stderr}\n{line}")?
171 }
172 }
173
174 Ok(())
175 }
176}
177
178#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
179pub enum NodeErrorCause {
180 GraceDuration,
182 Cascading {
184 caused_by_node: NodeId,
185 },
186 FailedToSpawn(String),
187 Other {
188 stderr: String,
189 },
190}
191
192#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
193pub enum NodeExitStatus {
194 Success,
195 IoError(String),
196 ExitCode(i32),
197 Signal(i32),
198 Unknown,
199}
200
201impl NodeExitStatus {
202 pub fn is_success(&self) -> bool {
203 matches!(self, NodeExitStatus::Success)
204 }
205}
206
207impl From<Result<std::process::ExitStatus, std::io::Error>> for NodeExitStatus {
208 fn from(result: Result<std::process::ExitStatus, std::io::Error>) -> Self {
209 match result {
210 Ok(status) => {
211 if status.success() {
212 NodeExitStatus::Success
213 } else if let Some(code) = status.code() {
214 Self::ExitCode(code)
215 } else {
216 #[cfg(unix)]
217 {
218 use std::os::unix::process::ExitStatusExt;
219 if let Some(signal) = status.signal() {
220 return Self::Signal(signal);
221 }
222 }
223 Self::Unknown
224 }
225 }
226 Err(err) => Self::IoError(err.to_string()),
227 }
228 }
229}
230
231#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
232pub struct Timestamped<T> {
233 pub inner: T,
234 pub timestamp: uhlc::Timestamp,
235}
236
237impl<T> Timestamped<T>
238where
239 T: serde::Serialize,
240{
241 pub fn serialize(&self) -> eyre::Result<Vec<u8>> {
242 bincode::serialize(self).wrap_err("failed to serialize timestamped message")
243 }
244}
245
246impl Timestamped<InterDaemonEvent> {
247 pub fn deserialize_inter_daemon_event(bytes: &[u8]) -> eyre::Result<Self> {
248 bincode::deserialize(bytes).wrap_err("failed to deserialize InterDaemonEvent")
249 }
250}
251
252pub type SharedMemoryId = String;
253
254#[derive(serde::Serialize, serde::Deserialize, Clone)]
255pub enum DataMessage {
256 Vec(AVec<u8, ConstAlign<128>>),
257}
258
259impl fmt::Debug for DataMessage {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 match self {
262 Self::Vec(v) => f
263 .debug_struct("Vec")
264 .field("len", &v.len())
265 .finish_non_exhaustive(),
266 }
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
271pub struct DaemonId {
272 machine_id: Option<String>,
273 uuid: Uuid,
274}
275
276impl DaemonId {
277 pub fn new(machine_id: Option<String>) -> Self {
278 DaemonId {
279 machine_id,
280 uuid: Uuid::now_v7(),
281 }
282 }
283
284 pub fn matches_machine_id(&self, machine_id: &str) -> bool {
285 self.machine_id
286 .as_ref()
287 .map(|id| id == machine_id)
288 .unwrap_or_default()
289 }
290
291 pub fn machine_id(&self) -> Option<&str> {
292 self.machine_id.as_deref()
293 }
294
295 pub fn from_display_str(s: &str) -> Option<Self> {
308 if let Ok(uuid) = Uuid::parse_str(s) {
310 return Some(DaemonId {
311 machine_id: None,
312 uuid,
313 });
314 }
315 const UUID_LEN: usize = 36;
318 let split = s.len().checked_sub(UUID_LEN)?;
319 let uuid = Uuid::parse_str(s.get(split..)?).ok()?;
320 let machine_id = s.get(..split)?.strip_suffix('-')?;
321 Some(DaemonId {
322 machine_id: Some(machine_id.to_string()),
323 uuid,
324 })
325 }
326}
327
328impl std::fmt::Display for DaemonId {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 if let Some(id) = &self.machine_id {
331 write!(f, "{id}-")?;
332 }
333 write!(f, "{}", self.uuid)
334 }
335}
336
337#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
338pub struct GitSource {
339 pub repo: String,
340 pub commit_hash: String,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub subdir: Option<String>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub hub: Option<HubProvenance>,
351}
352
353#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
355pub struct HubProvenance {
356 pub name: String,
358 pub version: String,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub manifest_digest: Option<String>,
368}
369
370#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
375pub struct BinaryPin {
376 pub platform: String,
378 pub url: String,
380 pub sha256: String,
382 pub hub: HubProvenance,
384}
385
386#[cfg(test)]
388mod tests {
389 use super::*;
390 #[test]
391 fn test_log_message_serialization() {
392 let log_message = LogMessage {
393 build_id: Some(BuildId(Uuid::new_v4())),
394 dataflow_id: Some(DataflowId::from(Uuid::new_v4())),
395 node_id: Some(NodeId("node-1".to_string())),
396 daemon_id: Some(DaemonId::new(Some("machine-1".to_string()))),
397 level: LogLevelOrStdout::LogLevel(LogLevel::Info),
398 target: Some("target".to_string()),
399 module_path: Some("module::path".to_string()),
400 file: Some("file.rs".to_string()),
401 line: Some(42),
402 message: "This is a log message".to_string(),
403 timestamp: Utc::now(),
404 fields: Some(BTreeMap::from([("key".to_string(), "value".to_string())])),
405 };
406 let serialized = serde_yaml::to_string(&log_message).unwrap();
407 let deserialized: LogMessageHelper = serde_yaml::from_str(&serialized).unwrap();
408 assert_eq!(log_message, LogMessage::from(deserialized));
409 }
410
411 #[test]
412 fn stdout_passes_stdout_filter() {
413 let stdout = LogLevelOrStdout::Stdout;
414 assert!(stdout.passes(&LogLevelOrStdout::Stdout));
415 }
416
417 #[test]
422 fn daemon_id_display_roundtrips_through_parse() {
423 let uuid = Uuid::new_v4();
424 for machine in [None, Some("host"), Some("my-host"), Some("a-b-c-d")] {
425 let id = DaemonId {
426 machine_id: machine.map(str::to_string),
427 uuid,
428 };
429 let parsed = DaemonId::from_display_str(&id.to_string())
430 .unwrap_or_else(|| panic!("failed to parse {id}"));
431 assert_eq!(parsed, id, "round-trip failed for machine_id={machine:?}");
432 }
433 }
434
435 #[test]
438 fn daemon_id_parse_edge_cases() {
439 let uuid = Uuid::new_v4();
440 let bare = DaemonId {
441 machine_id: None,
442 uuid,
443 };
444 assert_eq!(DaemonId::from_display_str(&bare.to_string()), Some(bare));
445 assert_eq!(DaemonId::from_display_str("not-a-daemon-id"), None);
446 assert_eq!(DaemonId::from_display_str(""), None);
447 }
448
449 #[test]
450 fn stdout_fails_non_stdout_filters() {
451 let stdout = LogLevelOrStdout::Stdout;
452 assert!(!stdout.passes(&LogLevelOrStdout::LogLevel(LogLevel::Info)));
453 assert!(!stdout.passes(&LogLevelOrStdout::LogLevel(LogLevel::Warn)));
454 assert!(!stdout.passes(&LogLevelOrStdout::LogLevel(LogLevel::Error)));
455 }
456
457 #[test]
458 fn any_log_level_passes_stdout_filter() {
459 let stdout_filter = LogLevelOrStdout::Stdout;
460 for level in [
461 LogLevel::Error,
462 LogLevel::Warn,
463 LogLevel::Info,
464 LogLevel::Debug,
465 LogLevel::Trace,
466 ] {
467 assert!(
468 LogLevelOrStdout::LogLevel(level).passes(&stdout_filter),
469 "{level:?} should pass stdout filter"
470 );
471 }
472 }
473
474 #[test]
475 fn error_passes_less_verbose_filters() {
476 let error = LogLevelOrStdout::LogLevel(LogLevel::Error);
477 assert!(error.passes(&LogLevelOrStdout::LogLevel(LogLevel::Error)));
478 assert!(error.passes(&LogLevelOrStdout::LogLevel(LogLevel::Warn)));
479 assert!(error.passes(&LogLevelOrStdout::LogLevel(LogLevel::Info)));
480 }
481
482 #[test]
483 fn debug_fails_info_filter() {
484 let debug = LogLevelOrStdout::LogLevel(LogLevel::Debug);
485 assert!(!debug.passes(&LogLevelOrStdout::LogLevel(LogLevel::Info)));
486 }
487
488 #[test]
489 fn same_level_passes_itself() {
490 for level in [
491 LogLevel::Error,
492 LogLevel::Warn,
493 LogLevel::Info,
494 LogLevel::Debug,
495 LogLevel::Trace,
496 ] {
497 let l = LogLevelOrStdout::LogLevel(level);
498 assert!(l.passes(&l), "{level:?} should pass itself");
499 }
500 }
501
502 #[test]
503 fn trace_passes_trace_fails_debug() {
504 let trace = LogLevelOrStdout::LogLevel(LogLevel::Trace);
505 assert!(trace.passes(&LogLevelOrStdout::LogLevel(LogLevel::Trace)));
506 assert!(!trace.passes(&LogLevelOrStdout::LogLevel(LogLevel::Debug)));
507 }
508
509 #[test]
510 fn daemon_id_uses_v7_uuid() {
511 let id = DaemonId::new(None);
512 assert_eq!(id.uuid.get_version_num(), 7);
514 }
515}