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
Actors cannot correctly identify their own telemetry, so all telemetry from an ActorTemplate collapses into a single time series with many writers.
This proposes that ateom expose a fixed OTLP endpoint inside the worker pod, stamp trusted ate.dev/* identity onto everything that arrives.
The principle is one Substrate already applies to logs:
Attribution belongs to the channel, not the payload.
internal/actorlog/logger.go:132-158 already does this for stdout — atelet injects ate.dev/actor_atespace, actor_name, actor_uid, actor_template_namespace, actor_template_name, container_name into every log line, out of band. The actor never touches them, so they can't be frozen, forged, or lost. Metrics and traces are the signals that haven't caught up.
It's also the same division of labour Kubernetes already uses. Under Prometheus, identity comes from the scrape target, not the workload — instance and podare assigned by the scraper, and emitting them yourself is an anti-pattern.
Actors can't be scraped, so the relay is that job relocated to somewhere it can still happen. This is not a novel mechanism; it's the conventional one adapted to push.
2. Goals
Every actor's telemetry is attributed to that actor, with no cooperation from actor code beyond pointing at an endpoint.
Attribution is trusted — derived by the platform, not asserted by the workload — so it can back per-tenant views, quota, and billing later.
Attribution survives suspend/resume/migration with nothing re-derived.
Storage cardinality is bounded and independent of actor count.
The actor's exporter config is a constant address, so it survives the golden snapshot intact. What answers at that address is per-actor, re-created on every activation. Substrate already relies on this trick twice: the constant interior IP (cmd/ateom-microvm/net.go:28-30 — "Because that address is a CONSTANT, a restored guest's frozen network config stays valid on any pod") and the constant identity path /run/ate/actor-id.
5. Design
5.1 Transport
169.254.17.1:4317 — the existing worker-side veth gateway, already created per activation by both runtimes.
Chosen over a Unix socket in the identity dir because:
It works on micro-VM.cmd/ateom-microvm/spec.go:91-96 records that the identity bind mount is dropped entirely there — "The micro-VM guest can't see host paths" — so a UDS is unavailable. But kata wires the guest to the same veth via virtio-net (cmd/ateom-microvm/net.go:26-30), so a network endpoint works on both runtimes with no new plumbing.
No host-fd exposure. runsc gates host Unix sockets behind --host-uds, which cmd/ateom-gvisor/runsc.go:84-96 does not pass. Enabling it would widen the sandbox surface and need a threat-model review. A netstack connection needs neither.
Checkpoint safety. An open host fd may block runsc checkpoint; a netstack socket is ordinary severed-connection handling.
The cost is that a shared address relies on one-actor-per-worker for unambiguous attribution. That holds today by definition (docs/glossary.md:30-32) and is the thing to revisit if concurrency lands
Env is safe here because these are constants. The golden-snapshot hazard applies to values that must differ per actor; these do not, so freezing them freezes the right answer.
Note these are captured into the golden, so changing them on an existing template requires a golden rebuild (docs/api-guide.md:101: values are inherited "until the golden snapshot is recreated").
Actors emit no identity attributes. Anything they do emit under ate.dev/is overridden.
5.3 Attribution
ateom sets, overriding any existing value:
service.instance.id (= actor UID; the standard name for this)
ate.dev/actor_uid
ate.dev/actor_name
ate.dev/actor_atespace
ate.dev/actor_template_namespace
ate.dev/actor_template_name
ate.dev/container_name
That is the set for traces and logs. Metrics get a strict subset (§5.4): actor_uid, actor_name, service.instance.id and actor_atespace are all dropped, because all four are unbounded. The signals still join — on the smaller set — and an exemplar walks from a metric datapoint to a span carrying the rest.
Overriding rather than merging is the point: a workload must not be able to attribute its telemetry to another actor or another atespace.
5.4 Cardinality policy
Signal
Actor identity
Rationale
Metrics
stripped before forwarding
a series is durable: index entry, interned labels, chunks, retained for the full window after the actor dies
Traces
kept
a span is an event, not a series — 10k actors/day is 10k rows, not 200k index entries
Logs
kept
already the case today
Forwarded metric labels: actor_template_namespace, actor_template_name, container_name. Output series count is templates × containers × metrics — constant, and independent of how many actors ever ran.
5.5 Forwarding and enrichment
Forwarded telemetry must not be re-enriched with k8s.pod.name / k8s.node.name upstream. Those describe the current host, change on every migration, and are what makes the failure read like ordinary pod churn. ateom should stamp ate.dev/worker_pod / ate.dev/worker_node instead — clearly named as host facts, not workload identity — and keep them off metric resources entirely.
ateom does not dial the cluster collector. It forwards over a node-scoped unix socket served by atelet, which holds the single upstream connection for the node. A direct path requires the worker pod to have network egress to the collector, and the worker pod is the pod running untrusted actor code. Forwarding over a unix socket on a host directory the pod already mounts means the worker pod can be denied network egress entirely and telemetry still flows. No configuration of a direct path yields that property.
Nothing to re-derive. The frozen exporter config points at a constant address; on resume that address is served by the new worker's ateom, which knows the new activation. The gRPC client sees a dead connection and reconnects to the same address, which now leads somewhere else. That is the whole mechanism.
This is why attribution does not depend on #450, though #450 is still needed for flushing (#503).
5.7 Temporality
Delta is required, not preferred.
Under cumulative, ateom would have to hold each actor's last-seen value and compute the merged total itself. On migration the destination ateom has no history and the merged series drops — the original bug, one layer up. Under delta each export is self-contained, so contributions from different workers simply add.
Delta also cancels the golden baseline. The SDK's last-collection bookmark is snapshotted alongside the counter, so a restored actor reports only its own work.
Delta is genuinely lossier. Cumulative self-heals across a dropped export; delta does not, so an onPause: Data kill loses that window permanently. Still the right trade, because what cumulative preserves faithfully is a wrong number.
6. Open questions
Concurrency. A shared worker-pod address relies on one-actor-per-worker. If that changes, attribution needs per-actor addressing or a per-actor socket. Worth knowing the cost before it's forced.
Gauges: Delta temporality does not apply to gauges. Two actors of one template writing the same gauge now share one series and race on last value. ateom cannot fix this. Maybe we should just make it explicit that per-actor (instance) gauges do not work for substrate and workload should emit logs/events when per-actor gauges are needed, and prefer counters and histograms wherever the quantity is additive.
Actor telemetry shares ateom's channel to atelet: Identities do not get mixed up, because atelet forwards messages unchanged and OTLP keeps a separate resource block per sender. The problems are elsewhere. First, if the socket is missing, ateom: export telemetry through an atelet unix-socket OTLP relay #809 falls back to sending over the pod network; that is right for ateom's own data but wrong for actor data, since this design uses the socket so the worker pod needs no network at all. Second, one connection per node carries both kinds, so a noisy actor can delay Substrate's own telemetry. Suggested order: stop the fallback for actor telemetry and rate-limit it in ateom now, then give atelet a second connection to the collector, and only later give actor telemetry its own socket — and only when atelet itself must treat the two kinds differently. Note that a separate socket only tells the two kinds apart; it does not prove who sent the data unless the file permissions change too.
Design: per-actor otel telemetry relay
Status: draft / for discussion
Related: #761 (actor instrumented metrics issue), #503 (telemetry continuity), #450 (lifecycle hooks)
1. Summary
Actors cannot correctly identify their own telemetry, so all telemetry from an ActorTemplate collapses into a single time series with many writers.
This proposes that ateom expose a fixed OTLP endpoint inside the worker pod, stamp trusted
ate.dev/*identity onto everything that arrives.The principle is one Substrate already applies to logs:
internal/actorlog/logger.go:132-158already does this for stdout — atelet injectsate.dev/actor_atespace,actor_name,actor_uid,actor_template_namespace,actor_template_name,container_nameinto every log line, out of band. The actor never touches them, so they can't be frozen, forged, or lost. Metrics and traces are the signals that haven't caught up.It's also the same division of labour Kubernetes already uses. Under Prometheus, identity comes from the scrape target, not the workload —
instanceandpodare assigned by the scraper, and emitting them yourself is an anti-pattern.Actors can't be scraped, so the relay is that job relocated to somewhere it can still happen. This is not a novel mechanism; it's the conventional one adapted to push.
2. Goals
3. Non-goals
onPause: Data. That is OpenTelemetry Application Telemetry Data Loss and Delivery Delays Across Actor Lifecycle Events #503 and needs Actor lifecycle hooks #450'sPreSuspend. Complementary, not a substitute.4. Architecture
The actor's exporter config is a constant address, so it survives the golden snapshot intact. What answers at that address is per-actor, re-created on every activation. Substrate already relies on this trick twice: the constant interior IP (
cmd/ateom-microvm/net.go:28-30— "Because that address is a CONSTANT, a restored guest's frozen network config stays valid on any pod") and the constant identity path/run/ate/actor-id.5. Design
5.1 Transport
169.254.17.1:4317— the existing worker-side veth gateway, already created per activation by both runtimes.Chosen over a Unix socket in the identity dir because:
cmd/ateom-microvm/spec.go:91-96records that the identity bind mount is dropped entirely there — "The micro-VM guest can't see host paths" — so a UDS is unavailable. But kata wires the guest to the same veth via virtio-net (cmd/ateom-microvm/net.go:26-30), so a network endpoint works on both runtimes with no new plumbing.--host-uds, whichcmd/ateom-gvisor/runsc.go:84-96does not pass. Enabling it would widen the sandbox surface and need a threat-model review. A netstack connection needs neither.runsc checkpoint; a netstack socket is ordinary severed-connection handling.The cost is that a shared address relies on one-actor-per-worker for unambiguous attribution. That holds today by definition (
docs/glossary.md:30-32) and is the thing to revisit if concurrency lands5.2 Actor-side configuration
Env is safe here because these are constants. The golden-snapshot hazard applies to values that must differ per actor; these do not, so freezing them freezes the right answer.
Note these are captured into the golden, so changing them on an existing template requires a golden rebuild (
docs/api-guide.md:101: values are inherited "until the golden snapshot is recreated").Actors emit no identity attributes. Anything they do emit under
ate.dev/is overridden.5.3 Attribution
ateom sets, overriding any existing value:
That is the set for traces and logs. Metrics get a strict subset (§5.4):
actor_uid,actor_name,service.instance.idandactor_atespaceare all dropped, because all four are unbounded. The signals still join — on the smaller set — and an exemplar walks from a metric datapoint to a span carrying the rest.Overriding rather than merging is the point: a workload must not be able to attribute its telemetry to another actor or another atespace.
5.4 Cardinality policy
Forwarded metric labels:
actor_template_namespace,actor_template_name,container_name. Output series count istemplates × containers × metrics— constant, and independent of how many actors ever ran.5.5 Forwarding and enrichment
Forwarded telemetry must not be re-enriched with
k8s.pod.name/k8s.node.nameupstream. Those describe the current host, change on every migration, and are what makes the failure read like ordinary pod churn. ateom should stampate.dev/worker_pod/ate.dev/worker_nodeinstead — clearly named as host facts, not workload identity — and keep them off metric resources entirely.ateom does not dial the cluster collector. It forwards over a node-scoped unix socket served by atelet, which holds the single upstream connection for the node. A direct path requires the worker pod to have network egress to the collector, and the worker pod is the pod running untrusted actor code. Forwarding over a unix socket on a host directory the pod already mounts means the worker pod can be denied network egress entirely and telemetry still flows. No configuration of a direct path yields that property.
Related: #809
5.6 Suspend and resume
Nothing to re-derive. The frozen exporter config points at a constant address; on resume that address is served by the new worker's ateom, which knows the new activation. The gRPC client sees a dead connection and reconnects to the same address, which now leads somewhere else. That is the whole mechanism.
This is why attribution does not depend on #450, though #450 is still needed for flushing (#503).
5.7 Temporality
Delta is required, not preferred.
Under cumulative, ateom would have to hold each actor's last-seen value and compute the merged total itself. On migration the destination ateom has no history and the merged series drops — the original bug, one layer up. Under delta each export is self-contained, so contributions from different workers simply add.
Delta also cancels the golden baseline. The SDK's last-collection bookmark is snapshotted alongside the counter, so a restored actor reports only its own work.
Delta is genuinely lossier. Cumulative self-heals across a dropped export; delta does not, so an
onPause: Datakill loses that window permanently. Still the right trade, because what cumulative preserves faithfully is a wrong number.6. Open questions