feat(saas): correlate mutation diagnostics

req: operations/001

req: operations/003

req: security/008
This commit is contained in:
slhx agent
2026-07-14 00:53:04 +02:00
parent 4e0d9ebaff
commit 4fc5e53adf
4 changed files with 78 additions and 13 deletions
+48 -5
View File
@@ -19,8 +19,12 @@ use std::time::Duration;
use ui::dashboard;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SessionId(u64);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Session {
session_id: SessionId,
user_id: UserId,
email: String,
csrf: CsrfToken,
@@ -31,6 +35,7 @@ pub struct Session {
impl Session {
pub fn demo() -> Self {
Self {
session_id: SessionId(1),
user_id: UserId(42),
email: "founder@example.com".to_owned(),
csrf: CsrfToken("demo-csrf".to_owned()),
@@ -204,8 +209,20 @@ fn persist_projects(path: &Path, projects: &[ProjectRecord]) -> io::Result<()> {
Ok(())
}
#[derive(Clone, Copy)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequestCorrelationId(String);
impl Display for RequestCorrelationId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone)]
pub struct MutationDiagnostic {
pub request_id: RequestCorrelationId,
pub session_id: SessionId,
pub user_id: UserId,
pub outcome: &'static str,
pub duration_micros: u64,
}
@@ -219,8 +236,12 @@ struct StderrDiagnosticSink;
impl DiagnosticSink for StderrDiagnosticSink {
fn record(&self, diagnostic: MutationDiagnostic) {
eprintln!(
"event=saas.project_mutation outcome={} duration_micros={}",
diagnostic.outcome, diagnostic.duration_micros
"event=saas.project_mutation request_id={} session_id={} user_id={} outcome={} duration_micros={}",
diagnostic.request_id,
diagnostic.session_id.0,
diagnostic.user_id.0,
diagnostic.outcome,
diagnostic.duration_micros
);
}
}
@@ -234,6 +255,7 @@ struct MutationMetrics {
mismatch: AtomicU64,
failed: AtomicU64,
duration_micros: AtomicU64,
next_request_id: AtomicU64,
}
#[derive(Clone)]
@@ -300,7 +322,21 @@ impl AppContext {
self
}
pub fn record_mutation(&self, outcome: &'static str, duration: Duration) {
pub fn next_request_id(&self) -> RequestCorrelationId {
let sequence = self
.metrics
.next_request_id
.fetch_add(1, Ordering::Relaxed)
.saturating_add(1);
RequestCorrelationId(format!("req-{}-{sequence}", std::process::id()))
}
pub fn record_mutation(
&self,
request_id: RequestCorrelationId,
outcome: &'static str,
duration: Duration,
) {
self.metrics.attempts.fetch_add(1, Ordering::Relaxed);
match outcome {
"succeeded" => &self.metrics.succeeded,
@@ -315,6 +351,9 @@ impl AppContext {
.duration_micros
.fetch_add(duration_micros, Ordering::Relaxed);
self.diagnostics.record(MutationDiagnostic {
request_id,
session_id: self.session.session_id,
user_id: self.session.user_id,
outcome,
duration_micros,
});
@@ -583,10 +622,14 @@ mod tests {
// req: operations/003 req: operations/005 req: security/008
let diagnostics = Arc::new(RecordingDiagnostics::default());
let ctx = AppContext::demo().with_diagnostic_sink(diagnostics.clone());
ctx.record_mutation("denied", Duration::from_micros(7));
let request_id = ctx.next_request_id();
ctx.record_mutation(request_id.clone(), "denied", Duration::from_micros(7));
let recorded = diagnostics.0.lock().unwrap();
assert_eq!(recorded.len(), 1);
assert_eq!(recorded[0].request_id, request_id);
assert_eq!(recorded[0].session_id, SessionId(1));
assert_eq!(recorded[0].user_id, UserId(42));
assert_eq!(recorded[0].outcome, "denied");
assert_eq!(recorded[0].duration_micros, 7);
}