Skip to content

feat(capsule): astrid-capsule-metrics — runtime telemetry rollup and exposition #705

Description

@joshuajbouw

Summary

Add astrid-capsule-metrics — a first-party capsule that aggregates kernel and capsule telemetry into structured rollups, exposes them over IPC, and optionally publishes Prometheus/OpenTelemetry-shaped exports. The current visibility story is tracing to stderr, kernel logs, and astrid events tail (#685) — adequate for live debugging, terrible for "how is the host actually behaving over the last hour."

Why

Scope

The capsule subscribes to standard topics on the event bus and maintains in-memory + persisted counters/histograms keyed by (metric, principal, capsule, outcome).

Metric families (initial):

  • astrid_tool_calls_total{capsule, tool, outcome} — every tool invocation
  • astrid_tool_call_duration_seconds{capsule, tool} — histogram per tool
  • astrid_llm_tokens_total{capsule, provider, model, direction}direction ∈ {input, output, cache_read, cache_write}
  • astrid_llm_cost_usd_total{capsule, provider, model} — derived from provider pricing tables (config-driven, not hardcoded)
  • astrid_http_egress_bytes_total{capsule, host} — outbound network volume
  • astrid_capability_decisions_total{principal, action, outcome}outcome ∈ {allow, deny}
  • astrid_approval_prompts_total{principal, action, outcome}outcome ∈ {approve, deny, expire}
  • astrid_kv_ops_total{capsule, op}op ∈ {get, put, delete}
  • astrid_event_bus_publishes_total{topic, principal} — bus throughput
  • astrid_kernel_uptime_seconds — gauge

Surfaces:

  • IPC query topic metrics.v1.query — request/response for {metric, filters, range, resolution} → series. The primary consumer is astrid usage report (feat(cli): astrid usage report — aggregated per-agent telemetry rollup #687) and any future TUI dashboard.
  • Prometheus exposition — optional. Capsule binds a localhost listener (capability-gated, off by default) on configurable port and serves /metrics in standard exposition format. Off by default because exposing a local port is a capability decision, not a freebie.
  • OpenTelemetry push — optional, behind a config flag. Pushes OTLP-formatted metrics to a configured collector. For shops that already run an observability stack.
  • Audit-anchored snapshots — periodic snapshot published to a topic so receipts (feat(cli,audit): astrid receipt — cryptographically signed compliance receipt for a session #693) can include "metrics at time T" as a signed property.

Storage:

  • Hot window in-memory (configurable, default 24h, 1m resolution).
  • Cold storage in capsule-local KV. Default retention configurable, default 30 days at 5m resolution downsampled.
  • Both bounded — the capsule must never grow unbounded. Eviction is loud (logs warning if a cardinality budget is hit).

Capabilities required:

  • Subscribe to a curated set of astrid.v1.* and *.v1.* topics (read-only).
  • kv:put/kv:get for its own namespace.
  • net:listen:127.0.0.1:<port> only if Prometheus exposition is enabled.
  • No fs_write outside its capsule sandbox.

Why uniquely Astrid

Most metrics stacks are agent-side scrapes that bypass authorization. Here, the metrics capsule is itself a capability-restricted principal — it only sees topics it's allowed to subscribe to. Sensitive payloads (credentials, secrets-bearing topics) are aggregated by metadata only, never by value. This is consistent with the bus tail design notes in #685.

Out of scope

  • Distributed metrics — single-host only. Multi-host aggregation belongs to a downstream collector via OTLP.
  • Per-event log retention — that's the audit chain's job. Metrics is counters/histograms, not events.
  • Alerting rules — out-of-band; consumers can poll the IPC interface or scrape Prom.
  • Cost pricing tables for every model — start with a pluggable provider pricing config; we are not maintaining the world's pricing.

Design questions

  • Cardinality bounds. What's the max series count before we shed labels? Hard-code a default; configurable. Need a clear behaviour when a capsule misbehaves and emits high-cardinality tool names.
  • Topic taxonomy. We're betting the bus already publishes enough signals. Audit which topics need to exist; some may need to be added/standardised before this capsule is useful. Likely overlaps with the bus-tail topic taxonomy work in feat(cli,kernel): astrid events tail — live event-bus subscriber for IPC debugging #685.
  • Sensitive payload redaction. Credential-bearing topics already redact payloads (per feat(cli,kernel): astrid events tail — live event-bus subscriber for IPC debugging #685). Metrics should additionally avoid recording label values from sensitive topics — confirm the redaction policy carries across both consumers.
  • Time source. Wall-clock from the host. The capsule cannot influence kernel time; just records it.

Dependencies

Acceptance

  • Capsule loads as a stock first-party capsule under the standard manifest.
  • astrid usage report --capsule <name> --range 24h returns aggregated data without scraping logs.
  • Disabled cleanly: removing the capsule causes no kernel-side errors; everything still routes.
  • Cardinality budget enforced — load test with a misbehaving emitter shows graceful shedding, not OOM.

Metadata

Metadata

Assignees

Labels

area/capsuleastrid-capsule: WASM sandboxing and executionfeatNew feature or capabilityneeds-designRequires architectural decision before implementationp2Normal — standard priority

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions