1use std::fs::OpenOptions;
6use std::io::Write;
7use std::path::Path;
8use std::sync::Mutex;
9
10use chrono::Utc;
11use serde_json::json;
12use tracing_subscriber::{prelude::*, EnvFilter};
13use uuid::Uuid;
14
15static SECURITY_EVENTS_PATH: Mutex<Option<String>> = Mutex::new(None);
16
17#[derive(Clone, Copy)]
19pub enum TracingMode {
20 Default,
22 Chat,
24}
25
26pub fn init_tracing(mode: TracingMode) {
29 let cfg = crate::config::ObservabilityConfig::from_env();
30 let mut level: String = if cfg.quiet {
31 "skilllite=warn".to_string()
32 } else {
33 cfg.log_level.clone()
34 };
35
36 if matches!(mode, TracingMode::Chat) {
38 level = format!("{},skilllite::agent=error", level);
39 }
40
41 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&level));
42
43 let json = cfg.log_json;
44
45 let _ = if json {
46 tracing_subscriber::registry()
47 .with(filter)
48 .with(
49 tracing_subscriber::fmt::layer()
50 .json()
51 .with_target(true)
52 .with_thread_ids(false),
53 )
54 .try_init()
55 } else {
56 tracing_subscriber::registry()
57 .with(filter)
58 .with(
59 tracing_subscriber::fmt::layer()
60 .with_target(true)
61 .with_thread_ids(false),
62 )
63 .try_init()
64 };
65}
66
67fn get_audit_path() -> Option<String> {
69 let base = crate::config::ObservabilityConfig::from_env()
70 .audit_log
71 .clone()?;
72 if base.is_empty() {
73 return None;
74 }
75 let path = Path::new(&base);
76 let file_path = if base.ends_with(".jsonl") {
77 path.to_path_buf()
78 } else {
79 let today = chrono::Utc::now().format("%Y-%m-%d");
80 path.join(format!("audit_{}.jsonl", today))
81 };
82 let file_path_str = file_path.to_string_lossy().into_owned();
83 if let Some(parent) = file_path.parent() {
84 let _ = std::fs::create_dir_all(parent);
85 }
86 Some(file_path_str)
87}
88
89fn get_security_events_path() -> Option<String> {
90 {
91 let guard = SECURITY_EVENTS_PATH.lock().ok()?;
92 if let Some(ref p) = *guard {
93 return Some(p.clone());
94 }
95 }
96 let path = crate::config::ObservabilityConfig::from_env()
97 .security_events_log
98 .clone()?;
99 if path.is_empty() {
100 return None;
101 }
102 if let Some(parent) = Path::new(&path).parent() {
103 let _ = std::fs::create_dir_all(parent);
104 }
105 {
106 let mut guard = SECURITY_EVENTS_PATH.lock().ok()?;
107 *guard = Some(path.clone());
108 }
109 Some(path)
110}
111
112fn append_jsonl(path: &str, record: &serde_json::Value) {
113 if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {
114 if let Ok(line) = serde_json::to_string(record) {
115 let _ = writeln!(f, "{}", line);
116 let _ = f.flush(); }
118 }
119}
120
121pub fn audit_confirmation_requested(
123 skill_id: &str,
124 code_hash: &str,
125 issues_count: usize,
126 severity: &str,
127) {
128 if let Some(path) = get_audit_path() {
129 let record = json!({
130 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
131 "event": "confirmation_requested",
132 "skill_id": skill_id,
133 "code_hash": code_hash,
134 "issues_count": issues_count,
135 "severity": severity,
136 "source": "rust"
137 });
138 append_jsonl(&path, &record);
139 }
140}
141
142pub fn audit_confirmation_response(skill_id: &str, approved: bool, source: &str) {
144 if let Some(path) = get_audit_path() {
145 let record = json!({
146 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
147 "event": "confirmation_response",
148 "skill_id": skill_id,
149 "approved": approved,
150 "source": source,
151 "source_layer": "rust"
152 });
153 append_jsonl(&path, &record);
154 }
155}
156
157pub fn audit_execution_started(skill_id: &str, cmd: &str, args: &[&str], cwd: &str) {
161 if let Some(path) = get_audit_path() {
162 let record = json!({
163 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
164 "event": "execution_started",
165 "skill_id": skill_id,
166 "cmd": cmd,
167 "args": args,
168 "cwd": cwd,
169 "source": "rust"
170 });
171 append_jsonl(&path, &record);
172 }
173}
174
175pub fn audit_command_invoked(skill_id: &str, cmd: &str, args: &[&str], cwd: &str) {
177 audit_execution_started(skill_id, cmd, args, cwd);
178}
179
180pub fn audit_execution_completed(
182 skill_id: &str,
183 exit_code: i32,
184 duration_ms: u64,
185 stdout_len: usize,
186) {
187 if let Some(path) = get_audit_path() {
188 let record = json!({
189 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
190 "event": "execution_completed",
191 "skill_id": skill_id,
192 "exit_code": exit_code,
193 "duration_ms": duration_ms,
194 "stdout_len": stdout_len,
195 "success": exit_code == 0,
196 "source": "rust"
197 });
198 append_jsonl(&path, &record);
199 }
200}
201
202pub fn audit_skill_invocation(
204 skill_id: &str,
205 entry_point: &str,
206 cwd: &str,
207 input_json: &str,
208 output: &str,
209 exit_code: i32,
210 duration_ms: u64,
211) {
212 if let Some(path) = get_audit_path() {
213 let context = crate::config::loader::env_optional(
214 crate::config::env_keys::observability::SKILLLITE_AUDIT_CONTEXT,
215 &[],
216 )
217 .unwrap_or_else(|| "cli".to_string());
218 let input_summary = input_summary_bytes(input_json);
219 let output_summary = output_summary_bytes(output);
220 let record = json!({
221 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
222 "event": "skill_invocation",
223 "skill_id": skill_id,
224 "entry_point": entry_point,
225 "cwd": cwd,
226 "context": context,
227 "input_summary": input_summary,
228 "output_summary": output_summary,
229 "exit_code": exit_code,
230 "duration_ms": duration_ms,
231 "success": exit_code == 0,
232 "source": "rust"
233 });
234 append_jsonl(&path, &record);
235 }
236}
237
238fn input_summary_bytes(input: &str) -> serde_json::Value {
239 let preview: String = input.chars().take(100).collect();
240 let truncated = input.chars().count() > 100;
241 serde_json::json!({"len": input.len(), "preview": if truncated { format!("{}...", preview) } else { preview } })
242}
243
244fn output_summary_bytes(output: &str) -> serde_json::Value {
245 let preview: String = output.chars().take(100).collect();
246 let truncated = output.chars().count() > 100;
247 serde_json::json!({"len": output.len(), "preview": if truncated { format!("{}...", preview) } else { preview } })
248}
249
250pub fn security_blocked_network(skill_id: &str, blocked_target: &str, reason: &str) {
252 tracing::warn!(
253 skill_id = %skill_id,
254 blocked_target = %blocked_target,
255 reason = %reason,
256 "Security: blocked network request"
257 );
258 if let Some(path) = get_security_events_path() {
259 let record = json!({
260 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
261 "type": "security_blocked",
262 "category": "network",
263 "skill_id": skill_id,
264 "details": {
265 "blocked_target": blocked_target,
266 "reason": reason
267 }
268 });
269 append_jsonl(&path, &record);
270 }
271}
272
273pub fn security_scan_high(skill_id: &str, severity: &str, issues: &serde_json::Value) {
275 if let Some(path) = get_security_events_path() {
276 let record = json!({
277 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
278 "type": "security_scan_high",
279 "category": "code_scan",
280 "skill_id": skill_id,
281 "details": {
282 "severity": severity,
283 "issues": issues
284 }
285 });
286 append_jsonl(&path, &record);
287 }
288}
289
290pub fn security_scan_approved(skill_id: &str, scan_id: &str, issues_count: usize) {
292 tracing::info!(
293 skill_id = %skill_id,
294 scan_id = %scan_id,
295 issues_count = %issues_count,
296 "Security: scan approved by user"
297 );
298 if let Some(path) = get_security_events_path() {
299 let record = json!({
300 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
301 "type": "security_scan_approved",
302 "category": "code_scan",
303 "skill_id": skill_id,
304 "details": {
305 "scan_id": scan_id,
306 "issues_count": issues_count,
307 "decision": "approved"
308 }
309 });
310 append_jsonl(&path, &record);
311 }
312}
313
314pub fn security_scan_rejected(skill_id: &str, scan_id: &str, issues_count: usize) {
316 tracing::info!(
317 skill_id = %skill_id,
318 scan_id = %scan_id,
319 issues_count = %issues_count,
320 "Security: scan rejected by user"
321 );
322 if let Some(path) = get_security_events_path() {
323 let record = json!({
324 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
325 "type": "security_scan_rejected",
326 "category": "code_scan",
327 "skill_id": skill_id,
328 "details": {
329 "scan_id": scan_id,
330 "issues_count": issues_count,
331 "decision": "rejected"
332 }
333 });
334 append_jsonl(&path, &record);
335 }
336}
337
338fn edit_audit_context() -> serde_json::Value {
346 crate::config::loader::env_optional(
347 crate::config::env_keys::observability::SKILLLITE_AUDIT_CONTEXT,
348 &[],
349 )
350 .map(serde_json::Value::String)
351 .unwrap_or(serde_json::Value::Null)
352}
353
354pub fn audit_edit_applied(
356 path: &str,
357 occurrences: usize,
358 first_changed_line: usize,
359 diff_excerpt: &str,
360 workspace: Option<&str>,
361) {
362 if let Some(audit) = get_audit_path() {
363 let record = json!({
364 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
365 "event": "edit_applied",
366 "category": "edit",
367 "source_layer": "agent",
368 "edit_id": Uuid::new_v4().to_string(),
369 "path": path,
370 "workspace": workspace.unwrap_or(""),
371 "context": edit_audit_context(),
372 "details": {
373 "occurrences": occurrences,
374 "first_changed_line": first_changed_line,
375 "diff_excerpt": diff_excerpt
376 }
377 });
378 append_jsonl(&audit, &record);
379 }
380}
381
382pub fn audit_edit_previewed(
384 path: &str,
385 occurrences: usize,
386 first_changed_line: usize,
387 diff_excerpt: &str,
388 workspace: Option<&str>,
389) {
390 if let Some(audit) = get_audit_path() {
391 let record = json!({
392 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
393 "event": "edit_previewed",
394 "category": "edit",
395 "source_layer": "agent",
396 "edit_id": Uuid::new_v4().to_string(),
397 "path": path,
398 "workspace": workspace.unwrap_or(""),
399 "context": edit_audit_context(),
400 "details": {
401 "occurrences": occurrences,
402 "first_changed_line": first_changed_line,
403 "diff_excerpt": diff_excerpt
404 }
405 });
406 append_jsonl(&audit, &record);
407 }
408}
409
410pub fn audit_edit_inserted(
412 path: &str,
413 line_num: usize,
414 lines_inserted: usize,
415 diff_excerpt: &str,
416 workspace: Option<&str>,
417) {
418 if let Some(audit) = get_audit_path() {
419 let record = json!({
420 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
421 "event": "edit_inserted",
422 "category": "edit",
423 "source_layer": "agent",
424 "edit_id": Uuid::new_v4().to_string(),
425 "path": path,
426 "workspace": workspace.unwrap_or(""),
427 "context": edit_audit_context(),
428 "details": {
429 "insert_after_line": line_num,
430 "lines_inserted": lines_inserted,
431 "diff_excerpt": diff_excerpt
432 }
433 });
434 append_jsonl(&audit, &record);
435 }
436}
437
438pub fn audit_edit_failed(path: &str, tool_name: &str, reason: &str, workspace: Option<&str>) {
440 if let Some(audit) = get_audit_path() {
441 let record = json!({
442 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
443 "event": "edit_failed",
444 "category": "edit",
445 "source_layer": "agent",
446 "edit_id": Uuid::new_v4().to_string(),
447 "path": path,
448 "reason": reason,
449 "tool": tool_name,
450 "workspace": workspace.unwrap_or(""),
451 "context": edit_audit_context(),
452 "details": {
453 "path": path,
454 "tool": tool_name,
455 "reason": reason
456 }
457 });
458 append_jsonl(&audit, &record);
459 }
460}
461
462pub fn audit_evolution_event(event_type: &str, target_id: &str, reason: &str, txn_id: &str) {
468 if let Some(path) = get_audit_path() {
469 let record = json!({
470 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
471 "event": "evolution",
472 "category": "evolution",
473 "source_layer": "agent",
474 "details": {
475 "type": event_type,
476 "target_id": target_id,
477 "reason": reason,
478 "txn_id": txn_id
479 }
480 });
481 append_jsonl(&path, &record);
482 }
483}
484
485pub fn security_sandbox_fallback(skill_id: &str, reason: &str) {
487 tracing::warn!(
488 skill_id = %skill_id,
489 reason = %reason,
490 "Security: sandbox fallback to simple execution"
491 );
492 if let Some(path) = get_security_events_path() {
493 let record = json!({
494 "ts": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
495 "type": "sandbox_fallback",
496 "category": "runtime",
497 "skill_id": skill_id,
498 "details": { "reason": reason }
499 });
500 append_jsonl(&path, &record);
501 }
502}