tap_node/event/
logger.rs

1//! # Event Logger for TAP Node
2//!
3//! This module provides an event logging system that captures all node events
4//! and logs them to a configurable location. It implements the `EventSubscriber`
5//! trait to receive events via callbacks from the event bus.
6//!
7//! The event logger supports different output formats and destinations, including:
8//! - Console logging via the standard logging framework
9//! - File-based logging with rotation support
10//! - Structured JSON logging for machine readability
11//!
12//! ## Usage
13//!
14//! ```no_run
15//! use std::sync::Arc;
16//! use tap_node::{NodeConfig, TapNode};
17//! use tap_node::event::logger::{EventLogger, EventLoggerConfig, LogDestination};
18//!
19//! async fn example() {
20//!     // Create a new TAP node
21//!     let node = TapNode::new(NodeConfig::default());
22//!
23//!     // Configure the event logger
24//!     let logger_config = EventLoggerConfig {
25//!         destination: LogDestination::File {
26//!             path: "/var/log/tap-node/events.log".to_string(),
27//!             max_size: Some(10 * 1024 * 1024), // 10 MB
28//!             rotate: true,
29//!         },
30//!         structured: true, // Use JSON format
31//!         log_level: log::Level::Info,
32//!     };
33//!
34//!     // Create and subscribe the event logger
35//!     let event_logger = Arc::new(EventLogger::new(logger_config));
36//!     node.event_bus().subscribe(event_logger).await;
37//!
38//!     // The event logger will now receive and log all events
39//! }
40//! ```
41
42use std::fmt;
43use std::fs::{File, OpenOptions};
44use std::io::{self, Write};
45use std::path::Path;
46use std::sync::{Arc, Mutex};
47use std::time::SystemTime;
48
49use async_trait::async_trait;
50use chrono::{DateTime, Utc};
51use serde_json::json;
52use tracing::{debug, error, info, trace, warn};
53
54use crate::error::{Error, Result};
55use crate::event::{EventSubscriber, NodeEvent};
56
57/// Configuration for where event logs should be sent
58#[derive(Clone)]
59pub enum LogDestination {
60    /// Log to the console via the standard logging framework
61    Console,
62
63    /// Log to a file with optional rotation
64    File {
65        /// Path to the log file
66        path: String,
67
68        /// Maximum file size before rotation (in bytes)
69        max_size: Option<usize>,
70
71        /// Whether to rotate log files when they reach max_size
72        rotate: bool,
73    },
74
75    /// Custom logging function
76    Custom(Arc<dyn Fn(&str) + Send + Sync>),
77}
78
79// Custom Debug implementation that doesn't try to print the function pointer
80impl fmt::Debug for LogDestination {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            LogDestination::Console => write!(f, "LogDestination::Console"),
84            LogDestination::File {
85                path,
86                max_size,
87                rotate,
88            } => f
89                .debug_struct("LogDestination::File")
90                .field("path", path)
91                .field("max_size", max_size)
92                .field("rotate", rotate)
93                .finish(),
94            LogDestination::Custom(_) => write!(f, "LogDestination::Custom(<function>)"),
95        }
96    }
97}
98
99/// Configuration for the event logger
100#[derive(Debug, Clone)]
101pub struct EventLoggerConfig {
102    /// Where to send the log output
103    pub destination: LogDestination,
104
105    /// Whether to use structured (JSON) logging
106    pub structured: bool,
107
108    /// The log level to use
109    pub log_level: log::Level,
110}
111
112impl Default for EventLoggerConfig {
113    fn default() -> Self {
114        Self {
115            destination: LogDestination::Console,
116            structured: false,
117            log_level: log::Level::Info,
118        }
119    }
120}
121
122/// Event logger for TAP Node
123///
124/// This component subscribes to the node's event bus and logs all events
125/// to the configured destination. It supports both plain text and structured
126/// (JSON) logging, and can output to the console or files.
127pub struct EventLogger {
128    /// Configuration for the logger
129    config: EventLoggerConfig,
130
131    /// File handle if using file destination
132    file: Option<Arc<Mutex<File>>>,
133}
134
135impl EventLogger {
136    /// Create a new event logger with the given configuration
137    pub fn new(config: EventLoggerConfig) -> Self {
138        let file = match &config.destination {
139            LogDestination::File { path, .. } => match Self::open_log_file(path) {
140                Ok(file) => Some(Arc::new(Mutex::new(file))),
141                Err(err) => {
142                    error!("Failed to open log file {}: {}", path, err);
143                    None
144                }
145            },
146            _ => None,
147        };
148
149        Self { config, file }
150    }
151
152    /// Open or create a log file
153    fn open_log_file(path: &str) -> io::Result<File> {
154        // Ensure directory exists
155        if let Some(parent) = Path::new(path).parent() {
156            std::fs::create_dir_all(parent)?;
157        }
158
159        // Open or create the file
160        OpenOptions::new().create(true).append(true).open(path)
161    }
162
163    /// Log an event to the configured destination
164    fn log_event(&self, event: &NodeEvent) -> Result<()> {
165        let log_message = if self.config.structured {
166            self.format_structured_log(event)?
167        } else {
168            self.format_plain_log(event)
169        };
170
171        match &self.config.destination {
172            LogDestination::Console => {
173                // Use the standard logging framework
174                match self.config.log_level {
175                    log::Level::Error => error!("{}", log_message),
176                    log::Level::Warn => warn!("{}", log_message),
177                    log::Level::Info => info!("{}", log_message),
178                    log::Level::Debug => debug!("{}", log_message),
179                    log::Level::Trace => trace!("{}", log_message),
180                }
181                Ok(())
182            }
183            LogDestination::File { .. } => {
184                if let Some(file) = &self.file {
185                    let mut file_guard = file.lock().map_err(|_| {
186                        Error::Configuration("Failed to acquire log file lock".to_string())
187                    })?;
188
189                    // Write to the file with newline
190                    writeln!(file_guard, "{}", log_message).map_err(|err| {
191                        Error::Configuration(format!("Failed to write to log file: {}", err))
192                    })?;
193
194                    // Ensure the log is flushed
195                    file_guard.flush().map_err(|err| {
196                        Error::Configuration(format!("Failed to flush log file: {}", err))
197                    })?;
198
199                    Ok(())
200                } else {
201                    // Fall back to console logging if file isn't available
202                    error!("{}", log_message);
203                    Ok(())
204                }
205            }
206            LogDestination::Custom(func) => {
207                // Call the custom logging function
208                func(&log_message);
209                Ok(())
210            }
211        }
212    }
213
214    /// Format an event as a plain text log message
215    fn format_plain_log(&self, event: &NodeEvent) -> String {
216        let timestamp = DateTime::<Utc>::from(SystemTime::now()).format("%Y-%m-%dT%H:%M:%S%.3fZ");
217
218        match event {
219            NodeEvent::PlainMessageReceived { message } => {
220                format!("[{}] MESSAGE RECEIVED: {}", timestamp, message)
221            }
222            NodeEvent::PlainMessageSent { message, from, to } => {
223                format!(
224                    "[{}] MESSAGE SENT: from={}, to={}, message={}",
225                    timestamp, from, to, message
226                )
227            }
228            NodeEvent::AgentRegistered { did } => {
229                format!("[{}] AGENT REGISTERED: {}", timestamp, did)
230            }
231            NodeEvent::AgentUnregistered { did } => {
232                format!("[{}] AGENT UNREGISTERED: {}", timestamp, did)
233            }
234            NodeEvent::DidResolved { did, success } => {
235                format!(
236                    "[{}] DID RESOLVED: did={}, success={}",
237                    timestamp, did, success
238                )
239            }
240            NodeEvent::AgentPlainMessage { did, message } => {
241                format!(
242                    "[{}] AGENT MESSAGE: did={}, message_length={}",
243                    timestamp,
244                    did,
245                    message.len()
246                )
247            }
248        }
249    }
250
251    /// Format an event as a structured (JSON) log message
252    fn format_structured_log(&self, event: &NodeEvent) -> Result<String> {
253        // Create common fields for all event types
254        let timestamp = DateTime::<Utc>::from(SystemTime::now()).to_rfc3339();
255
256        // Create event-specific fields
257        let (event_type, event_data) = match event {
258            NodeEvent::PlainMessageReceived { message } => (
259                "message_received",
260                json!({
261                    "message": message,
262                }),
263            ),
264            NodeEvent::PlainMessageSent { message, from, to } => (
265                "message_sent",
266                json!({
267                    "from": from,
268                    "to": to,
269                    "message": message,
270                }),
271            ),
272            NodeEvent::AgentRegistered { did } => (
273                "agent_registered",
274                json!({
275                    "did": did,
276                }),
277            ),
278            NodeEvent::AgentUnregistered { did } => (
279                "agent_unregistered",
280                json!({
281                    "did": did,
282                }),
283            ),
284            NodeEvent::DidResolved { did, success } => (
285                "did_resolved",
286                json!({
287                    "did": did,
288                    "success": success,
289                }),
290            ),
291            NodeEvent::AgentPlainMessage { did, message } => (
292                "agent_message",
293                json!({
294                    "did": did,
295                    "message_length": message.len(),
296                }),
297            ),
298        };
299
300        // Combine into a single JSON object
301        let log_entry = json!({
302            "timestamp": timestamp,
303            "event_type": event_type,
304            "data": event_data,
305        });
306
307        // Serialize to a string
308        serde_json::to_string(&log_entry).map_err(Error::Serialization)
309    }
310}
311
312#[async_trait]
313impl EventSubscriber for EventLogger {
314    async fn handle_event(&self, event: NodeEvent) {
315        if let Err(err) = self.log_event(&event) {
316            error!("Failed to log event: {}", err);
317        }
318    }
319}
320
321impl fmt::Debug for EventLogger {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        f.debug_struct("EventLogger")
324            .field("config", &self.config)
325            .field("file", &self.file.is_some())
326            .finish()
327    }
328}