#[derive(Clone, Debug, Default)]
pub struct EventFields {
pub level: &'static str,
pub source: &'static str,
pub action: &'static str,
pub team: Option<String>,
pub session_id: Option<String>,
pub agent_id: Option<String>,
pub agent_name: Option<String>,
pub target: Option<String>,
pub result: Option<String>,
pub message_id: Option<String>,
pub request_id: Option<String>,
pub error: Option<String>,
pub count: Option<u64>,
pub message_text: Option<String>,
pub runtime: Option<String>,
pub runtime_session_id: Option<String>,
pub teardown_stage: Option<String>,
pub spawn_mode: Option<String>,
}
fn forward_to_unified(event: crate::logging_event::LogEventV1) {
if let Some(tx) = crate::logging::producer_sender() {
let _ = tx.try_send(event);
}
}
fn fields_to_log_event(fields: &EventFields) -> crate::logging_event::LogEventV1 {
use crate::logging_event::LogEventV1;
use chrono::Utc;
let hostname = hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_default();
LogEventV1 {
v: 1,
ts: Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
level: fields.level.to_string(),
source_binary: fields.source.to_string(),
hostname,
pid: std::process::id(),
target: fields.target.clone().unwrap_or_default(),
action: fields.action.to_string(),
team: fields.team.clone(),
agent: fields
.agent_id
.clone()
.or_else(|| fields.agent_name.clone()),
session_id: fields.session_id.clone(),
request_id: fields.request_id.clone(),
correlation_id: None,
outcome: fields.result.clone(),
error: fields.error.clone(),
fields: {
let mut map = serde_json::Map::new();
if let Some(mid) = &fields.message_id {
map.insert(
"message_id".to_string(),
serde_json::Value::String(mid.clone()),
);
}
if let Some(cnt) = fields.count {
map.insert("count".to_string(), serde_json::Value::Number(cnt.into()));
}
if let Some(runtime) = &fields.runtime {
map.insert(
"runtime".to_string(),
serde_json::Value::String(runtime.clone()),
);
}
if let Some(runtime_session_id) = &fields.runtime_session_id {
map.insert(
"runtime_session_id".to_string(),
serde_json::Value::String(runtime_session_id.clone()),
);
}
if let Some(teardown_stage) = &fields.teardown_stage {
map.insert(
"teardown_stage".to_string(),
serde_json::Value::String(teardown_stage.clone()),
);
}
if let Some(spawn_mode) = &fields.spawn_mode {
map.insert(
"spawn_mode".to_string(),
serde_json::Value::String(spawn_mode.clone()),
);
}
if let Some(ppid) = crate::pid::parent_pid() {
map.entry("ppid".to_string())
.or_insert_with(|| serde_json::Value::Number(ppid.into()));
}
map
},
spans: vec![],
}
}
pub fn emit_event_best_effort(mut fields: EventFields) {
if fields.level.is_empty() || fields.source.is_empty() || fields.action.is_empty() {
return;
}
if fields.session_id.is_none() {
fields.session_id = std::env::var("CLAUDE_SESSION_ID").ok();
}
let event = fields_to_log_event(&fields);
forward_to_unified(event);
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn test_fields_to_log_event_session_id_none_when_unset() {
unsafe { std::env::remove_var("CLAUDE_SESSION_ID") };
let fields = EventFields {
level: "info",
source: "atm",
action: "test_action",
session_id: None,
..Default::default()
};
let event = fields_to_log_event(&fields);
assert!(
event.session_id.is_none(),
"session_id should be None when CLAUDE_SESSION_ID is unset and caller passed None"
);
}
#[test]
fn test_fields_to_log_event_session_id_propagated() {
let fields = EventFields {
level: "info",
source: "atm",
action: "test_action",
session_id: Some("my-session-42".to_string()),
..Default::default()
};
let event = fields_to_log_event(&fields);
assert_eq!(event.session_id.as_deref(), Some("my-session-42"));
}
#[test]
fn test_fields_to_log_event_omits_message_text_for_privacy() {
let fields = EventFields {
level: "info",
source: "atm",
action: "send",
message_text: Some("secret body".to_string()),
..Default::default()
};
let event = fields_to_log_event(&fields);
assert!(
!event.fields.contains_key("message_text"),
"message_text should be intentionally omitted from persisted log fields"
);
}
#[cfg(unix)]
#[test]
fn test_fields_to_log_event_includes_parent_pid_field_on_unix() {
let fields = EventFields {
level: "info",
source: "atm",
action: "test_action",
..Default::default()
};
let event = fields_to_log_event(&fields);
assert!(
event.fields.get("ppid").is_some(),
"fields_to_log_event should include ppid field on unix"
);
}
#[test]
fn test_emit_event_best_effort_is_fail_open() {
emit_event_best_effort(EventFields {
level: "info",
source: "atm",
action: "test_fail_open",
team: Some("atm-dev".to_string()),
session_id: Some("sess-123".to_string()),
..Default::default()
});
}
#[test]
fn test_emit_event_best_effort_drops_empty_required_fields() {
emit_event_best_effort(EventFields {
level: "",
source: "atm",
action: "test_action",
..Default::default()
});
emit_event_best_effort(EventFields {
level: "info",
source: "",
action: "test_action",
..Default::default()
});
emit_event_best_effort(EventFields {
level: "info",
source: "atm",
action: "",
..Default::default()
});
}
}