common_game/
logging.rs

1//! Logging module for structured log events within the game application.
2//!
3//! This module defines standardized log channels, event types, and data
4//! structures to facilitate consistent logging across different components
5//! of the game, such as planets, explorers, and the orchestrator.
6//!
7//! It provides mechanisms to create log events with timestamps, participants,
8//! and payloads, as well as utilities to emit these events using the `log` crate
9//! for integration with various logging backends.
10use 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/// Sender or receiver classification for a log event.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum ActorType {
23    /// Planet entity
24    Planet,
25    /// Explorer entity
26    Explorer,
27    /// Orchestrator entity
28    Orchestrator,
29    /// User command
30    User,
31    /// System-wide
32    Broadcast,
33    /// Self-directed (same sender and receiver)
34    SelfActor,
35}
36
37/// Standardized log channels shared across the application.
38/// Note: "event" here means a series of messages with a specific effect
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum Channel {
41    /// Anything that leads to a panic
42    Error,
43    /// Unexpected behavior that doesn’t stop the game/lead to a panic
44    Warning,
45    /// Important events, to be emitted by the Orchestrator once the last ack message in the conversation is recieved.
46    /// The events this level should be used for are:
47    /// - [`Planet`](`crate::components::planet`) creation,destruction,start,stop
48    /// - [`Explorer`](crate#explorer) movement,death,start/stop
49    Info,
50    /// All other events that are not covered by [`Channel::Info`]
51    Debug,
52    /// All messages
53    Trace,
54}
55
56/// High-level event categories.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum EventType {
59    /// Message between planet and orchestrator
60    MessagePlanetToOrchestrator,
61    /// Message between orchestrator and planet
62    MessageOrchestratorToPlanet,
63    /// Message between planet and explorer
64    MessagePlanetToExplorer,
65    /// Message between orchestrator and explorer
66    MessageOrchestratorToExplorer,
67    /// Message between explorer and planet
68    MessageExplorerToPlanet,
69    /// Message between explorer and orchestrator
70    MessageExplorerToOrchestrator,
71
72    /// Internal planet action
73    InternalPlanetAction,
74    /// Internal explorer action
75    InternalExplorerAction,
76    /// Internal orchestrator action
77    InternalOrchestratorAction,
78
79    /// User command to planet
80    UserToPlanet,
81    /// User command to explorer
82    UserToExplorer,
83    /// User command to orchestrator
84    UserToOrchestrator,
85}
86
87/// Simple key–value payload: string → string.
88pub type Payload = BTreeMap<String, String>;
89
90/// Participant in a log event. Either side of an interaction can be absent.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct Participant {
93    /// Entity role that produced or received the event.
94    pub actor_type: ActorType,
95    /// Stable identifier for the actor.
96    ///
97    /// It is suggested to use id 0 for [`ActorType::Orchestrator`].
98    pub id: ID,
99}
100
101impl Participant {
102    /// Create a new participant with a concrete type and identifier.
103    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/// Bundle of data emitted through the logging system.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct LogEvent {
114    /// UNIX timestamp in seconds when the event was created.
115    pub timestamp_unix: u64,
116    /// Optional sender of the event.
117    pub sender: Option<Participant>,
118    /// Optional receiver of the event.
119    pub receiver: Option<Participant>,
120    /// High-level event category.
121    pub event_type: EventType,
122    /// Logging channel / severity level.
123    pub channel: Channel,
124    /// Arbitrary key–value payload.
125    pub payload: Payload,
126}
127
128impl LogEvent {
129    /// Create an event with the current UNIX timestamp and optional participants.
130    ///
131    /// The timestamp uses `SystemTime::now().duration_since(UNIX_EPOCH)` and
132    /// falls back to `0` if the clock is earlier than the Unix epoch. This
133    /// avoids panics on misconfigured systems while still producing a stable,
134    /// clearly out-of-band value.
135    #[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    /// Convenience: broadcast from a known sender to no specific receiver.
159    #[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    /// Convenience: emit an event without sender or receiver (e.g. system state).
170    #[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    /// Convenience: emit an event where sender and receiver are the same actor.
176    #[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    /// Generate a deterministic identifier from an arbitrary string.
194    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    /// Emit this event using the `log` crate.
201    ///
202    /// Uses the `Debug` representation to preserve all structured fields. If no
203    /// logger is initialized by the final binary this is a no-op, which is fine
204    /// for library consumers.
205    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}