DreamLake

Telemetry and introspection

The nymph daemon emits structured logs and — when you enable it — exposes a loopback-only HTTP server that surfaces its current status and a buffer of recent log events. nymph tui is the primary consumer; curl works just as well.

Nothing here leaves the host. The server is unauthenticated because it refuses to bind anywhere but loopback.

Structured logging

Configured under [log] in udf-daemon.toml:

udf-daemon.tomltoml
[log]
level = "info"          # error | warn | info | debug | trace
format = "human"        # human | json
event_buffer = 1024     # recent-events ring capacity; 0 disables it
KeyTypeDefaultMeaning
levelstring"info"Minimum level to emit.
formatstring"human"human (pretty) or json (one object per line).
event_bufferinteger1024Capacity of the ring buffer /events serves. 0 disables it — the layer still installs but drops everything.

Environment overrides

Env varOverridesNotes
RUST_LOGlog.levelFull tracing EnvFilter syntax, e.g. RUST_LOG=nymph::drain=debug,hyper=warn. Highest precedence.
LAKESHORE_LOG_FORMATlog.formathuman or json, case-insensitive. Highest precedence.

An unrecognised format value falls back to human output and prints nymph: unknown log.format <value>; falling back to "human" to stderr.

With format = "json" the daemon installs tracing_subscriber's JSON layer, so each line is one object carrying timestamp, level, target, and a fields object (the log message itself lands inside fields as message). Point a log shipper at stdout.

The same events are mirrored into the ring buffer the introspection server reads, using the slightly different RecordedEvent shape described below.

The introspection server

Off by default. Enable it under [introspect]:

udf-daemon.tomltoml
[introspect]
enabled = true          # default false
bind = "127.0.0.1"      # loopback only
port = 9876             # default; 0 = OS-assigned ephemeral
KeyTypeDefaultMeaning
enabledbooleanfalseMaster switch. TOML is the only way to turn this on.
bindstring"127.0.0.1"Interface to bind. A non-loopback address is refused.
portinteger9876TCP port. 0 lets the OS pick — useful in tests.

The default endpoint is http://127.0.0.1:9876.

LAKESHORE_INTROSPECT does not work

A comment in nymph/src/config.rs advertises a LAKESHORE_INTROSPECT environment variable. Nothing in the codebase reads it. Set [introspect] enabled = true in the TOML — there is no env-var shortcut.

Two behaviours worth knowing:

  • Startup order. The server binds before /hello, so a daemon that cannot reach the control plane is still inspectable.
  • Bind failure is not fatal. A refused or unavailable address logs a warning and the daemon continues without introspection. A non-loopback bind (e.g. 0.0.0.0) is an error at that point — introspection is skipped, the daemon runs on.
Loopback-only by design

The server has no authentication. That is safe only because it will not bind a routable interface. Do not put a reverse proxy in front of it.

The implementation is a hand-rolled HTTP/1.1 server on a tokio::net::TcpListener — no keep-alive, explicit Content-Length, Connection: close. Every response is JSON.

RequestResponse
GET /status200 — the StatusSnapshot object.
GET /events200 — { count, capacity, events[] }.
GET / or /healthz200 — {"ok":true}.
Any other path404 — {"error":"not_found","message":"no such route"}.
Any non-GET method405 — {"error":"method_not_allowed","message":"only GET is supported"}.

GET /status

status.jsonjson
{
  "version": "0.1.4",
  "worker_id": "65f…",
  "queue": "default",
  "run_state": "polling",
  "backoff_s": 0.0,
  "last_poll_ok_ts": 1750000000.5,
  "uptime_s": 42.7,
  "inflight": { "count": 1, "last_exec_id": "01J…" }
}
FieldTypeMeaning
versionstringDaemon binary version (CARGO_PKG_VERSION).
worker_idstring | nullWorker id in use — server-issued when available, else the local {machine_id}-{ulid}. Null before hello.
queuestring | nullQueue this daemon subscribes to.
run_statestringstarting | polling | backoff | hibernating | draining.
backoff_snumberCurrent poll backoff. 0.0 in steady state; grows on repeated failures.
last_poll_ok_tsnumber | nullUnix seconds of the last successful poll.
uptime_snumberSeconds since daemon boot.
inflightobject{ count, last_exec_id }. count covers invocations and execs; last_exec_id is a best-effort hint, not a list.

These field names are the nymph tui contract — treat them as stable.

GET /events

The ring buffer, oldest → newest:

events.jsonjson
{
  "count": 2,
  "capacity": 1024,
  "events": [
    { "ts": 1750000000.1, "level": "INFO", "target": "nymph::drain",
      "message": "first event", "fields": {} },
    { "ts": 1750000000.2, "level": "WARN", "target": "nymph::drain",
      "message": "poll failed", "fields": { "reason": "connection timeout" } }
  ]
}
Query paramDefaultMeaning
limit=N200Return at most the N most-recent events after filtering.
level=L(no filter)Only events at or above severity L: error, warn/warning, info, debug, trace (case-insensitive). An unrecognised value skips the filter rather than erroring.

Level ranks are ERROR=1, WARN=2, INFO=3, DEBUG=4, TRACE=5, and the filter keeps everything with a rank at or below the requested one.

Each entry is a RecordedEvent: ts (unix seconds, float, sub-second resolution), level (uppercased), target (the tracing target / module path), message (empty when the event carried only structured fields), and fields (a string → string map — every value is stringified, ordered for stable output). count is the number returned after filtering; capacity is log.event_buffer.

The buffer is a VecDeque behind a mutex; the oldest entry is evicted once it is full.

Read next

  • nymph tui — the live monitor that renders /status and /events.
  • Daemon configuration — the full udf-daemon.toml schema.
  • Daemon protocol — the control-plane channel, which is entirely separate from this local server.