Rust SDK · Recorder
With the recorder constructed and the middleware wired, your handlers can record events. The examples below are lifted from the secrets-vault examples repo.
Simple write
Set fields on CurrentEvent; the middleware records on response finish.
Handlers that don't set action are skipped entirely (no audit noise):
use everscribe::axum::CurrentEvent;
use everscribe::event::Target;
async fn reveal(State(v): State<Vault>, ev: CurrentEvent, Path(id): Path<String>) -> Response {
ev.with(|e| e.target = Target::new("secret", &id));
let Some(s) = v.reveal(&id) else {
return (StatusCode::NOT_FOUND, "not found").into_response();
};
ev.with(|e| {
e.action = "secret.reveal".into();
e.with_field("name", &s.name);
});
Json(json!({ "value": s.value })).into_response()
}
The middleware auto-fills result from the response status, so error and
success paths both record correctly without you setting result by hand.
Mutation with before/after diff
For events that change a resource, populate change so the audit trail shows
what changed:
async fn rotate(State(v): State<Vault>, ev: CurrentEvent, Path(id): Path<String>, Json(body): Json<Value>) -> Response {
ev.with(|e| e.target = Target::new("secret", &id));
let before = snapshot(&v.get(&id)); // pre-mutation state
v.rotate(&id, body["value"].as_str().unwrap_or_default());
let after = snapshot(&v.get(&id)); // post-mutation state
ev.with(|e| {
e.action = "secret.rotate".into();
e.diff(&before, &after); // populates change with the diff
});
StatusCode::NO_CONTENT.into_response()
}
before/after are anything that implements serde::Serialize. The diff
helper JSON-normalizes both and stores them; the audit-log API computes the
patch on ingest.
Redacted fields
diff_redacted scrubs sensitive JSON Pointer
paths with "[REDACTED]" before storage:
ev.with(|e| {
e.diff_redacted(&before, &after, &["/password_hash", "/api_keys/0"]);
});
Or pre-redact inside your snapshot function so plaintext never reaches the SDK
— the vault examples take that approach (value is always "[REDACTED]").
Batch recording
The buffered recorder batches under the hood. record() enqueues; a background
task sends batches every flush_interval or when flush_size is reached. Tune
via RecorderOptions:
use everscribe::recorder::RecorderOptions;
use std::time::Duration;
let rec = es.new_recorder(RecorderOptions {
buffer_size: 5000,
flush_size: 500,
flush_interval: Duration::from_secs(2),
..Default::default()
});
For graceful shutdown, drain the buffer before exit:
rec.close().await?; // flushes pending events, then disposes
Multiple events per request
Some handlers produce more than one event. Build each Event directly with
Event::new, copy the actor over, and record it explicitly; the recorder
batches them on the next flush.
use everscribe::event::{Event, Target};
// `rec` constructed at boot; `actor` came off the request.
for uid in user_ids {
let mut e = Event::new("secret.share");
e.actor = actor.clone();
e.target = Target::new("secret", &id);
e.with_field("shared_with", &uid);
rec.record(e).await.ok();
}
Direct recording
Not every event comes from an HTTP request. Cron jobs, queue workers, startup
hooks. Build the event yourself: set actor to a service identity and set
outcome by hand since there's no response status to derive it from.
use everscribe::event::{Actor, Event, Outcome, Target};
async fn rotate_expiring_secrets(rec: &everscribe::recorder::BufferedRecorder, vault: &Vault) {
for s in vault.due_for_rotation() {
let mut e = Event::new("secret.rotate");
e.actor = Actor { r#type: "service".into(), id: "rotation-worker".into(), ..Default::default() };
e.target = Target::new("secret", &s.id);
e.outcome = Outcome { status: "ok".into(), ..Default::default() };
rec.record(e).await.ok();
}
}
The same recorder instance backs both your request handlers and your background
jobs. Construct one at boot, share it, and close() it once on shutdown.