Iris: the embedded observer
LOTUS_OBS=1 makes any Hale binary publish an observation segment (see Operations & debugging). Iris is the program that reads it: it attaches to every…
LOTUS_OBS=1 makes any Hale binary publish an observation
segment (see Operations & debugging). Iris
is the program that reads it: it attaches to every publishing
process on the machine, fuses them into one system graph, and
serves it over HTTP with a live browser view. It ships inside
the hale binary, so there is nothing to install:
hale run --observe path/to/prog.hl # program + observer, one command# … in another terminal, or a browser:open http://127.0.0.1:8787--observe sets LOTUS_OBS=1 on the program and launches
hale iris beside it for the program’s lifetime. Nothing reaches
the program’s own argv; the flag is consumed by hale run.
--observe=<port> serves the session on <port> instead of 8787
(0 asks the kernel for a free one: what a script or a test that
runs beside others wants, since a fixed port is one someone else
may hold).
The session’s lifetime is bounded by hale’s. Kill the hale run
— timeout, a CI cancel, Ctrl-C — and the observer goes with it;
there is never a session left listening that only you know to kill.
It writes to its own pipe, which hale relays to its stderr, so
the program’s stdout stays the program’s: hale run --observe prog.hl | cat gives you the program’s output and returns when the
program does.
The commands
Section titled “The commands”hale iris [port] [artifact.json] attach + fuse, serve :port (default 8787) --diff <a.topology> <b.topology> … with the review view over the pair --diff <diff.json> … over a ready `hale model diff` documenthale iris inspect <artifact.json> [url] artifact-side inspectorhale iris --where print the cache directoryhale iris --build-only materialize + build, print the binaryhale iris attaches to every LOTUS_OBS=1 process it can
see (the registration files under $XDG_RUNTIME_DIR/hale/ or
/tmp/hale-obs/), fuses their rings into one graph, and serves:
/— the live view (locus tower, message topology, transport edges with throughput / depth / latency / loss, supervision events as they fire)./snapshot— the fused graph as JSON, one document./events— the same as a server-sent-event stream.
Iris’s memory is bounded. Its live-locus table holds at most 8192
loci across the processes it watches: past that, a birth is not
tracked, and /snapshot counts those in loci_untracked. The rows
kept are the ones born first — a program’s main, its servers, its
workers — so their traffic stays attributed; a process that births
loci and never dissolves them (or births them faster than a view can
show) fills the table, and its later loci go unseen until rows free
up. If the observer dies, hale iris
says how — a signal is named, and the exit status is 128 plus its
number.
Pass a --dump-topology artifact as the second argument and
iris overlays what the compiler declared on what the runtime
does: declared-but-silent topics, undeclared traffic, drift.
The review view. Give iris a change instead of a snapshot and
it renders the change over the running fleet (key 4):
hale check . --dump-topology=before.topology # before the edithale check . --dump-topology=after.topology # afterhale iris --diff before.topology after.topology # or: --diff diff.json--diff a b runs hale model diff in-process and hands the
document to the observer; --diff doc.json hands over one you
already have. The view lists the semantic rows (+ locus EmailIntake, ! fn Intake::on_mail gains publish, ! locus Support subscribes: +Tickets, ! claim iso: holds → violated),
and, because every observed process reports the model hash it was
built from, it says which live processes are still expressing the
old artifact: they are listed as stale and ringed on the canvas.
With a pair and no artifact argument, the after side is also the
law artifact.
An organism is a Hale binary. Iris inspects a DNA organism the
way it inspects any other program — hale dna run attaches it to
the organization’s process — and knows nothing of DNA: no record,
no Board, no status projection. DNA’s people surface is the face,
under dna/face, served by the head (see DNA).
hale iris inspect is the artifact-side half of that
comparison: point it at an artifact (and optionally a running
hale iris URL) and it reports drift, declared-but-silent
surface, and the law rows, without a browser.
How it ships
Section titled “How it ships”Iris is itself a Hale program (a fuse-hl consumer, a C attach
shim over the shared-memory segment, and a vanilla-JS renderer).
Rather than rewrite it in Rust, the hale binary carries its
sources the way it carries the standard library: embedded at
compile time, materialized on first use into
$XDG_CACHE_HOME/hale/iris/<toolchain-hash>/ (or ~/.cache/…)and built there with the very compiler you are running. The
hash covers the compiler version and its sources, the build
knobs set in the environment the build inherits (HALE_DEV, a
sanitizer, LOTUS_LTO: the ones a recording’s execution
identity names), and every embedded byte, so a compiler upgrade
rebuilds the observer once, an observer built under a sanitizer
is never handed to a run without one, and a second launch is
exec-only (hale iris --where prints the directory for the
current environment; delete it to force a rebuild). The observer is compiled by the same
codegen that compiled the observed program, so the wire layout
(crates/hale-codegen/runtime/obs_protocol.h, pinned by
_Static_asserts in the runtime and by a decoder test) can
never drift between the two halves of one installation.
The observer needs the same C toolchain hale build needs.
hale iris --build-only is the way to pay that cost ahead of
time (in a container image, say).
What iris does not do
Section titled “What iris does not do”It does not schedule, replay, or edit. Attaching is
read-only: the segment’s observer_count goes up, the emitting
process starts writing records instead of just counters, and
that is the whole footprint. Record a run with
LOTUS_OBS_RECORD=<path> and replay it later
under the same observer.