Telemetry and operator inspection

Copy Markdown View Source

SmolBox emits optional asynchronous :telemetry observations from explicitly started managed runtimes. Stored execution evidence is authoritative. Notifications are lossy: they can be dropped, repeated during reconciliation, or interrupted between stage start and stop. They are unsuitable for driving workflows, billing, receipts, or exactly-once delivery. Low-level SmolBox.Client calls do not emit these managed lifecycle events.

Use SmolBox.Telemetry.events/0 to register a handler with :telemetry.attach_many/4. Prefer a named external function. The host owns attachment, detachment, export and retention. A handler should finish promptly; any downstream queue or service it uses needs its own bounds and redaction policy.

Attach a handler

This complete example logs event names at debug level without exposing execution identifiers, command output, or artifact data:

defmodule MyApp.SmolBoxEvents do
  require Logger

  def handle_event(event, _measurements, _metadata, _config) do
    Logger.debug(fn -> "SmolBox event: " <> Enum.join(event, ".") end)
  end
end

:ok = :telemetry.attach_many(
  "my-app-smolbox-events",
  SmolBox.Telemetry.events(),
  &MyApp.SmolBoxEvents.handle_event/4,
  nil
)

Attach once during host startup. Remove the handler with :telemetry.detach("my-app-smolbox-events"). Applications that export measurements can use the fields below, with bounded queues and finite metric labels.

Events and measurements

All names start with [:smolbox]. Execution events carry count, stored version, observed_at_ms, age_ms, and the current reservation measurements reserved_slots, reserved_cpus, reserved_memory_mb, reserved_disk_gb. Released reservations report zero. An observed result adds exit_code, stdout_bytes, stderr_bytes and numeric truncated (0 or 1). Unknown command outcomes have no fabricated exit measurement. Persisted cancellation intent adds the original cancel_requested_at_ms; repeated cancellation does not reset it.

SuffixMeaning and additional measurements
[:execution, :accepted]Store acknowledged insertion of a new identity; duplicate lookup emits no second acceptance
[:execution, :reserved]Store acknowledged resource assignment; queue_wait_ms is elapsed stored wall-clock time since acceptance
[:execution, :updated]A meaningful state/evidence/result/collection/cleanup write was acknowledged; lease and scan-only writes are omitted
[:execution, :cancel_requested]Store acknowledged cancellation intent; inspect evidence for actual termination
[:execution, :released]Store acknowledged capacity release
[:stage, :start]Attempt started; system_time_ms gives the observation timestamp
[:stage, :stop]Attempt returned or raised; duration_ms is monotonic elapsed time
[:worker, :status]Worker status changed or drain was explicitly requested; count: 1
[:store, :error]A store call returned an error; count: 1, finite operation and category metadata; expected not-found reads can also appear

Stage metadata identifies :preparation, :execution, :collection or :cleanup. Preparation includes machine preparation and input staging. Execution covers the controller's command observation attempt, including transport; it is not isolated guest CPU time. Collection covers output retrieval and persistence. Cleanup measures each active reconciliation attempt, excluding retention waits and controller downtime. Start has outcome :pending; stop has :returned, :error or :exception. A returned attempt can still record an unknown or failed execution; this category does not certify business success. No exception reason is emitted.

Execution :updated metadata includes persisted state, evidence, collection and cleanup. This exposes dispatch intent, observed running/exit, unknown outcome, collection failure and cleanup failure without suggesting that a notification is a worker receipt. Wall-clock differences are clamped at zero and depend on the host's clock discipline. They are not cross-host monotonic durations.

Payload privacy and cardinality

Events include configured runtime and namespace. Execution/stage events add bounded scope and execution_id; worker observations add worker_id and finite platform/status. Identifiers belong only in access-controlled traces. Choose finite event kind, stage, platform and outcome categories for metric labels. Do not label metrics with arbitrary runtime, namespace, scope or worker IDs.

Payloads omit command arguments, environment, stdin, business metadata, fingerprints, artifact paths/digests/bytes, stdout/stderr and raw errors. Numeric measurements are bounded signed integers. Malformed projections are discarded rather than substituting raw data. No opt-in raw-output sink is provided by this library. Third-party HTTP telemetry, exporter logging, and host code remain outside this contract; inspect those separately before exporting from a sensitive environment.

Bounded delivery and failures

Runtime options telemetry_max_pending (default 128, range 1..1024) and telemetry_timeout_ms (default 100, range 1..1000) control delivery. One handler delivery process runs at a time per runtime. The pending cap includes active, queued and reserved/in-transit notifications. Overflow is dropped before mailbox delivery; producers do not wait for exporters. Each event is a small bounded projection, independent of command/file/output length.

A handler deadline terminates that delivery process. Its execution task and the coordinator continue. Handler failure follows :telemetry's handler behavior; ordinary exceptions can detach that handler. Dispatcher failure restarts the notification child separately from the work subtree. This is ordinary OTP fault isolation, not protection against hostile BEAM code, exhausted VM resources or repeated failures exceeding supervisor restart intensity. Handlers can spawn other work, and that work is outside this dispatcher's supervision and capacity bound. Scheduling pauses can delay timer handling; the timeout is not a real-time limit.

SmolBox.telemetry_stats(runtime) returns {:ok, stats} with available, epoch, limit, pending, processed, dropped and timed_out. These are ephemeral counters. processed means the delivery process returned normally, not that an exporter durably received the event or every attached handler succeeded. Timeouts are counted separately from drops. Concurrent rejected offers can briefly make the pending snapshot exceed the limit while returning their speculative credits; the number of accepted event messages remains bounded.

Dispatcher restart resets counters and changes epoch. Two consecutive idle sweeps also retire credits abandoned by producers that died before sending. Late messages from that retired epoch are discarded. Consequently, compare counters only within the same epoch; missing events and reset counters are expected. Never use them to reconstruct durable execution accounting.

Inspecting authoritative state

Hosts must authorize all inspection calls. SmolBox.fetch/3 returns the stored record, including the full specification and result: unlike telemetry, it is a protected data API. Use state, evidence, version, cancel_requested_at_ms, next_due_at_ms, deadlines, result, artifacts, collection, cleanup, last_error and reservation to distinguish command outcome, output availability and remaining resource ownership. Do not expose the full record to an unauthenticated client.

SmolBox.workers/1 gives version/qualification, architecture/platform, recent health, drain status, allocation floors and configured capacity. Capacity is a host declaration, not measured free resources. SmolBox.audit_worker/3 provides bounded read-only orphan inspection; it never adopts or deletes resources. fetch, workers, telemetry_stats and audit_worker are observational. cancel records intent that can lead to owned-guest termination. reconcile advances observation/cleanup of existing evidence and never authorizes replay. A new authorized execution requires a new identity via submit.