use crate::core::Timestamp;
pub const METRIC: &str = "agentplane.metric";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Counter,
Gauge,
}
impl Kind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Counter => "counter",
Self::Gauge => "gauge",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Instrument {
pub name: &'static str,
pub kind: Kind,
pub unit: &'static str,
pub dimension: Option<&'static str>,
pub description: &'static str,
}
pub const RUNS: Instrument = Instrument {
name: "agentplane.runs",
kind: Kind::Counter,
unit: "1",
dimension: Some("outcome"),
description: "Runs that reached a terminal status. The dimension is that status, \
so 'how many failed' needs no second instrument.",
};
pub const EFFECTS: Instrument = Instrument {
name: "agentplane.effects",
kind: Kind::Counter,
unit: "1",
dimension: Some("kind"),
description: "Effect attempts dispatched to the world. A retry counts again.",
};
pub const EFFECTS_REPLAYED: Instrument = Instrument {
name: "agentplane.effects.replayed",
kind: Kind::Counter,
unit: "1",
dimension: Some("kind"),
description: "Effect results served from the journal rather than performed.",
};
pub const DIVERGENCES: Instrument = Instrument {
name: "agentplane.replay.divergences",
kind: Kind::Counter,
unit: "1",
dimension: None,
description: "Replays that recomputed a different effect key than the journal holds.",
};
pub const QUARANTINES: Instrument = Instrument {
name: "agentplane.quarantines",
kind: Kind::Counter,
unit: "1",
dimension: None,
description: "Runs set aside for a human because an outcome could not be \
determined and guessing was forbidden. Never self-healing: this \
number only falls when someone acts.",
};
pub const UNDECIDABLE: Instrument = Instrument {
name: "agentplane.effects.undecidable",
kind: Kind::Counter,
unit: "1",
dimension: None,
description: "Effect outcomes that could not be determined and where guessing was forbidden.",
};
pub const UNREPRODUCIBLE: Instrument = Instrument {
name: "agentplane.runs.unreproducible",
kind: Kind::Counter,
unit: "1",
dimension: None,
description: "Runs quarantined because a read they pinned by digest or version \
came back different. Counts runs that noticed, not versions that \
moved: one rewrite can strand every run that read it.",
};
pub const RECONCILIATIONS: Instrument = Instrument {
name: "agentplane.effects.reconciled",
kind: Kind::Counter,
unit: "1",
dimension: Some("verdict"),
description: "Unknown outcomes resolved by asking the provider.",
};
pub const BUDGET_REFUSALS: Instrument = Instrument {
name: "agentplane.budget.refusals",
kind: Kind::Counter,
unit: "1",
dimension: Some("limit"),
description: "Operations refused by a limit before they started.",
};
pub const COMPENSATIONS: Instrument = Instrument {
name: "agentplane.saga.compensations",
kind: Kind::Counter,
unit: "1",
dimension: Some("outcome"),
description: "Completed steps undone during an unwind, by whether the \
compensation itself succeeded. A failed one leaves the run \
partly unwound and is the loudest thing here.",
};
pub const DEAD_LETTERS: Instrument = Instrument {
name: "agentplane.events.dead_lettered",
kind: Kind::Counter,
unit: "1",
dimension: None,
description: "Events that aged out with nobody waiting — a correlation bug somewhere.",
};
pub const DEADLINE_BREACHES: Instrument = Instrument {
name: "agentplane.deadlines.breached",
kind: Kind::Counter,
unit: "1",
dimension: None,
description: "Obligations whose instant passed with the obligation unmet. \
A regulatory window that closed, counted at the moment it closed \
rather than when someone noticed.",
};
pub const TIMERS_FIRED: Instrument = Instrument {
name: "agentplane.timers.fired",
kind: Kind::Counter,
unit: "1",
dimension: None,
description: "Durable wake-ups delivered to sleeping runs. Not a fault: a \
fired timer is the system working, reported so a quiet plane is \
distinguishable from a stalled one.",
};
pub const RUNS_RECOVERED: Instrument = Instrument {
name: "agentplane.runs.recovered",
kind: Kind::Counter,
unit: "1",
dimension: Some("outcome"),
description: "Runs taken over and resumed after their owner's lease lapsed \
without release — an instance died holding them. Each one is a \
crash the plane healed; a steady rate means instances are dying \
steadily, which the recovery hides from everything but this.",
};
pub const RECOVERY_FAILURES: Instrument = Instrument {
name: "agentplane.runs.recovery_failures",
kind: Kind::Counter,
unit: "1",
dimension: None,
description: "Abandoned runs the recovery sweep tried to resume and could \
not. The run stays listed and is retried next tick, so a \
persistent count here is one stuck run, not many — but it is \
stuck, and nothing else will unstick it.",
};
pub const REPLANS: Instrument = Instrument {
name: "agentplane.replans",
kind: Kind::Counter,
unit: "1",
dimension: None,
description: "Runs that replaced their plan with a versioned successor. \
Rising sharply means plans are being written that do not survive \
contact with their own first step.",
};
pub const POLICY_DENIALS: Instrument = Instrument {
name: "agentplane.policy.denials",
kind: Kind::Counter,
unit: "1",
dimension: Some("action"),
description: "Actions refused by the authorization layer before they were \
attempted. A denial is not a fault — it is the policy working — \
but a sudden rise means an agent is asking for something new.",
};
pub const BATCH_ITEMS: Instrument = Instrument {
name: "agentplane.batch.items",
kind: Kind::Counter,
unit: "1",
dimension: Some("outcome"),
description: "Batch items that reached an outcome. The dimension is what makes \
partial failure visible without reading every item record.",
};
pub const OPEN_CASES: Instrument = Instrument {
name: "agentplane.cases.open",
kind: Kind::Gauge,
unit: "1",
dimension: None,
description: "Cases not yet closed. Read with the oldest-age gauge: a count \
alone cannot distinguish a healthy queue from a stuck one.",
};
pub const OLDEST_CASE_AGE: Instrument = Instrument {
name: "agentplane.cases.oldest_age",
kind: Kind::Gauge,
unit: "s",
dimension: None,
description: "Age of the longest-open case. A backlog that is quietly ageing is \
invisible in a count alone: ten cases open for an hour and ten open \
for a month are the same number.",
};
pub const DUE_DEADLINES: Instrument = Instrument {
name: "agentplane.deadlines.due",
kind: Kind::Gauge,
unit: "1",
dimension: None,
description: "Obligations at or past their instant and not yet resolved.",
};
pub const PENDING_TIMERS: Instrument = Instrument {
name: "agentplane.timers.pending",
kind: Kind::Gauge,
unit: "1",
dimension: None,
description: "Runs sleeping on a durable timer. Each costs a row, not a \
thread, so a large number here is capacity rather than load.",
};
pub const OPEN_TASKS: Instrument = Instrument {
name: "agentplane.tasks.open",
kind: Kind::Gauge,
unit: "1",
dimension: None,
description: "Human decisions awaiting an answer, including claimed ones. A \
reviewer opening a task does not reduce the backlog; answering \
it does.",
};
pub const CATALOGUE: &[Instrument] = &[
RUNS,
EFFECTS,
EFFECTS_REPLAYED,
DIVERGENCES,
UNREPRODUCIBLE,
QUARANTINES,
UNDECIDABLE,
RECONCILIATIONS,
BUDGET_REFUSALS,
COMPENSATIONS,
DEAD_LETTERS,
DEADLINE_BREACHES,
TIMERS_FIRED,
RUNS_RECOVERED,
RECOVERY_FAILURES,
REPLANS,
BATCH_ITEMS,
POLICY_DENIALS,
OPEN_CASES,
OLDEST_CASE_AGE,
DUE_DEADLINES,
PENDING_TIMERS,
OPEN_TASKS,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Census {
pub open_cases: u64,
pub oldest_case_age_secs: Option<u64>,
pub due_deadlines: u64,
pub pending_timers: u64,
pub open_tasks: u64,
}
impl Census {}
#[must_use]
pub fn age_secs(opened_at: Timestamp, now: Timestamp) -> u64 {
u64::try_from(now.unix_timestamp() - opened_at.unix_timestamp()).unwrap_or(0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TenantLabel {
#[default]
Omitted,
Name,
}
impl TenantLabel {
#[must_use]
pub fn render(self, tenant: &crate::core::TenantId) -> String {
match self {
Self::Omitted => String::new(),
Self::Name => tenant.to_string(),
}
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Meter {
tenant: String,
}
impl Meter {
pub(crate) fn new(policy: TenantLabel, tenant: &crate::core::TenantId) -> Self {
Self {
tenant: policy.render(tenant),
}
}
pub(crate) fn count(&self, i: Instrument, dim: &str) {
self.count_by(i, dim, 1);
}
pub(crate) fn count_by(&self, i: Instrument, dim: &str, by: u64) {
tracing::info!(
target: METRIC,
metric = i.name,
kind = i.kind.as_str(),
unit = i.unit,
value = by,
dim,
tenant = self.tenant,
);
}
pub(crate) fn gauge(&self, i: Instrument, value: u64) {
tracing::info!(
target: METRIC,
metric = i.name,
kind = i.kind.as_str(),
unit = i.unit,
value,
dim = "",
tenant = self.tenant,
);
}
pub(crate) fn census(&self, c: &Census) {
self.gauge(OPEN_CASES, c.open_cases);
if let Some(age) = c.oldest_case_age_secs {
self.gauge(OLDEST_CASE_AGE, age);
}
self.gauge(DUE_DEADLINES, c.due_deadlines);
self.gauge(PENDING_TIMERS, c.pending_timers);
self.gauge(OPEN_TASKS, c.open_tasks);
}
}