Skip to main content

netget/protocol/
event_logger.rs

1//! Event lifecycle logging
2//!
3//! Provides centralized logging for event processing lifecycle:
4//! - Event start (when event is received)
5//! - Event completion (when actions are executed)
6//! - Action execution logging
7//!
8//! Uses LogTemplate for protocol-specific log formatting.
9
10use crate::llm::actions::protocol_trait::ActionResult;
11use crate::protocol::log_template::{LogLevel, LogTemplate};
12use crate::protocol::Event;
13use crate::server::connection::ConnectionId;
14use crate::state::ServerId;
15use crate::utils::clock::Instant;
16use serde_json::Value;
17use std::net::SocketAddr;
18use tokio::sync::mpsc::UnboundedSender;
19use tracing::{debug, info, trace};
20
21/// Context for logging an event's lifecycle
22///
23/// Tracks timing and context information from event start to completion.
24/// Provides methods to log at appropriate levels using the event's LogTemplate.
25///
26/// # Example
27/// ```rust,ignore
28/// let ctx = EventLogContext::new(
29///     &event,
30///     server_id,
31///     Some(connection_id),
32///     Some(client_addr),
33///     "HTTP",
34/// );
35///
36/// ctx.log_start(Some(&status_tx));
37///
38/// // ... process event, get action results ...
39///
40/// ctx.log_complete(Some(&status_tx), &action_results);
41/// ```
42pub struct EventLogContext<'a> {
43    /// Reference to the event being logged
44    pub event: &'a Event,
45    /// When event processing started
46    pub start_time: Instant,
47    /// Server ID processing this event
48    pub server_id: ServerId,
49    /// Connection ID if applicable
50    pub connection_id: Option<ConnectionId>,
51    /// Client address if known
52    pub client_addr: Option<SocketAddr>,
53    /// Protocol name for fallback logging
54    pub protocol_name: String,
55}
56
57impl<'a> EventLogContext<'a> {
58    /// Create a new event log context
59    ///
60    /// Automatically records the start time for duration tracking.
61    pub fn new(
62        event: &'a Event,
63        server_id: ServerId,
64        connection_id: Option<ConnectionId>,
65        client_addr: Option<SocketAddr>,
66        protocol_name: impl Into<String>,
67    ) -> Self {
68        Self {
69            event,
70            start_time: Instant::now(),
71            server_id,
72            connection_id,
73            client_addr,
74            protocol_name: protocol_name.into(),
75        }
76    }
77
78    /// Build enriched data with context fields
79    ///
80    /// Adds client_ip, client_port, server_id, connection_id to the event data.
81    ///
82    /// `pub` so `tests/event_logger_test.rs` can assert on the enrichment
83    /// directly — CLAUDE.md forbids unit-test modules in `src/`, so an
84    /// internal helper has to be reachable to be tested.
85    pub fn build_enriched_data(&self) -> Value {
86        let mut data = self.event.data.clone();
87        if let Some(obj) = data.as_object_mut() {
88            if let Some(addr) = self.client_addr {
89                obj.insert(
90                    "client_ip".to_string(),
91                    serde_json::json!(addr.ip().to_string()),
92                );
93                obj.insert("client_port".to_string(), serde_json::json!(addr.port()));
94            }
95            obj.insert(
96                "server_id".to_string(),
97                serde_json::json!(self.server_id.as_u32()),
98            );
99            if let Some(conn_id) = self.connection_id {
100                obj.insert(
101                    "connection_id".to_string(),
102                    serde_json::json!(conn_id.as_u32()),
103                );
104            }
105            obj.insert(
106                "protocol".to_string(),
107                serde_json::json!(self.protocol_name.clone()),
108            );
109            obj.insert("event_id".to_string(), serde_json::json!(self.event.id()));
110        }
111        data
112    }
113
114    /// Log event start (DEBUG level)
115    ///
116    /// Called when an event is received and about to be processed.
117    /// Uses the event's debug template if available, otherwise falls back
118    /// to a generic message.
119    pub fn log_start(&self, status_tx: Option<&UnboundedSender<String>>) {
120        let data = self.build_enriched_data();
121
122        if let Some(ref template) = self.event.event_type.log_template {
123            if let Some(msg) = template.render(LogLevel::Debug, &data) {
124                debug!("{}", msg);
125                if let Some(tx) = status_tx {
126                    let _ = tx.send(format!("[DEBUG] {}", msg));
127                }
128                return;
129            }
130        }
131
132        // Fallback: log event ID and basic info
133        let fallback_msg = format!(
134            "{} event '{}' from {}",
135            self.protocol_name,
136            self.event.id(),
137            self.client_addr
138                .map(|a| a.to_string())
139                .unwrap_or_else(|| "unknown".to_string())
140        );
141        debug!("{}", fallback_msg);
142        if let Some(tx) = status_tx {
143            let _ = tx.send(format!("[DEBUG] {}", fallback_msg));
144        }
145    }
146
147    /// Log event completion (INFO/DEBUG/TRACE levels)
148    ///
149    /// Called after actions have been executed.
150    /// Enriches data with timing and response information.
151    ///
152    /// Logs at:
153    /// - INFO: One-liner access log style
154    /// - DEBUG: With timing information
155    /// - TRACE: Full details
156    pub fn log_complete(
157        &self,
158        status_tx: Option<&UnboundedSender<String>>,
159        action_results: &[ActionResult],
160    ) {
161        let duration = self.start_time.elapsed();
162        let mut data = self.build_enriched_data();
163
164        // Add timing and result info
165        if let Some(obj) = data.as_object_mut() {
166            obj.insert(
167                "duration_ms".to_string(),
168                serde_json::json!(duration.as_millis() as u64),
169            );
170            obj.insert(
171                "action_count".to_string(),
172                serde_json::json!(action_results.len()),
173            );
174
175            // Calculate total response bytes
176            let response_bytes: usize = action_results
177                .iter()
178                .flat_map(|r| r.get_all_output())
179                .map(|o| o.len())
180                .sum();
181            obj.insert(
182                "response_bytes".to_string(),
183                serde_json::json!(response_bytes),
184            );
185
186            // Add status from first Custom result if present
187            for result in action_results {
188                if let ActionResult::Custom {
189                    name,
190                    data: custom_data,
191                } = result
192                {
193                    if let Some(status) = custom_data.get("status") {
194                        obj.insert("status".to_string(), status.clone());
195                    }
196                    obj.insert("action_name".to_string(), serde_json::json!(name));
197                    break;
198                }
199            }
200        }
201
202        if let Some(ref template) = self.event.event_type.log_template {
203            // TRACE: Full details
204            if let Some(msg) = template.render(LogLevel::Trace, &data) {
205                trace!("{}", msg);
206                if let Some(tx) = status_tx {
207                    let _ = tx.send(format!("[TRACE] {}", msg));
208                }
209            }
210
211            // INFO: Access log style (one-liner)
212            if let Some(msg) = template.render(LogLevel::Info, &data) {
213                info!("{}", msg);
214                if let Some(tx) = status_tx {
215                    let _ = tx.send(format!("[INFO] {}", msg));
216                }
217            }
218
219            // DEBUG: With timing (always log at debug for event completion)
220            if let Some(msg) = template.render(LogLevel::Debug, &data) {
221                let msg_with_timing = format!("{} ({}ms)", msg, duration.as_millis());
222                debug!("{}", msg_with_timing);
223                if let Some(tx) = status_tx {
224                    let _ = tx.send(format!("[DEBUG] {}", msg_with_timing));
225                }
226            } else {
227                // Fallback debug with timing
228                let fallback_msg = format!(
229                    "{} '{}' completed in {}ms, {} action(s), {} bytes",
230                    self.protocol_name,
231                    self.event.id(),
232                    duration.as_millis(),
233                    action_results.len(),
234                    data.get("response_bytes")
235                        .and_then(|v| v.as_u64())
236                        .unwrap_or(0)
237                );
238                debug!("{}", fallback_msg);
239                if let Some(tx) = status_tx {
240                    let _ = tx.send(format!("[DEBUG] {}", fallback_msg));
241                }
242            }
243        } else {
244            // No template: fallback logging
245            let fallback_msg = format!(
246                "{} '{}' completed in {}ms, {} action(s)",
247                self.protocol_name,
248                self.event.id(),
249                duration.as_millis(),
250                action_results.len()
251            );
252            debug!("{}", fallback_msg);
253            if let Some(tx) = status_tx {
254                let _ = tx.send(format!("[DEBUG] {}", fallback_msg));
255            }
256        }
257    }
258}
259
260/// Log action execution result
261///
262/// Called after a protocol action has been executed.
263/// Uses the action's log template if available.
264///
265/// # Arguments
266/// * `action_name` - The action type name (e.g., "send_http_response")
267/// * `action_data` - The action JSON data
268/// * `result` - The ActionResult from execution
269/// * `template` - Optional LogTemplate from the ActionDefinition
270/// * `status_tx` - Optional channel for TUI updates
271pub fn log_action_result(
272    action_name: &str,
273    action_data: &Value,
274    result: &ActionResult,
275    template: Option<&LogTemplate>,
276    status_tx: Option<&UnboundedSender<String>>,
277) {
278    // Enrich action data with result info
279    let mut enriched_data = action_data.clone();
280    if let Some(obj) = enriched_data.as_object_mut() {
281        // Add output info
282        let output_bytes: usize = result.get_all_output().iter().map(|o| o.len()).sum();
283        obj.insert("output_bytes".to_string(), serde_json::json!(output_bytes));
284        obj.insert(
285            "closes_connection".to_string(),
286            serde_json::json!(result.closes_connection()),
287        );
288        obj.insert(
289            "waits_for_more".to_string(),
290            serde_json::json!(result.waits_for_more()),
291        );
292        obj.insert("action_name".to_string(), serde_json::json!(action_name));
293    }
294
295    if let Some(template) = template {
296        // Use template if available
297        if let Some(msg) = template.render(LogLevel::Trace, &enriched_data) {
298            trace!("Action {}: {}", action_name, msg);
299            if let Some(tx) = status_tx {
300                let _ = tx.send(format!("[TRACE] Action {}: {}", action_name, msg));
301            }
302        }
303
304        if let Some(msg) = template.render(LogLevel::Debug, &enriched_data) {
305            debug!("Action {}: {}", action_name, msg);
306            if let Some(tx) = status_tx {
307                let _ = tx.send(format!("[DEBUG] Action {}: {}", action_name, msg));
308            }
309        }
310
311        if let Some(msg) = template.render(LogLevel::Info, &enriched_data) {
312            info!("Action {}: {}", action_name, msg);
313            if let Some(tx) = status_tx {
314                let _ = tx.send(format!("[INFO] Action {}: {}", action_name, msg));
315            }
316        }
317    } else {
318        // Fallback to summarize_action
319        let summary = crate::llm::actions::summarize_action(action_data);
320        debug!("Action executed: {}", summary);
321        if let Some(tx) = status_tx {
322            let _ = tx.send(format!("[DEBUG] Action: {}", summary));
323        }
324    }
325}