use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Once, OnceLock};
use std::time::Instant;
use agentstategraph_core::{Commit, ObjectId};
use agentstategraph_storage::CommitTrace;
use agentstategraph_taint::TaintError;
use metrics::{Label, Unit};
use tracing::Span;
use crate::repo::RepoError;
macro_rules! write_span {
($name:literal, $ref_name:expr, $options:expr) => {
tracing::info_span!(
$name,
asg.ref.name = $ref_name,
gen_ai.agent.id = $options.agent_id.as_str(),
asg.intent.category = ?$options.intent.category,
asg.speculation.id = tracing::field::Empty,
asg.commit.id = tracing::field::Empty,
asg.ref.cas_attempts = tracing::field::Empty,
error.type = tracing::field::Empty,
otel.status_code = tracing::field::Empty,
)
};
}
pub(crate) use write_span;
macro_rules! op_span {
($level:ident, $name:literal $(, $($fields:tt)*)?) => {
tracing::span!(
tracing::Level::$level,
$name,
error.type = tracing::field::Empty,
otel.status_code = tracing::field::Empty,
$($($fields)*)?
)
};
}
pub(crate) use op_span;
pub(crate) fn write(
operation: &'static str,
span: Span,
write: impl FnOnce() -> Result<ObjectId, RepoError>,
) -> Result<ObjectId, RepoError> {
let _entered = span.enter();
let started = now();
let result = write();
describe_metrics();
let mut labels = vec![Label::new("asg.operation.name", operation)];
match &result {
Ok(id) => {
span.record("asg.commit.id", id.to_string());
}
Err(e) => {
failed(&span, e);
labels.push(Label::new("error.type", e.code()));
}
}
if let Some(started) = started {
metrics::histogram!("asg.commit.duration", labels).record(started.elapsed().as_secs_f64());
}
result
}
fn now() -> Option<Instant> {
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
return Some(Instant::now());
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
return None;
}
pub(crate) fn run<T>(
span: Span,
op: impl FnOnce() -> Result<T, RepoError>,
) -> Result<T, RepoError> {
let _entered = span.enter();
let result = op();
if let Err(e) = &result {
failed(&span, e);
}
result
}
fn describe_metrics() {
static DESCRIBED: Once = Once::new();
DESCRIBED.call_once(|| {
metrics::describe_histogram!(
"asg.commit.duration",
Unit::Seconds,
"How long a write that makes a commit took, including any retries."
);
metrics::describe_counter!(
"asg.ref.cas_retries",
"How many times a write rebuilt its commit because another writer moved the ref."
);
metrics::describe_counter!(
"asg.gc.sweeps",
"Garbage collection sweeps asked to delete, by whether they did or refused."
);
});
}
fn failed(span: &Span, error: &RepoError) {
span.record("error.type", error.code());
span.record("otel.status_code", "error");
}
pub(crate) fn cas_attempt(operation: &'static str, attempt: usize) {
if attempt == 0 {
return;
}
describe_metrics();
metrics::counter!("asg.ref.cas_retries", "asg.operation.name" => operation).increment(1);
Span::current().record("asg.ref.cas_attempts", attempt + 1);
tracing::debug!(
asg.ref.cas_attempts = attempt + 1,
"the ref moved; rebuilding the commit on its new head"
);
}
pub(crate) fn cas_gave_up(operation: &'static str, attempts: usize) {
tracing::warn!(
asg.operation.name = operation,
asg.ref.cas_attempts = attempts,
error.type = "write_conflict",
"the ref kept moving under the write, which gave up"
);
}
pub(crate) fn taint_refused(error: &TaintError, taint_id: Option<&str>) {
let effect = match error {
TaintError::Blocked { .. } => "block",
TaintError::InsufficientConfidence { .. } => "review",
TaintError::NotAuthorized { .. } => "quarantine",
_ => return,
};
tracing::info!(
asg.taint.id = taint_id,
asg.taint.effect = effect,
error.type = "taint_blocked",
"a taint refused the write"
);
}
type TraceSource = Box<dyn Fn() -> Option<CommitTrace> + Send + Sync>;
static TRACE_SOURCE: OnceLock<TraceSource> = OnceLock::new();
pub fn link_commits_to_traces(
source: impl Fn() -> Option<CommitTrace> + Send + Sync + 'static,
) -> bool {
TRACE_SOURCE.set(Box::new(source)).is_ok()
}
pub(crate) fn current_trace() -> Option<CommitTrace> {
TRACE_SOURCE.get().and_then(|source| source())
}
static COMMIT_EVENTS: AtomicBool = AtomicBool::new(false);
pub fn emit_commit_events(enabled: bool) {
COMMIT_EVENTS.store(enabled, Ordering::Relaxed);
}
pub(crate) fn commit_events_enabled() -> bool {
COMMIT_EVENTS.load(Ordering::Relaxed)
}
pub(crate) fn commit_landed(namespace: &str, ref_name: &str, commit: &Commit) {
tracing::event!(
name: "asg.commit",
target: "agentstategraph::provenance",
tracing::Level::INFO,
asg.commit.id = %commit.id,
asg.ref.name = ref_name,
asg.namespace = namespace,
gen_ai.agent.id = commit.agent_id.as_str(),
asg.intent.category = ?commit.intent.category,
asg.commit.confidence = commit.confidence,
"a commit landed"
);
}
pub(crate) fn gc_swept(refused: bool) {
describe_metrics();
let outcome = if refused { "refused" } else { "swept" };
metrics::counter!("asg.gc.sweeps", "asg.gc.outcome" => outcome).increment(1);
}