1use crate::logging::{redact_event, FactsEmitter};
12use serde_json::{json, Map, Value};
13use std::cell::Cell;
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::time::{SystemTime, UNIX_EPOCH};
16use uuid::Uuid;
17
18pub(crate) const SCHEMA_VERSION: i64 = 2;
19
20#[derive(Clone, Debug, Default)]
21pub(crate) struct AuditMode {
22 pub dry_run: bool,
23 pub redact: bool,
24}
25
26#[derive(Debug)]
27pub(crate) struct AuditCtx<'a> {
28 pub facts: &'a dyn FactsEmitter,
29 pub plan_id: String,
30 pub run_id: String,
31 pub ts: String,
32 pub mode: AuditMode,
33 pub seq: Cell<u64>,
34}
35
36impl<'a> AuditCtx<'a> {
37 pub(crate) fn new(
38 facts: &'a dyn FactsEmitter,
39 plan_id: String,
40 run_id: String,
41 ts: String,
42 mode: AuditMode,
43 ) -> Self {
44 Self {
45 facts,
46 plan_id,
47 run_id,
48 ts,
49 mode,
50 seq: Cell::new(0),
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug)]
57pub enum Stage {
58 Plan,
59 Preflight,
60 PreflightSummary,
61 ApplyAttempt,
62 ApplyResult,
63 Rollback,
64 RollbackSummary,
65 PruneResult,
66}
67
68impl Stage {
69 const fn as_event(self) -> &'static str {
70 match self {
71 Stage::Plan => "plan",
72 Stage::Preflight => "preflight",
73 Stage::PreflightSummary => "preflight.summary",
74 Stage::ApplyAttempt => "apply.attempt",
75 Stage::ApplyResult => "apply.result",
76 Stage::Rollback => "rollback",
77 Stage::RollbackSummary => "rollback.summary",
78 Stage::PruneResult => "prune.result",
79 }
80 }
81}
82
83#[derive(Clone, Copy, Debug)]
85pub enum Decision {
86 Success,
87 Failure,
88 Warn,
89}
90
91impl Decision {
92 const fn as_str(self) -> &'static str {
93 match self {
94 Decision::Success => "success",
95 Decision::Failure => "failure",
96 Decision::Warn => "warn",
97 }
98 }
99}
100
101#[derive(Debug)]
103pub struct StageLogger<'a> {
104 ctx: &'a AuditCtx<'a>,
105}
106
107impl<'a> StageLogger<'a> {
108 pub(crate) const fn new(ctx: &'a AuditCtx<'a>) -> Self {
109 Self { ctx }
110 }
111
112 #[must_use]
113 pub fn plan(&'a self) -> EventBuilder<'a> {
114 EventBuilder::new(self.ctx, Stage::Plan)
115 }
116 #[must_use]
117 pub fn preflight(&'a self) -> EventBuilder<'a> {
118 EventBuilder::new(self.ctx, Stage::Preflight)
119 }
120 #[must_use]
121 pub fn preflight_summary(&'a self) -> EventBuilder<'a> {
122 EventBuilder::new(self.ctx, Stage::PreflightSummary)
123 }
124 #[must_use]
125 pub fn apply_attempt(&'a self) -> EventBuilder<'a> {
126 EventBuilder::new(self.ctx, Stage::ApplyAttempt)
127 }
128 #[must_use]
129 pub fn apply_result(&'a self) -> EventBuilder<'a> {
130 EventBuilder::new(self.ctx, Stage::ApplyResult)
131 }
132 #[must_use]
133 pub fn rollback(&'a self) -> EventBuilder<'a> {
134 EventBuilder::new(self.ctx, Stage::Rollback)
135 }
136 #[must_use]
137 pub fn rollback_summary(&'a self) -> EventBuilder<'a> {
138 EventBuilder::new(self.ctx, Stage::RollbackSummary)
139 }
140 #[must_use]
141 pub fn prune_result(&'a self) -> EventBuilder<'a> {
142 EventBuilder::new(self.ctx, Stage::PruneResult)
143 }
144}
145
146#[derive(Debug)]
147pub struct EventBuilder<'a> {
148 ctx: &'a AuditCtx<'a>,
149 stage: Stage,
150 fields: Map<String, Value>,
151}
152
153impl<'a> EventBuilder<'a> {
154 fn new(ctx: &'a AuditCtx<'a>, stage: Stage) -> Self {
155 let mut fields = Map::new();
156 fields.insert("stage".to_string(), json!(stage.as_event()));
157 Self { ctx, stage, fields }
158 }
159
160 #[must_use]
161 pub fn action(mut self, action_id: impl Into<String>) -> Self {
162 self.fields
163 .insert("action_id".into(), json!(action_id.into()));
164 self
165 }
166
167 #[must_use]
169 pub fn action_id(self, aid: impl Into<String>) -> Self {
170 self.action(aid)
171 }
172
173 #[must_use]
174 pub fn path(mut self, path: impl Into<String>) -> Self {
175 self.fields.insert("path".into(), json!(path.into()));
176 self
177 }
178
179 #[must_use]
181 pub fn perf(mut self, hash_ms: u64, backup_ms: u64, swap_ms: u64) -> Self {
182 self.fields.insert(
183 "perf".to_string(),
184 json!({
185 "hash_ms": hash_ms,
186 "backup_ms": backup_ms,
187 "swap_ms": swap_ms,
188 }),
189 );
190 self
191 }
192
193 #[must_use]
195 pub fn error_id(mut self, id: crate::api::errors::ErrorId) -> Self {
196 self.fields.insert(
197 "error_id".to_string(),
198 json!(crate::api::errors::id_str(id)),
199 );
200 self
201 }
202
203 #[must_use]
205 pub fn exit_code_for(mut self, id: crate::api::errors::ErrorId) -> Self {
206 self.fields.insert(
207 "exit_code".to_string(),
208 json!(crate::api::errors::exit_code_for(id)),
209 );
210 self
211 }
212
213 #[must_use]
214 pub fn field(mut self, key: &str, value: Value) -> Self {
215 self.fields.insert(key.to_string(), value);
216 self
217 }
218
219 #[must_use]
220 pub fn merge(mut self, extra: &Value) -> Self {
221 if let Some(obj) = extra.as_object() {
222 for (k, v) in obj {
223 self.fields.insert(k.clone(), v.clone());
224 }
225 }
226 self
227 }
228
229 pub fn emit(self, decision: Decision) {
230 let mut fields = Value::Object(self.fields);
231 ensure_provenance(&mut fields);
233 if let Some(obj) = fields.as_object_mut() {
234 obj.entry("decision").or_insert(json!(decision.as_str()));
235 }
236 redact_and_emit(
237 self.ctx,
238 "switchyard",
239 self.stage.as_event(),
240 decision.as_str(),
241 fields,
242 );
243 }
244
245 pub fn emit_success(self) {
246 self.emit(Decision::Success);
247 }
248 pub fn emit_failure(self) {
249 self.emit(Decision::Failure);
250 }
251 pub fn emit_warn(self) {
252 self.emit(Decision::Warn);
253 }
254}
255
256fn redact_and_emit(
257 ctx: &AuditCtx<'_>,
258 subsystem: &str,
259 event: &str,
260 decision: &str,
261 mut fields: Value,
262) {
263 if let Some(obj) = fields.as_object_mut() {
265 obj.entry("schema_version").or_insert(json!(SCHEMA_VERSION));
266 obj.entry("ts").or_insert(json!(ctx.ts));
267 obj.entry("plan_id").or_insert(json!(ctx.plan_id));
268 obj.entry("run_id").or_insert(json!(ctx.run_id));
269 obj.entry("event_id").or_insert(json!(new_event_id()));
270 obj.entry("switchyard_version")
271 .or_insert(json!(env!("CARGO_PKG_VERSION")));
272 obj.entry("redacted").or_insert(json!(ctx.mode.redact));
274 obj.entry("redaction")
275 .or_insert(json!({"applied": ctx.mode.redact}));
276
277 #[cfg(feature = "envmeta")]
279 {
280 use serde_json::map::Entry;
281 if let Entry::Vacant(e) = obj.entry("host".to_string()) {
283 let mut host_obj = Map::new();
284 if let Some(hostname) = std::env::var("HOSTNAME").ok() {
285 host_obj.insert("hostname".to_string(), json!(hostname));
286 }
287 host_obj.insert("os".to_string(), json!(std::env::consts::OS.to_string()));
288 host_obj.insert(
289 "arch".to_string(),
290 json!(std::env::consts::ARCH.to_string()),
291 );
292 if let Some(kernel) = std::fs::read_to_string("/proc/version")
294 .ok()
295 .and_then(|s| s.split_whitespace().nth(2).map(ToString::to_string))
296 {
297 host_obj.insert("kernel".to_string(), json!(kernel));
298 }
299 e.insert(json!(host_obj));
300 }
301 if let Entry::Vacant(e) = obj.entry("process".to_string()) {
303 let process_id = std::process::id();
304 let parent_process_id = rustix::process::Pid::as_raw(rustix::process::getppid());
305 e.insert(json!({"pid": process_id, "ppid": parent_process_id}));
306 }
307 if let Entry::Vacant(e) = obj.entry("actor".to_string()) {
309 let effective_user_id = rustix::process::geteuid().as_raw();
310 let effective_group_id = rustix::process::getegid().as_raw();
311 e.insert(json!({"euid": effective_user_id, "egid": effective_group_id}));
312 }
313 if let Entry::Vacant(e) = obj.entry("build".to_string()) {
315 let mut build_obj = Map::new();
316 if let Some(git_sha) = std::env::var("GIT_SHA").ok() {
317 build_obj.insert("git_sha".to_string(), json!(git_sha));
318 }
319 if let Some(rustc) = std::env::var("RUSTC_VERSION").ok() {
320 build_obj.insert("rustc".to_string(), json!(rustc));
321 }
322 e.insert(json!(build_obj));
323 }
324 }
325 let cur = ctx.seq.get();
327 obj.entry("seq").or_insert(json!(cur));
328 ctx.seq.set(cur.saturating_add(1));
329 obj.entry("dry_run").or_insert(json!(ctx.mode.dry_run));
330 }
331 let out = if ctx.mode.redact {
333 redact_event(fields)
334 } else {
335 fields
336 };
337 ctx.facts.emit(subsystem, event, decision, out);
338}
339
340fn new_event_id() -> String {
341 static NEXT_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
343 let nanos = SystemTime::now()
344 .duration_since(UNIX_EPOCH)
345 .unwrap_or_default()
346 .as_nanos();
347 let c = NEXT_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
348 let name = format!("{nanos}:{c}:event");
349 Uuid::new_v5(&Uuid::NAMESPACE_URL, name.as_bytes()).to_string()
350}
351
352pub(crate) fn new_run_id() -> String {
353 static NEXT_RUN_COUNTER: AtomicU64 = AtomicU64::new(0);
355 let nanos = SystemTime::now()
356 .duration_since(UNIX_EPOCH)
357 .unwrap_or_default()
358 .as_nanos();
359 let c = NEXT_RUN_COUNTER.fetch_add(1, Ordering::Relaxed);
360 let name = format!("{nanos}:{c}:run");
361 Uuid::new_v5(&Uuid::NAMESPACE_URL, name.as_bytes()).to_string()
362}
363
364pub(crate) fn ensure_provenance(extra: &mut Value) {
369 if let Some(obj) = extra.as_object_mut() {
370 let prov = obj
372 .entry("provenance")
373 .or_insert_with(|| Value::Object(Map::new()));
374
375 if !prov.is_object() {
377 *prov = Value::Object(Map::new());
378 }
379
380 if let Value::Object(prov_obj) = prov {
382 prov_obj.entry("env_sanitized").or_insert(Value::Bool(true));
383 }
384 }
385}