1use std::collections::BTreeMap;
11use std::collections::hash_map::DefaultHasher;
12use std::hash::Hash;
13use std::hash::Hasher;
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15
16use std::fmt;
17
18use crate::utils::ID;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum ActorType {
23 Planet,
25 Explorer,
27 Orchestrator,
29 User,
31 Broadcast,
33 SelfActor,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Channel {
41 Error,
43 Warning,
45 Info,
50 Debug,
52 Trace,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum EventType {
59 MessagePlanetToOrchestrator,
61 MessageOrchestratorToPlanet,
63 MessagePlanetToExplorer,
65 MessageOrchestratorToExplorer,
67 MessageExplorerToPlanet,
69 MessageExplorerToOrchestrator,
71
72 InternalPlanetAction,
74 InternalExplorerAction,
76 InternalOrchestratorAction,
78
79 UserToPlanet,
81 UserToExplorer,
83 UserToOrchestrator,
85}
86
87pub type Payload = BTreeMap<String, String>;
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct Participant {
93 pub actor_type: ActorType,
95 pub id: ID,
99}
100
101impl Participant {
102 pub fn new(actor_type: ActorType, id: impl Into<ID>) -> Self {
104 Self {
105 actor_type,
106 id: id.into(),
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct LogEvent {
114 pub timestamp_unix: u64,
116 pub sender: Option<Participant>,
118 pub receiver: Option<Participant>,
120 pub event_type: EventType,
122 pub channel: Channel,
124 pub payload: Payload,
126}
127
128impl LogEvent {
129 #[must_use]
136 pub fn new(
137 sender: Option<Participant>,
138 receiver: Option<Participant>,
139 event_type: EventType,
140 channel: Channel,
141 payload: Payload,
142 ) -> Self {
143 let now = SystemTime::now()
144 .duration_since(UNIX_EPOCH)
145 .unwrap_or_else(|_| Duration::from_secs(0))
146 .as_secs();
147
148 Self {
149 timestamp_unix: now,
150 sender,
151 receiver,
152 event_type,
153 channel,
154 payload,
155 }
156 }
157
158 #[must_use]
160 pub fn broadcast(
161 sender: Participant,
162 event_type: EventType,
163 channel: Channel,
164 payload: Payload,
165 ) -> Self {
166 Self::new(Some(sender), None, event_type, channel, payload)
167 }
168
169 #[must_use]
171 pub fn system(event_type: EventType, channel: Channel, payload: Payload) -> Self {
172 Self::new(None, None, event_type, channel, payload)
173 }
174
175 #[must_use]
177 pub fn self_directed(
178 actor: Participant,
179 event_type: EventType,
180 channel: Channel,
181 payload: Payload,
182 ) -> Self {
183 Self::new(
184 Some(actor.clone()),
185 Some(actor),
186 event_type,
187 channel,
188 payload,
189 )
190 }
191
192 #[must_use]
193 pub fn id_from_str(s: &str) -> u64 {
195 let mut hasher = DefaultHasher::new();
196 s.hash(&mut hasher);
197 hasher.finish()
198 }
199
200 pub fn emit(&self) {
206 use Channel::{Debug, Error, Info, Trace, Warning};
207
208 match self.channel {
209 Error => log::error!("{self:?}"),
210 Warning => log::warn!("{self:?}"),
211 Info => log::info!("{self:?}"),
212 Debug => log::debug!("{self:?}"),
213 Trace => log::trace!("{self:?}"),
214 }
215 }
216}
217
218impl fmt::Display for LogEvent {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 let sender = self.sender.as_ref().map_or_else(
221 || "none".to_string(),
222 |p| format!("{:?}#{}", p.actor_type, p.id),
223 );
224
225 let receiver = self.receiver.as_ref().map_or_else(
226 || "none".to_string(),
227 |p| format!("{:?}#{}", p.actor_type, p.id),
228 );
229
230 write!(
231 f,
232 "LogEvent {{ ts: {}, sender: {}, receiver: {}, event: {:?}, channel: {:?}, payload: {:?} }}",
233 self.timestamp_unix, sender, receiver, self.event_type, self.channel, self.payload
234 )
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use log::{Level, Log, Metadata, Record};
242 use std::sync::{Mutex, Once};
243
244 static LOGGER: TestLogger = TestLogger {
245 messages: Mutex::new(Vec::new()),
246 };
247 static LOGGER_INIT: Once = Once::new();
248
249 struct TestLogger {
250 messages: Mutex<Vec<(Level, String)>>,
251 }
252
253 impl Log for TestLogger {
254 fn enabled(&self, _metadata: &Metadata) -> bool {
255 true
256 }
257
258 fn log(&self, record: &Record) {
259 if self.enabled(record.metadata()) {
260 let mut guard = self.messages.lock().expect("logger mutex poisoned");
261 guard.push((record.level(), format!("{}", record.args())));
262 }
263 }
264
265 fn flush(&self) {}
266 }
267
268 fn init_logger() {
269 LOGGER_INIT.call_once(|| {
270 log::set_logger(&LOGGER).expect("failed to install test logger");
271 log::set_max_level(log::LevelFilter::Trace);
272 });
273
274 LOGGER
275 .messages
276 .lock()
277 .expect("logger mutex poisoned")
278 .clear();
279 }
280
281 fn sample_payload() -> Payload {
282 let mut payload = Payload::new();
283 payload.insert("key".into(), "value".into());
284 payload
285 }
286
287 fn sample_participant(actor_type: ActorType, id: ID) -> Participant {
288 Participant::new(actor_type, id)
289 }
290
291 #[test]
292 fn new_populates_timestamp_and_participants() {
293 let sender = sample_participant(ActorType::User, 1);
294 let receiver = sample_participant(ActorType::Planet, 2);
295
296 let event = LogEvent::new(
297 Some(sender.clone()),
298 Some(receiver.clone()),
299 EventType::MessageExplorerToPlanet,
300 Channel::Info,
301 sample_payload(),
302 );
303
304 assert!(event.timestamp_unix > 0);
305 assert_eq!(event.sender, Some(sender));
306 assert_eq!(event.receiver, Some(receiver));
307 }
308
309 #[test]
310 fn id_from_str_is_deterministic() {
311 let id1 = LogEvent::id_from_str("example");
312 let id2 = LogEvent::id_from_str("example");
313 let id3 = LogEvent::id_from_str("different");
314
315 assert_eq!(id1, id2);
316 assert_ne!(id1, id3);
317 }
318
319 #[test]
320 fn broadcast_event_has_no_receiver() {
321 let event = LogEvent::broadcast(
322 sample_participant(ActorType::Explorer, 7),
323 EventType::MessageExplorerToOrchestrator,
324 Channel::Debug,
325 sample_payload(),
326 );
327
328 assert!(event.receiver.is_none());
329 assert!(event.sender.is_some());
330 }
331
332 #[test]
333 fn system_event_has_no_participants() {
334 let event = LogEvent::system(
335 EventType::InternalOrchestratorAction,
336 Channel::Trace,
337 sample_payload(),
338 );
339
340 assert!(event.sender.is_none());
341 assert!(event.receiver.is_none());
342 }
343
344 #[test]
345 fn self_directed_event_sets_both_sides() {
346 let actor = sample_participant(ActorType::Planet, 3);
347 let event = LogEvent::self_directed(
348 actor.clone(),
349 EventType::InternalPlanetAction,
350 Channel::Warning,
351 sample_payload(),
352 );
353
354 assert_eq!(event.sender, Some(actor.clone()));
355 assert_eq!(event.receiver, Some(actor));
356 }
357
358 #[test]
359 fn display_formats_optional_participants() {
360 let mut event = LogEvent::system(
361 EventType::InternalExplorerAction,
362 Channel::Info,
363 sample_payload(),
364 );
365
366 event.timestamp_unix = 42;
367 let rendered = format!("{event}");
368
369 assert!(rendered.contains("ts: 42"));
370 assert!(rendered.contains("sender: none"));
371 assert!(rendered.contains("receiver: none"));
372 }
373
374 #[test]
375 fn emit_writes_to_logger_with_channel_level() {
376 init_logger();
377
378 let mut event = LogEvent::broadcast(
379 sample_participant(ActorType::User, 9),
380 EventType::UserToExplorer,
381 Channel::Error,
382 sample_payload(),
383 );
384
385 event.timestamp_unix = 7;
386 event.emit();
387
388 let guard = LOGGER.messages.lock().expect("logger mutex poisoned");
389
390 let (level, message) = guard.last().expect("expected a logged message");
391 assert_eq!(*level, Level::Error);
392 assert!(message.contains("LogEvent"));
393 assert!(message.contains("sender:"));
394 }
395}