1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! Diagnostic logging utilities
//!
//! Logs diagnostic information to a logfile. This information is sent
//! via the environment manager to session-ingress to monitor issues from
//! within the container.
//!
//! *Important* - this function MUST NOT be called with any PII, including
//! file paths, project names, repo names, prompts, etc.
use crate::constants::env::ai_code;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::Path;
use serde::{Deserialize, Serialize};
/// Diagnostic log level
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DiagnosticLogLevel {
Debug,
Info,
Warn,
Error,
}
/// A diagnostic log entry
#[derive(Debug, Serialize, Deserialize)]
pub struct DiagnosticLogEntry {
pub timestamp: String,
pub level: DiagnosticLogLevel,
pub event: String,
pub data: std::collections::HashMap<String, serde_json::Value>,
}
/// Get the diagnostic log file path from environment
fn get_diagnostic_log_file() -> Option<String> {
std::env::var(ai_code::DIAGNOSTICS_FILE).ok()
}
/// Logs diagnostic information to a logfile. This information is sent
/// via the environment manager to session-ingress to monitor issues from
/// within the container.
///
/// *Important* - this function MUST NOT be called with any PII, including
/// file paths, project names, repo names, prompts, etc.
///
/// # Arguments
/// * `level` - Log level. Only used for information, not filtering
/// * `event` - A specific event: "started", "mcp_connected", etc.
/// * `data` - Optional additional data to log
pub fn log_for_diagnostics_no_pii(
level: DiagnosticLogLevel,
event: &str,
data: Option<std::collections::HashMap<String, serde_json::Value>>,
) {
let log_file = match get_diagnostic_log_file() {
Some(path) => path,
None => return,
};
let entry = DiagnosticLogEntry {
timestamp: chrono::Utc::now().to_rfc3339(),
level,
event: event.to_string(),
data: data.unwrap_or_default(),
};
let line = serde_json::to_string(&entry).unwrap_or_default();
let line = format!("{}\n", line);
// Try to append first
if let Ok(file) = OpenOptions::new()
.create(true)
.append(true)
.open(&log_file)
{
if file.write_all(line.as_bytes()).is_ok() {
return;
}
}
// If append fails, try creating the directory first
if let Some(parent) = Path::new(&log_file).parent() {
let _ = fs::create_dir_all(parent);
let _ = OpenOptions::new()
.create(true)
.append(true)
.open(&log_file)
.and_then(|mut file| file.write_all(line.as_bytes()));
}
}
/// Wraps an async function with diagnostic timing logs.
/// Logs `{event}_started` before execution and `{event}_completed` after with duration_ms.
///
/// # Arguments
/// * `event` - Event name prefix (e.g., "git_status" -> logs "git_status_started" and "git_status_completed")
/// * `fut` - Async function to execute and time
/// * `get_data` - Optional function to extract additional data from the result for the completion log
/// # Returns
/// The result of the wrapped function
pub async fn with_diagnostics_timing<T, F, R>(
event: &str,
fut: F,
get_data: Option<R>,
) -> Result<T, Box<dyn std::error::Error + Send + Sync>>
where
F: Future<Output = Result<T, Box<dyn std::error::Error + Send + Sync>>>,
R: FnOnce(&T) -> std::collections::HashMap<String, serde_json::Value>,
{
let start_time = std::time::Instant::now();
log_for_diagnostics_no_pii(DiagnosticLogLevel::Info, &format!("{}_started", event), None);
match fut.await {
Ok(result) => {
let mut additional_data = std::collections::HashMap::new();
additional_data.insert(
"duration_ms".to_string(),
serde_json::json!(start_time.elapsed().as_millis() as u64),
);
if let Some(ref get_data_fn) = get_data {
additional_data.extend(get_data_fn(&result));
}
log_for_diagnostics_no_pii(
DiagnosticLogLevel::Info,
&format!("{}_completed", event),
Some(additional_data),
);
Ok(result)
}
Err(error) => {
let mut data = std::collections::HashMap::new();
data.insert(
"duration_ms".to_string(),
serde_json::json!(start_time.elapsed().as_millis() as u64),
);
log_for_diagnostics_no_pii(
DiagnosticLogLevel::Error,
&format!("{}_failed", event),
Some(data),
);
Err(error)
}
}
}