You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This proposal covers the four metrics that complement the initial set in #433.
All four follow the #433 cardinality rule — labels limited to template, worker pool, sandbox class, and closed outcome/reason enums; never actor, atespace, or session — and reuse the internal/ateattr label conventions.
Naming follows OTel semantic conventions (revised per review): dedicated instruments instead of a state attribute; self-descriptive instrument names (eligible_workers); all new attribute keys namespaced under ate.*; and error.type restricted to standardized values (gRPC status codes at RPC boundaries), with substrate-specific taxonomies in namespaced attributes such as ate.failure.reason.
Each metric is either a label extension on an existing instrument or a single counter/gauge at a code choke point where the data is already in hand, so implementation cost is low.
Relationship to the north stars (docs/architecture.md)
North star
Target
Status today
Relationship to these metrics
Activation latency
100ms @ p95
Unmeasurable — atenet.router.route.duration mixes warm routes (ms) and cold starts (up to 20s+) in one distribution
Measured directly by metric 1: p95 of resume=triggered
Throughput
1000 wakeups/sec
Not isolatable from warm lookups
Measured directly by metric 1: rate of resume=triggered
Scale
1B actors/cluster
Not measured by anything in this proposal — under the cardinality rule these metrics count operations, not actors, and freely double-count
Not measured here. The population measurement is #433's ate.actors (total actors tracked in the store). Metrics 2–4 provide what operating at that scale requires: actor-loss rate (2) and saturation visibility (3, 4)
At a glance
#
Metric
Measures
Golden signal
North star / driver
1
atenet.router.route.duration — new labels
Edge routing latency, split warm vs. cold, failures classified
1. Extend atenet.router.route.duration with resume and richer outcome labels
Golden signals: Latency + Traffic. North star: makes the 100ms p95 activation target and the 1000 wakeups/sec throughput target measurable.
Measures: time from Envoy handing request headers to ext_proc until the target worker endpoint is resolved (Host parse + ResumeActor + authority rewrite), excluding actor compute and the response.
Change: labels only, on the existing histogram.
New label ate.router.resume ∈ {none, triggered, joined}:
none — actor was already RUNNING; the call was a pure location lookup. This series is the steady-state routing SLI every warm request pays.
triggered — this request won the per-actor singleflight and executed the resume. Its latency is the activation latency measured at the edge — the series for the 100ms p95 north star. Its rate is wakeups/sec — the throughput north star.
joined — a resume was already in flight; this request parked on the shared call. Kept separate because it is a biased sample of activation latency: one 20s cold start with 50 queued callers must count as 1 slow activation in SLO math, not 51.
Split the catch-all outcome=error into no_capacity (FailedPrecondition "no free workers available"), timeout, lock_conflict (Aborted), resume_error. "Fleet is full" (capacity/ops problem) and "routing is broken" (bug) are currently the same data point.
Attribute-key migration: the histogram's existing bare keys (actor_template_namespace, actor_template_name, outcome) predate the namespacing convention — migrate them to ate.template.namespace, ate.template.name, ate.router.outcome in the same change, with a coordinated dashboard update, rather than leaving two conventions side by side.
CUJ:"An agent user's request hung or got a 503 — was the actor cold, was the fleet full, or is routing broken?"
2. ate.actor.crashes (counter)
Golden signal: Errors. North star: reliability accounting required to operate at 1B-actor scale — today the platform cannot state its actor-loss rate.
Naming note (from review discussion): the name deliberately matches the state machine's terminal STATUS_CRASHED state — the metric counts entries into that state, not process-level crashes. Not all entries are data loss in fact (some are conservative terminal-izations of control-plane inconsistencies); the ate.failure.reason label keeps those populations separable.
Measures: count of actors entering the terminal CRASHED state, attributed to the operation that failed and the underlying cause.
Emitted by: ateapi — one increment inside crashActor, the single choke point every terminal failure flows through. Code anchors:cmd/ateapi/internal/controlapi/crash.go; internal/ateerrors/ateerrors.go.
Why P0: CRASHED is terminal unavailability — with possible loss of un-persisted state — and has no alertable signal of any kind today — discoverable only by listing actors or grepping logs. The cause label is essentially free: atelet already tags failures with a typed Reason as AIP-193 ErrorInfo and ateapi already parses it back. This is the definitional gap in the Errors golden signal.
CUJ:"Actors are ending up CRASHED — how many, and why?" Alert: crash rate > 0. A spike of TERMINAL_FILE_SYSTEM_ERROR = node disks full; FAILED_GET_EXTERNAL_OBJECT = storage backend unhealthy; per-template attribution catches one bad template crashing repeatedly.
Golden signal: Saturation. North star: protects the activation-latency target under load; direct input to #198's warm-buffer controller.
Measures: the number of eligible free workers remaining after all constraint filters, sampled at every scheduling decision — how close to empty the schedulable pool was.
Emitted by: ateapi, one record of len(candidates) inside Scheduler.Schedule. Code anchor:cmd/ateapi/internal/scheduling/scheduling.go.
Why P0:ate.workerpool.workers (utilization) and Initial set of platform metrics around lifecycle, scheduling, and fleet-saturation #433's assignment-duration (outcome) bracket the decision but miss how close to empty it was and why. ErrNoCapacity fires when the constraint intersection is empty — which can happen while the pool is half idle (e.g., paused actors pinned via RequiredNodes to a cordoned node). Without candidate counts, both states produce identical 503s with opposite remediations (scale up vs. fix selectors/drain policy). The candidate-count distribution trending toward zero is the leading saturation indicator feat: WorkerPool autoscaling — demand-reactive capacity for warm worker pools #198 needs — it warns before the first rejection, whereas the 503 rate only reports after users are failing.
CUJ:"Resumes are failing with 503 but ate.workerpool.workers shows idle workers — why?" Alert: p50 candidates < N as a pre-saturation warning.
Golden signal: Saturation (supply side). North star: makes scale-to-zero and #198 autoscaling operable — closes the control loop between demand signals and delivered capacity.
Measures: per pool, how many worker pods were requested (spec.replicas) versus how many are actually Ready — whether commanded capacity was delivered.
Labels:ate.workerpool.name on both instruments. Dedicated instruments rather than one metric with a state attribute, matching K8s semconv (k8s.deployment.desired_pods / k8s.deployment.available_pods): desired + ready is not a meaningful sum, unlike idle + assigned on the existing ate.workerpool.workers.
Emitted by: atecontroller, from the WorkerPool status sync, which already reads both numbers every reconcile. Code anchor:cmd/atecontroller/internal/controllers/workerpool_controller.go:101-121.
Why P0: the other saturation signals say when to scale; nothing says whether scaling worked. With the scale subresource + pod selector merged (Add pod selector to the WorkerPool scale subresource #547) and feat: WorkerPool autoscaling — demand-reactive capacity for warm worker pools #198 about to write spec.replicas programmatically, an autoscaler without a delivery signal is an open control loop: desired rises, nodes are exhausted, ready flatlines, and every substrate-side metric insists the mitigation was applied while requests keep getting 503s. desired − ready is also the anti-windup input feat: WorkerPool autoscaling — demand-reactive capacity for warm worker pools #198's warm-buffer refill computes against (capacity already in flight). Failures in this gap (quota, nodepool exhaustion, stuck pods) belong to the cluster-admin persona — this metric converts "the substrate seems full" into "the cluster owes us 20 pods."
CUJ:"I scaled the pool (or the HPA did) — did the capacity actually arrive?" Alert: desired − ready > 0 sustained beyond a few minutes.
Prerequisite
Metric 4 requires OTel wiring in atecontroller (internal/serverboot init), which currently has none — only the unscraped controller-runtime registry on :8080. This wiring is a one-time cost also needed by planned P1/P2 atecontroller metrics (golden-snapshot pipeline), so doing it here pays it down early.
What's next
I propose metrics based on north star and SRE golden signals. More metrics proposal will come in separate issue, this issue focus most important metrics in my opinion. Open for discussion and feedbacks.
Activation SLI, crash accounting, and capacity signals
Related: #433, #198, #27, #174, #547, #166
Summary
This proposal covers the four metrics that complement the initial set in #433.
All four follow the #433 cardinality rule — labels limited to template, worker pool, sandbox class, and closed outcome/reason enums; never actor, atespace, or session — and reuse the
internal/ateattrlabel conventions.Naming follows OTel semantic conventions (revised per review): dedicated instruments instead of a
stateattribute; self-descriptive instrument names (eligible_workers); all new attribute keys namespaced underate.*; anderror.typerestricted to standardized values (gRPC status codes at RPC boundaries), with substrate-specific taxonomies in namespaced attributes such asate.failure.reason.Each metric is either a label extension on an existing instrument or a single counter/gauge at a code choke point where the data is already in hand, so implementation cost is low.
Relationship to the north stars (docs/architecture.md)
atenet.router.route.durationmixes warm routes (ms) and cold starts (up to 20s+) in one distributionresume=triggeredresume=triggeredate.actors(total actors tracked in the store). Metrics 2–4 provide what operating at that scale requires: actor-loss rate (2) and saturation visibility (3, 4)At a glance
atenet.router.route.duration— new labelsate.actor.crashesate.scheduler.eligible_workersate.workerpool.desired_workers/ate.workerpool.ready_workers1. Extend
atenet.router.route.durationwithresumeand richeroutcomelabelsGolden signals: Latency + Traffic. North star: makes the 100ms p95 activation target and the 1000 wakeups/sec throughput target measurable.
ResumeActor+ authority rewrite), excluding actor compute and the response.ate.router.resume ∈ {none, triggered, joined}:none— actor was already RUNNING; the call was a pure location lookup. This series is the steady-state routing SLI every warm request pays.triggered— this request won the per-actor singleflight and executed the resume. Its latency is the activation latency measured at the edge — the series for the 100ms p95 north star. Its rate is wakeups/sec — the throughput north star.joined— a resume was already in flight; this request parked on the shared call. Kept separate because it is a biased sample of activation latency: one 20s cold start with 50 queued callers must count as 1 slow activation in SLO math, not 51.outcome=errorintono_capacity(FailedPrecondition "no free workers available"),timeout,lock_conflict(Aborted),resume_error. "Fleet is full" (capacity/ops problem) and "routing is broken" (bug) are currently the same data point.actor_template_namespace,actor_template_name,outcome) predate the namespacing convention — migrate them toate.template.namespace,ate.template.name,ate.router.outcomein the same change, with a coordinated dashboard update, rather than leaving two conventions side by side.cmd/atenet/internal/router/extproc.go(classifyOutcome),resumer.go(singleflight).resumesplit, the platform's most important promise has no measurement at the user boundary.outcome=no_capacityrate is the capacity-pressure signal feat: WorkerPool autoscaling — demand-reactive capacity for warm worker pools #198 specifies at the request edge, and the baseline for how many requests Router needs to park requests and wait for capacity #27's parking would save. Thejoined:triggeredratio quantifies wake storms.2.
ate.actor.crashes(counter)Golden signal: Errors. North star: reliability accounting required to operate at 1B-actor scale — today the platform cannot state its actor-loss rate.
operation.name ∈ {resume, suspend, pause};ate.failure.reason— theateerrors.Reasonenum (TERMINAL_FILE_SYSTEM_ERROR,INVALID_SANDBOX_ASSET,INVALID_CHECKPOINT_RESULT,FAILED_SAVE_SNAPSHOT,INVALID_OBJECT_URL,FAILED_GET_EXTERNAL_OBJECT,INVALID_CONTAINER_CONFIG) plus control-plane causes (corrupted_assignment,worker_reassigned,worker_pod_gone);ate.template.name/namespace,ate.workerpool.name,ate.sandbox.class.crashActor, the single choke point every terminal failure flows through. Code anchors:cmd/ateapi/internal/controlapi/crash.go;internal/ateerrors/ateerrors.go.TERMINAL_FILE_SYSTEM_ERROR= node disks full;FAILED_GET_EXTERNAL_OBJECT= storage backend unhealthy; per-template attribution catches one bad template crashing repeatedly.3.
ate.scheduler.eligible_workers(histogram,{worker})Golden signal: Saturation. North star: protects the activation-latency target under load; direct input to #198's warm-buffer controller.
ate.workerpool.name(when determinable),ate.sandbox.class,ate.scheduling.constraint ∈ {none, required_nodes, selector}.len(candidates)insideScheduler.Schedule. Code anchor:cmd/ateapi/internal/scheduling/scheduling.go.ate.workerpool.workers(utilization) and Initial set of platform metrics around lifecycle, scheduling, and fleet-saturation #433's assignment-duration (outcome) bracket the decision but miss how close to empty it was and why.ErrNoCapacityfires when the constraint intersection is empty — which can happen while the pool is half idle (e.g., paused actors pinned via RequiredNodes to a cordoned node). Without candidate counts, both states produce identical 503s with opposite remediations (scale up vs. fix selectors/drain policy). The candidate-count distribution trending toward zero is the leading saturation indicator feat: WorkerPool autoscaling — demand-reactive capacity for warm worker pools #198 needs — it warns before the first rejection, whereas the 503 rate only reports after users are failing.ate.workerpool.workersshows idle workers — why?" Alert: p50 candidates < N as a pre-saturation warning.4.
ate.workerpool.desired_workers/ate.workerpool.ready_workers(observable UpDownCounters,{worker})Golden signal: Saturation (supply side). North star: makes scale-to-zero and #198 autoscaling operable — closes the control loop between demand signals and delivered capacity.
spec.replicas) versus how many are actually Ready — whether commanded capacity was delivered.ate.workerpool.nameon both instruments. Dedicated instruments rather than one metric with astateattribute, matching K8s semconv (k8s.deployment.desired_pods/k8s.deployment.available_pods): desired + ready is not a meaningful sum, unlike idle + assigned on the existingate.workerpool.workers.cmd/atecontroller/internal/controllers/workerpool_controller.go:101-121.spec.replicasprogrammatically, an autoscaler without a delivery signal is an open control loop: desired rises, nodes are exhausted, ready flatlines, and every substrate-side metric insists the mitigation was applied while requests keep getting 503s.desired − readyis also the anti-windup input feat: WorkerPool autoscaling — demand-reactive capacity for warm worker pools #198's warm-buffer refill computes against (capacity already in flight). Failures in this gap (quota, nodepool exhaustion, stuck pods) belong to the cluster-admin persona — this metric converts "the substrate seems full" into "the cluster owes us 20 pods."desired − ready > 0sustained beyond a few minutes.Prerequisite
Metric 4 requires OTel wiring in atecontroller (
internal/serverbootinit), which currently has none — only the unscraped controller-runtime registry on:8080. This wiring is a one-time cost also needed by planned P1/P2 atecontroller metrics (golden-snapshot pipeline), so doing it here pays it down early.What's next
I propose metrics based on north star and SRE golden signals. More metrics proposal will come in separate issue, this issue focus most important metrics in my opinion. Open for discussion and feedbacks.