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)]
21#[must_use]
22pub struct LogMessage {
23 pub build_id: Option<BuildId>,
25 pub dataflow_id: Option<DataflowId>,
27 pub node_id: Option<NodeId>,
29 pub daemon_id: Option<DaemonId>,
31 pub level: LogLevelOrStdout,
33 pub target: Option<String>,
35 pub module_path: Option<String>,
37 pub file: Option<String>,
39 pub line: Option<u32>,
41 pub message: String,
43 pub timestamp: DateTime<Utc>,
45 pub fields: Option<BTreeMap<String, String>>,
47}
48
49#[derive(Deserialize)]
50pub struct LogMessageHelper {
51 build_id: Option<BuildId>,
52 dataflow_id: Option<DataflowId>,
53 node_id: Option<NodeId>,
54 daemon_id: Option<DaemonId>,
55 level: LogLevelOrStdout,
56 target: Option<String>,
57 module_path: Option<String>,
58 file: Option<String>,
59 line: Option<u32>,
60 message: Option<String>,
61 timestamp: DateTime<Utc>,
62 fields: Option<BTreeMap<String, String>>,
63}
64
65impl From<LogMessageHelper> for LogMessage {
66 fn from(helper: LogMessageHelper) -> Self {
67 let fields = helper.fields.as_ref();
68 LogMessage {
73 build_id: helper.build_id.or_else(|| {
74 fields
75 .and_then(|f| f.get("build_id").cloned())
76 .and_then(|id| BuildId::from_display_str(&id))
77 }),
78 dataflow_id: helper.dataflow_id.or_else(|| {
79 fields
80 .and_then(|f| f.get("dataflow_id").cloned())
81 .and_then(|id| Uuid::parse_str(&id).ok())
82 }),
83 node_id: helper.node_id.or_else(|| {
84 fields
85 .and_then(|f| f.get("node_id").cloned())
86 .and_then(|id| id.parse::<NodeId>().ok())
87 }),
88 daemon_id: helper.daemon_id.or_else(|| {
89 fields
90 .and_then(|f| f.get("daemon_id").cloned())
91 .and_then(|id| DaemonId::from_display_str(&id))
92 }),
93 level: helper.level,
94 target: helper
95 .target
96 .or_else(|| fields.and_then(|f| f.get("target").cloned())),
97 module_path: helper
98 .module_path
99 .or_else(|| fields.and_then(|f| f.get("module_path").cloned())),
100 file: helper
101 .file
102 .or_else(|| fields.and_then(|f| f.get("file").cloned())),
103 line: helper.line.or_else(|| {
104 fields
105 .and_then(|f| f.get("line").cloned())
106 .and_then(|s| s.parse().ok())
107 }),
108 message: helper
109 .message
110 .or_else(|| fields.and_then(|f| f.get("message").cloned()))
111 .unwrap_or_default(),
112 fields: helper.fields,
113 timestamp: helper.timestamp,
114 }
115 }
116}
117
118#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord)]
119#[serde(rename_all = "UPPERCASE")]
120pub enum LogLevelOrStdout {
121 #[serde(rename = "stdout")]
122 Stdout,
123 #[serde(untagged)]
124 LogLevel(LogLevel),
125}
126
127impl LogLevelOrStdout {
128 pub fn passes(&self, min: &LogLevelOrStdout) -> bool {
161 match (self, min) {
162 (LogLevelOrStdout::Stdout, LogLevelOrStdout::Stdout) => true,
163 (LogLevelOrStdout::Stdout, _) => false,
164 (LogLevelOrStdout::LogLevel(_), LogLevelOrStdout::Stdout) => true,
165 (LogLevelOrStdout::LogLevel(msg), LogLevelOrStdout::LogLevel(max)) => msg <= max,
166 }
167 }
168}
169
170impl From<LogLevel> for LogLevelOrStdout {
171 fn from(level: LogLevel) -> Self {
172 Self::LogLevel(level)
173 }
174}
175
176#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
177pub struct NodeError {
178 pub timestamp: uhlc::Timestamp,
179 pub cause: NodeErrorCause,
180 pub exit_status: NodeExitStatus,
181}
182
183impl std::fmt::Display for NodeError {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 if let NodeErrorCause::FailedToSpawn(err) = &self.cause {
186 return write!(f, "failed to spawn node: {err}");
187 }
188 match &self.exit_status {
189 NodeExitStatus::Success => write!(f, "<success>"),
190 NodeExitStatus::IoError(err) => write!(f, "I/O error while reading exit status: {err}"),
191 NodeExitStatus::ExitCode(code) => write!(f, "exited with code {code}"),
192 NodeExitStatus::Signal(signal) => {
193 let signal_str: Cow<_> = match signal {
194 1 => "SIGHUP".into(),
195 2 => "SIGINT".into(),
196 3 => "SIGQUIT".into(),
197 4 => "SIGILL".into(),
198 6 => "SIGABRT".into(),
199 8 => "SIGFPE".into(),
200 9 => "SIGKILL".into(),
201 11 => "SIGSEGV".into(),
202 13 => "SIGPIPE".into(),
203 14 => "SIGALRM".into(),
204 15 => "SIGTERM".into(),
205 22 => "SIGTTOU".into(),
206 23 => "SIGURG".into(),
207 other => other.to_string().into(),
208 };
209 if matches!(self.cause, NodeErrorCause::GraceDuration) {
210 write!(
211 f,
212 "node was killed by dora because it didn't react to a stop message in time ({signal_str})"
213 )
214 } else {
215 write!(f, "exited because of signal {signal_str}")
216 }
217 }
218 NodeExitStatus::Unknown => write!(f, "unknown exit status"),
219 }?;
220
221 match &self.cause {
222 NodeErrorCause::GraceDuration => {} NodeErrorCause::Cascading { caused_by_node } => write!(
224 f,
225 ". This error occurred because node `{caused_by_node}` exited before connecting to dora."
226 )?,
227 NodeErrorCause::FailedToSpawn(err) => write!(f, "failed to spawn node: {err}")?,
233 NodeErrorCause::Other { stderr } if stderr.is_empty() => {}
234 NodeErrorCause::Other { stderr } => {
235 let line: &str = "---------------------------------------------------------------------------------\n";
236 let stderr = stderr.trim_end();
237 write!(f, " with stderr output:\n{line}{stderr}\n{line}")?
238 }
239 }
240
241 Ok(())
242 }
243}
244
245#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
246pub enum NodeErrorCause {
247 GraceDuration,
249 Cascading {
251 caused_by_node: NodeId,
252 },
253 FailedToSpawn(String),
254 Other {
255 stderr: String,
256 },
257}
258
259#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
266pub enum NodeExitStatus {
267 Success,
269 IoError(String),
271 ExitCode(i32),
273 Signal(i32),
275 Unknown,
277}
278
279impl NodeExitStatus {
280 pub fn is_success(&self) -> bool {
289 matches!(self, NodeExitStatus::Success)
290 }
291}
292
293impl From<Result<std::process::ExitStatus, std::io::Error>> for NodeExitStatus {
294 fn from(result: Result<std::process::ExitStatus, std::io::Error>) -> Self {
295 match result {
296 Ok(status) => {
297 if status.success() {
298 NodeExitStatus::Success
299 } else if let Some(code) = status.code() {
300 Self::ExitCode(code)
301 } else {
302 #[cfg(unix)]
303 {
304 use std::os::unix::process::ExitStatusExt;
305 if let Some(signal) = status.signal() {
306 return Self::Signal(signal);
307 }
308 }
309 Self::Unknown
310 }
311 }
312 Err(err) => Self::IoError(err.to_string()),
313 }
314 }
315}
316
317#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
318pub struct Timestamped<T> {
319 pub inner: T,
320 pub timestamp: uhlc::Timestamp,
321}
322
323impl Timestamped<InterDaemonEvent> {
324 pub fn serialize(&self) -> eyre::Result<Vec<u8>> {
326 crate::encode_presized(self, self.inner.encode_size_hint())
327 .wrap_err("failed to serialize timestamped message")
328 }
329
330 pub fn deserialize_inter_daemon_event(bytes: &[u8]) -> eyre::Result<Self> {
331 crate::decode(bytes).wrap_err("failed to deserialize InterDaemonEvent")
332 }
333}
334
335pub type SharedMemoryId = String;
336
337#[derive(serde::Serialize, serde::Deserialize, Clone)]
338pub enum DataMessage {
339 Vec(#[serde(with = "crate::bulk_bytes")] AVec<u8, ConstAlign<128>>),
340}
341
342impl DataMessage {
343 pub fn len(&self) -> usize {
345 match self {
346 Self::Vec(v) => v.len(),
347 }
348 }
349
350 pub fn is_empty(&self) -> bool {
352 self.len() == 0
353 }
354}
355
356impl fmt::Debug for DataMessage {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 match self {
359 Self::Vec(_) => f
360 .debug_struct("Vec")
361 .field("len", &self.len())
362 .finish_non_exhaustive(),
363 }
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
376pub struct DaemonId {
377 machine_id: Option<String>,
378 uuid: Uuid,
379}
380
381impl DaemonId {
382 pub fn new(machine_id: Option<String>) -> Self {
385 DaemonId {
386 machine_id,
387 uuid: Uuid::now_v7(),
388 }
389 }
390
391 pub fn matches_machine_id(&self, machine_id: &str) -> bool {
395 self.machine_id
396 .as_ref()
397 .map(|id| id == machine_id)
398 .unwrap_or_default()
399 }
400
401 pub fn machine_id(&self) -> Option<&str> {
403 self.machine_id.as_deref()
404 }
405
406 pub fn from_display_str(s: &str) -> Option<Self> {
437 if let Ok(uuid) = Uuid::parse_str(s) {
439 return Some(DaemonId {
440 machine_id: None,
441 uuid,
442 });
443 }
444 const UUID_LEN: usize = 36;
447 let split = s.len().checked_sub(UUID_LEN)?;
448 let uuid = Uuid::parse_str(s.get(split..)?).ok()?;
449 let machine_id = s.get(..split)?.strip_suffix('-')?;
450 Some(DaemonId {
451 machine_id: Some(machine_id.to_string()),
452 uuid,
453 })
454 }
455}
456
457impl std::fmt::Display for DaemonId {
458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459 if let Some(id) = &self.machine_id {
460 write!(f, "{id}-")?;
461 }
462 write!(f, "{}", self.uuid)
463 }
464}
465
466#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
467pub struct GitSource {
468 pub repo: String,
469 pub commit_hash: String,
470 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub subdir: Option<String>,
474 #[serde(default, skip_serializing_if = "Option::is_none")]
479 pub hub: Option<HubProvenance>,
480}
481
482#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
484pub struct HubProvenance {
485 pub name: String,
487 pub version: String,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub manifest_digest: Option<String>,
497}
498
499#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)]
504pub struct BinaryPin {
505 pub platform: String,
507 pub url: String,
509 pub sha256: String,
511 pub hub: HubProvenance,
513}
514
515#[cfg(test)]
517mod tests {
518 use super::*;
519
520 fn node_error_with_signal(signal: i32) -> NodeError {
521 NodeError {
522 timestamp: uhlc::HLC::default().new_timestamp(),
523 cause: NodeErrorCause::Other {
524 stderr: String::new(),
525 },
526 exit_status: NodeExitStatus::Signal(signal),
527 }
528 }
529
530 #[test]
531 fn node_error_signal_display_uses_linux_signal_names() {
532 for (signal, name) in [(6, "SIGABRT"), (22, "SIGTTOU"), (23, "SIGURG")] {
533 assert_eq!(
534 node_error_with_signal(signal).to_string(),
535 format!("exited because of signal {name}")
536 );
537 }
538 }
539
540 #[test]
541 fn node_error_signal_display_falls_back_to_signal_number() {
542 assert_eq!(
543 node_error_with_signal(40).to_string(),
544 "exited because of signal 40"
545 );
546 }
547
548 #[test]
549 fn test_log_message_serialization() {
550 let log_message = LogMessage {
551 build_id: Some(BuildId(Uuid::new_v4())),
552 dataflow_id: Some(DataflowId::from(Uuid::new_v4())),
553 node_id: Some(NodeId("node-1".to_string())),
554 daemon_id: Some(DaemonId::new(Some("machine-1".to_string()))),
555 level: LogLevelOrStdout::LogLevel(LogLevel::Info),
556 target: Some("target".to_string()),
557 module_path: Some("module::path".to_string()),
558 file: Some("file.rs".to_string()),
559 line: Some(42),
560 message: "This is a log message".to_string(),
561 timestamp: Utc::now(),
562 fields: Some(BTreeMap::from([("key".to_string(), "value".to_string())])),
563 };
564 let serialized = serde_yaml::to_string(&log_message).unwrap();
565 let deserialized: LogMessageHelper = serde_yaml::from_str(&serialized).unwrap();
566 assert_eq!(log_message, LogMessage::from(deserialized));
567 }
568
569 #[test]
570 fn stdout_passes_stdout_filter() {
571 let stdout = LogLevelOrStdout::Stdout;
572 assert!(stdout.passes(&LogLevelOrStdout::Stdout));
573 }
574
575 #[test]
583 fn node_id_recovered_from_fields_is_validated() {
584 let make = |node_id: &str| LogMessageHelper {
585 build_id: None,
586 dataflow_id: None,
587 node_id: None,
588 daemon_id: None,
589 level: LogLevelOrStdout::Stdout,
590 target: None,
591 module_path: None,
592 file: None,
593 line: None,
594 message: Some("m".to_string()),
595 timestamp: Utc::now(),
596 fields: Some(BTreeMap::from([(
597 "node_id".to_string(),
598 node_id.to_string(),
599 )])),
600 };
601
602 assert_eq!(
604 LogMessage::from(make("sensor")).node_id,
605 Some("sensor".parse().unwrap())
606 );
607
608 assert_eq!(LogMessage::from(make("a/b")).node_id, None);
611 assert_eq!(LogMessage::from(make("../evil")).node_id, None);
612 }
613
614 #[test]
622 fn build_id_recovered_from_fields_survives_display_round_trip() {
623 let build_id = BuildId(Uuid::new_v4());
624 let make = |value: &str| LogMessageHelper {
625 build_id: None,
626 dataflow_id: None,
627 node_id: None,
628 daemon_id: None,
629 level: LogLevelOrStdout::Stdout,
630 target: None,
631 module_path: None,
632 file: None,
633 line: None,
634 message: Some("m".to_string()),
635 timestamp: Utc::now(),
636 fields: Some(BTreeMap::from([(
637 "build_id".to_string(),
638 value.to_string(),
639 )])),
640 };
641
642 assert_eq!(
644 LogMessage::from(make(&build_id.to_string())).build_id,
645 Some(build_id)
646 );
647 assert_eq!(
649 LogMessage::from(make(&build_id.uuid().to_string())).build_id,
650 Some(build_id)
651 );
652 assert_eq!(LogMessage::from(make("not-a-build-id")).build_id, None);
654 }
655
656 #[test]
661 fn daemon_id_display_roundtrips_through_parse() {
662 let uuid = Uuid::new_v4();
663 for machine in [None, Some("host"), Some("my-host"), Some("a-b-c-d")] {
664 let id = DaemonId {
665 machine_id: machine.map(str::to_string),
666 uuid,
667 };
668 let parsed = DaemonId::from_display_str(&id.to_string())
669 .unwrap_or_else(|| panic!("failed to parse {id}"));
670 assert_eq!(parsed, id, "round-trip failed for machine_id={machine:?}");
671 }
672 }
673
674 #[test]
677 fn daemon_id_parse_edge_cases() {
678 let uuid = Uuid::new_v4();
679 let bare = DaemonId {
680 machine_id: None,
681 uuid,
682 };
683 assert_eq!(DaemonId::from_display_str(&bare.to_string()), Some(bare));
684 assert_eq!(DaemonId::from_display_str("not-a-daemon-id"), None);
685 assert_eq!(DaemonId::from_display_str(""), None);
686 }
687
688 #[test]
689 fn stdout_fails_non_stdout_filters() {
690 let stdout = LogLevelOrStdout::Stdout;
691 assert!(!stdout.passes(&LogLevelOrStdout::LogLevel(LogLevel::Info)));
692 assert!(!stdout.passes(&LogLevelOrStdout::LogLevel(LogLevel::Warn)));
693 assert!(!stdout.passes(&LogLevelOrStdout::LogLevel(LogLevel::Error)));
694 }
695
696 #[test]
697 fn any_log_level_passes_stdout_filter() {
698 let stdout_filter = LogLevelOrStdout::Stdout;
699 for level in [
700 LogLevel::Error,
701 LogLevel::Warn,
702 LogLevel::Info,
703 LogLevel::Debug,
704 LogLevel::Trace,
705 ] {
706 assert!(
707 LogLevelOrStdout::LogLevel(level).passes(&stdout_filter),
708 "{level:?} should pass stdout filter"
709 );
710 }
711 }
712
713 #[test]
714 fn error_passes_less_verbose_filters() {
715 let error = LogLevelOrStdout::LogLevel(LogLevel::Error);
716 assert!(error.passes(&LogLevelOrStdout::LogLevel(LogLevel::Error)));
717 assert!(error.passes(&LogLevelOrStdout::LogLevel(LogLevel::Warn)));
718 assert!(error.passes(&LogLevelOrStdout::LogLevel(LogLevel::Info)));
719 }
720
721 #[test]
722 fn debug_fails_info_filter() {
723 let debug = LogLevelOrStdout::LogLevel(LogLevel::Debug);
724 assert!(!debug.passes(&LogLevelOrStdout::LogLevel(LogLevel::Info)));
725 }
726
727 #[test]
728 fn same_level_passes_itself() {
729 for level in [
730 LogLevel::Error,
731 LogLevel::Warn,
732 LogLevel::Info,
733 LogLevel::Debug,
734 LogLevel::Trace,
735 ] {
736 let l = LogLevelOrStdout::LogLevel(level);
737 assert!(l.passes(&l), "{level:?} should pass itself");
738 }
739 }
740
741 #[test]
742 fn trace_passes_trace_fails_debug() {
743 let trace = LogLevelOrStdout::LogLevel(LogLevel::Trace);
744 assert!(trace.passes(&LogLevelOrStdout::LogLevel(LogLevel::Trace)));
745 assert!(!trace.passes(&LogLevelOrStdout::LogLevel(LogLevel::Debug)));
746 }
747
748 #[test]
749 fn daemon_id_uses_v7_uuid() {
750 let id = DaemonId::new(None);
751 assert_eq!(id.uuid.get_version_num(), 7);
753 }
754}