|
| 1 | +# Portable observability |
| 2 | + |
| 3 | +Configure `instrumentation(event)` to receive structured events. No exporter SDK |
| 4 | +is required. The same JSON envelope is emitted by Ruby, SQLite, PostgreSQL, MySQL, |
| 5 | +and the Durable Objects host. Existing JavaScript `name`, `occurredAt`, and |
| 6 | +`attributes` fields remain available. Ruby's Active Support notifications remain |
| 7 | +available with their existing snake_case payloads. |
| 8 | + |
| 9 | +## Schema version 1 |
| 10 | + |
| 11 | +Every event has `schemaVersion`, `name` (prefixed with `solid_objects.`), |
| 12 | +`occurredAt` (UTC ISO 8601), `adapter`, `actorType`, `actorId`, `incarnation`, |
| 13 | +`revision`, `messageId`, `attempt`, `attributes`, and `metrics`. |
| 14 | +Unavailable identifiers are null; `attempt` is zero outside a message attempt. |
| 15 | +An incarnation identifies a persisted actor instance, independently of its lease |
| 16 | +generation. Revisions and IDs are strings. Process-wide events have null actor |
| 17 | +identity. A message ID or revision correlates actor work where applicable. |
| 18 | + |
| 19 | +Only known scalar metadata fields enter the portable envelope. Arguments, state, |
| 20 | +results, credentials, backtraces, exception text, nested provider data, and unknown |
| 21 | +attributes are excluded. Actor IDs remain correlation data: applications should |
| 22 | +use opaque actor identifiers and apply their own retention policy to event logs. |
| 23 | +Events and metric samples are immutable. Throwing observers, rejected observer |
| 24 | +promises, and a failing instrumentation error logger cannot change a turn's |
| 25 | +result. Delivery is best effort and synchronous callbacks should be short; |
| 26 | +JavaScript does not await exporters. Telemetry is not a durable audit trail. |
| 27 | + |
| 28 | +| Event | Meaning | |
| 29 | +| --- | --- | |
| 30 | +| `activation.started/completed/failed` | Local actor activation hook lifecycle | |
| 31 | +| `message.started/completed/failed/rejected` | One attempt's execution outcome | |
| 32 | +| `message.retry` | Failed attempt durably queued for another attempt | |
| 33 | +| `dead_letter.created` | Message exhausted retries or failed permanently | |
| 34 | +| `mailbox.depth` | On-demand diagnostic sample; exact depth only if not truncated | |
| 35 | +| `reminder.enqueued` | Due reminder dispatch; lateness is measured from due time | |
| 36 | +| `outbox.age` | Delivery observation; age is time since the item's current availability time | |
| 37 | +| `recovery.reclaimed` | A previously claimed, interrupted message begins another attempt | |
| 38 | +| `recovery.completed/failed` | Durable effect recovery callback commits or enters the dead-letter queue | |
| 39 | +| `snapshot.read` | Authorized snapshot constructed without exposing its contents | |
| 40 | +| `realtime.connected/disconnected` | Actor subscription added or removed | |
| 41 | + |
| 42 | +Events describe local observations. Concurrent deletion, crashes, and failed |
| 43 | +exporters can omit events. Never infer exactly-once delivery from event counts. |
| 44 | +Additional existing runtime events retain their names. |
| 45 | + |
| 46 | +## Metrics and tracing |
| 47 | + |
| 48 | +Metrics are sample descriptions. Exporting them is opt-in: the runtime does not |
| 49 | +register meters, allocate per-actor metric series, or install a vendor SDK. |
| 50 | + |
| 51 | +| Name | Kind | Unit | Aggregation | |
| 52 | +| --- | --- | --- | --- | |
| 53 | +| `solid_objects.events` | counter | `1` | Sum one per event | |
| 54 | +| `solid_objects.duration` | histogram | `ms` | Distribution of observed attempt duration | |
| 55 | +| `solid_objects.reminder.lateness` | histogram | `ms` | Distribution of reminder dispatch delay | |
| 56 | +| `solid_objects.outbox.age` | histogram | `ms` | Distribution of delivery delay since availability | |
| 57 | +| `solid_objects.mailbox.depth` | gauge | `1` | Last exact sampled actor depth; omit truncated samples | |
| 58 | + |
| 59 | +Labels contain only event name, adapter family, and declared actor type. Keep the |
| 60 | +actor type registry finite. Never add actor ID, incarnation, message ID, operation |
| 61 | +arguments, request IDs, or error text to metric labels. A gauge without an actor |
| 62 | +label represents the most recently observed actor; it is not total fleet backlog. |
| 63 | +For tracing, correlate start/outcome events using `adapter`, `incarnation`, |
| 64 | +`messageId`, and `attempt`, and close or expire spans when no outcome arrives. |
| 65 | + |
| 66 | +## Actor observers and diagnostics |
| 67 | + |
| 68 | +```ruby |
| 69 | +cart = ShoppingCart.ref("demo-cart") |
| 70 | +stop = cart.observe(authorization_context: operator) do |event| |
| 71 | + logger.info(event.to_json) |
| 72 | +end |
| 73 | +summary = cart.diagnostics(authorization_context: operator, limit: 50) |
| 74 | +stop.call |
| 75 | +``` |
| 76 | + |
| 77 | +Ruby uses `reference.observe(authorization_context:) { |event| ... }` and |
| 78 | +`reference.diagnostics(authorization_context:, limit: 50)`. Stop observing by |
| 79 | +calling the returned proc. `on` filters one event name, such as `message.retry`. |
| 80 | +Observers receive only this actor's events in the current runtime/process; they |
| 81 | +are not subscriptions to workers on other hosts. Dispose them when the caller's |
| 82 | +session ends or authorization is revoked. JS limits local observers to 1,000. |
| 83 | +For remote Durable Objects, configure `instrumentation` on the actor host and |
| 84 | +filter by actor identity there; process-local reference observers raise |
| 85 | +`UnsupportedCapability`. Remote `reference.diagnostics` is supported. |
| 86 | + |
| 87 | +Both APIs default to denied. Set `authorizeAdministration` / `authorize_administration` |
| 88 | +to allow action `observe` or `inspect`, resource `actor_diagnostics`, and resource ID |
| 89 | +`JSON.stringify([actorType, actorId])`. Ruby receives a symbol action. Authorization |
| 90 | +runs before reading summaries or registering observers; possessing an actor ID |
| 91 | +confers no permission. |
| 92 | + |
| 93 | +Diagnostics read at most `limit + 1` rows per queue source, with a hard limit of |
| 94 | +100. Each category returns `sampled`, `truncated`, and `oldestAgeMilliseconds`. |
| 95 | +The last value measures nonnegative time since availability (or terminal failure |
| 96 | +for recovery callbacks); future reminders have zero age. No payloads or row |
| 97 | +identifiers are returned. Samples are observations across several queries, |
| 98 | +not an atomic fleet snapshot. Large queues can still require database scanning; |
| 99 | +the bound limits materialized rows and response size, not query execution time. |
| 100 | + |
| 101 | +Categories are mailbox (ready and claimed), outbox (pending and processing effects |
| 102 | +and broadcasts), reminders (scheduled and paused), retries (failed messages still |
| 103 | +eligible to run), and recoveryFailures (dead internal effect recovery |
| 104 | +callback messages). Durable Objects does not implement process-heartbeat effect recovery; |
| 105 | +its recoveryFailures category is empty. Recovery failures are durable records, |
| 106 | +not a history of transient database or exporter exceptions. |
| 107 | + |
| 108 | +## A query that works across adapters |
| 109 | + |
| 110 | +Write one envelope per line to `events.jsonl`, using the same instrumentation hook |
| 111 | +with SQLite and PostgreSQL. This query reports completed attempts by adapter: |
| 112 | + |
| 113 | +```sh |
| 114 | +jq -s 'map(select(.name == "solid_objects.message.completed")) |
| 115 | + | group_by(.adapter) |
| 116 | + | map({adapter: .[0].adapter, completed: length, |
| 117 | + mean_ms: (map(.attributes.durationMilliseconds) | add / length)})' events.jsonl |
| 118 | +``` |
| 119 | + |
| 120 | +A dashboard can chart the event counter by adapter and outcome and the duration, |
| 121 | +reminder lateness, and outbox age distributions using exactly the same fields. |
| 122 | +Compare workloads with the same actor types and sampling policy. Counts measure |
| 123 | +observations and cannot replace a database query for authoritative queue state. |
0 commit comments