Log Operator Console (pm-log-ui)¶
Learning objectives
After reading this page you will understand:
- What the Log Operator Console is for, and the one question it is built to answer
- How it relates to
pm-log-srvandpm-log-cli, and why it replaces neither - Where LALF and LALF-PS fit — and why the console speaks only one of them
- Every way to start the application, and what each one needs
- What each of the six views shows and how to work with it
- The acknowledgement model, and why it is shared rather than per-browser
- How to configure, secure, and troubleshoot a deployment
About the figures on this page
Blocks marked 📷 Figure N are placeholders indicating where a UI
screenshot should be added, together with a short description of what to
capture and a suggested asset path under
docs/user-guide/images/log-gui/. Replace each block with the image and
an italic caption once the screenshots are taken.
Overview¶
The Log Operator Console (pm-log-ui) is a browser-based console over the
logging collected by pm-log-srv. Where pm-log-cli
answers "what did this process log?", the console answers a different and more
operational question:
Is anything wrong right now — and has someone dealt with it?
That second clause is the part no CLI provides. A diagnose report tells you
something is broken; it cannot tell you that a colleague already looked at it
twenty minutes ago and decided it was benign. The console adds a shared
acknowledgement layer on top of the same data, so a room full of operators
converges on one view of what is outstanding rather than each re-investigating
the same recurring error.
| Component | Role | Type |
|---|---|---|
pm-log-srv |
Collector — receives LALF from every pm-* process, appends to log.db, republishes live rows over LALF-PS |
Long-running process |
pm-log-cli |
Query/troubleshooting CLI — reads log.db directly |
One-shot CLI |
pm-log-ui bridge |
Backend — one LALF-PS subscription, read-only log.db access, WebSocket fan-out, acknowledgement store |
Long-running process |
pm-log-ui web |
Frontend — six views over the bridge's REST + WebSocket API | Browser application |
The console is strictly additive. It never writes to log.db, never
alters pm-log-srv's behaviour, and can be stopped at any time without
affecting log collection. Everything it displays remains available through
pm-log-cli whether the console is running or not.
flowchart LR
subgraph procs["Any pm-* process"]
ENGINE["pm-engine"]
GWY["pm-*-gwy"]
OTHER["...every other pm-* process"]
end
ENGINE -->|"LALF / TCP :5600"| LOGSRV["pm-log-srv"]
GWY -->|"LALF / TCP :5600"| LOGSRV
OTHER -->|"LALF / TCP :5600"| LOGSRV
LOGSRV -->|"appends"| DB[("log.db (SQLite)")]
LOGSRV -->|"LALF-PS PUB :5601"| BRIDGE["pm-log-ui bridge"]
BRIDGE -->|"LALF-PS PUSH :5602"| LOGSRV
DB -->|"read-only SQL"| BRIDGE
BRIDGE -->|"read-write"| ACK[("log-ui-acks.db")]
BRIDGE -->|"REST + WebSocket :8091"| WEB["Browser"]
CLI["pm-log-cli"] -->|"read-only SQL"| DB
BRIDGE -. "execFile --format json diagnose" .-> CLI
How it relates to the log server¶
The console is a consumer of pm-log-srv, and it consumes it through two
independent channels that fail independently — a distinction that matters
because the UI reports them separately rather than collapsing them into one
"connected" light.
| Live channel | History channel | |
|---|---|---|
| Source | LALF-PS (ZeroMQ) from pm-log-srv |
log.db read directly from disk |
| Carries | New rows as they land, server heartbeats | Search, aggregation, counts, export |
Works when pm-log-srv is stopped |
No | Yes |
Works when log.db is unreachable |
Yes | No |
| Latency | Pushed on commit | Query-time |
Because history comes from the file rather than the wire, the console
deliberately never sends log.backfill_request. Backfill exists in
LALF-PS for subscribers with no filesystem access to log.db; this bridge has
that access, so replaying history over ZeroMQ would be strictly worse — slower,
bounded by the server's backfill limits, and competing with live delivery.
Three consequences worth internalising before you deploy:
- Stop
pm-log-srvand the console still works for everything historical. The top bar shows log server down, the live tail stops, and search, aggregation and export continue unaffected. - Move
log.dbout from under the bridge and the live tail still works. The top bar shows log.db unavailable, and history views return a clean 503 rather than a blank page. - The bridge holds exactly one LALF-PS subscription no matter how many
browser tabs are open. Ten operators watching the console cost
pm-log-srvone subscriber, not ten. Fan-out to tabs happens over the bridge's own WebSocket.
The console is not a substitute for pm-log-cli
pm-log-cli remains the tool of record for scripting, for exporting large
ranges, for hosts with no browser, and for any situation where you need
the log data without a running service. The console is a viewing layer;
pm-log-cli is the access layer. The Diagnostics view makes this
explicit by shelling out to pm-log-cli rather than reimplementing it.
LALF, LALF-PS, and where the console sits¶
This is the single most common source of confusion, so it is worth stating plainly:
The Log Operator Console does not speak LALF.
| Protocol | Direction | Transport | Who speaks it |
|---|---|---|---|
| LALF | pm-* process → pm-log-srv |
TCP :5600, line-oriented text |
Every process that logs. Normative spec: LALF Protocol Reference |
| LALF-PS | pm-log-srv → subscriber |
ZeroMQ PUB :5601 / PULL :5602, JSON |
Live viewers, including this console. Reference: LALF-PS |
LALF is a producer protocol: a process opens one long-lived TCP connection,
does a HELLO/WELCOME handshake, and streams its own LOG records until it
exits. It is unidirectional and subscription-free — a LALF client can only
ever send its own logging, never read anyone else's.
That property is exactly why the console cannot use it. A viewer needs to
read everything, which is the opposite of what LALF offers. So pm-log-srv
exposes a second, separate interface — LALF-PS — and the console subscribes to
that.
Read Appendix: LALF Protocol Reference when you are
instrumenting a process to send its logs. Read it for this console only to
understand the shape of the rows that eventually arrive: the LOG header field
set in §"LOG" is what becomes a row in every table and detail pane described
below.
What the bridge does on the LALF-PS wire¶
The bridge implements the subscriber side in
web-apps/log-gui/apps/bridge/src/lalf-ps-uplink.ts. The sequence, in order:
- Connect
SUBto:5601andPUSHto:5602. - Subscribe to the ZeroMQ topic prefix
log.— notlog.<sub_id>. The server publishes per-subscriber topics aslog.<msgtype>.<sub_id>(plus the broadcastlog.server_state), so the shared prefix is the only filter that catches them all. The bridge then discards anything not addressed to its ownsub_id. - Wait 200 ms before sending anything. ZeroMQ's slow-joiner behaviour
silently drops messages published before a
SUBconnection finishes establishing; without this settle the firstsubscribe_ackcan vanish. - Send
log.subscribewithmode: STREAMand a lease. - Retry the subscribe if no ack arrives within one second. Subscribe is
idempotent by
sub_id, so a duplicate is harmless. - Renew at half the lease interval, using
renew_before_secfrom the ack when the server supplies one. - On
log.lease_expiredor alog.errorwithUNKNOWN_SUB, re-subscribe immediately rather than assuming the old lease survived.
The sub_id is pm-log-bridge-<pid> by default, so two bridges on one host do
not collide. It is shown in the Health view — quote it if you need to
correlate the console against pm-log-srv's own subscriber list.
The bridge tracks four uplink states, surfaced in the top bar:
| Uplink state | Top bar | Meaning |
|---|---|---|
CONNECTING |
reconnecting… | Subscribe sent, no ack yet |
ACTIVE |
log-srv up | Lease held, rows flowing |
RECONNECTING |
reconnecting… | Lease expired or unknown; re-subscribing |
SERVER_DOWN |
log server down | log.server_state said DOWN, or heartbeats stopped |
Running the application¶
Three supported ways, in increasing order of setup effort.
| Method | Best for | Needs | URL |
|---|---|---|---|
| Container stack | Most users; production | Podman/Docker + Make | http://localhost:8091 |
| Pre-built image artifact | Offline hosts, no repo clone | Podman/Docker only | http://localhost:8091 |
| Local development | Contributing to the console itself | Node.js + npm | http://127.0.0.1:5178 |
Prerequisites¶
Whichever method you choose, the console needs to reach both of its sources:
pm-log-srv's LALF-PS ports —5601(PUB) and5602(PULL) by default. Note these are not the LALF port5600; the console has no use for that one.- A filesystem path to
log.db— read access is sufficient, and read-only is preferred.
Any modern browser (Chrome/Edge ≥ 110, Firefox ≥ 115, Safari ≥ 16). No extensions are required.
Option 1 — One command with the container stack¶
The fastest path with a repository checkout and a container runtime. From the
web-apps/log-gui/ directory:
make up auto-detects Podman or Docker (preferring Podman), starts the Podman
machine on macOS if needed, builds the image, and starts the stack detached.
Open http://localhost:8091.
make down # stop and remove the stack
make restart # restart it
make logs # follow the container log
make ps # show stack status
make help # list every target
The compose file mounts two separate volumes, deliberately:
volumes:
# pm-log-srv's directory, mounted READ-ONLY
- ./logsrv-data:/app/logsrv-data:ro
# this project's own ack store, read-write
- ./ack-data:/app/ack-data
The :ro on the first mount is not decoration. "This project never writes to
log.db" is enforced by the kernel rather than by application code, so a bug
in the console cannot corrupt the collector's database.
Set LOG_SRV_HOST in docker-compose.yml to wherever pm-log-srv is
reachable from inside the container — host.docker.internal on Docker
Desktop, host.containers.internal on Podman, or a real hostname.
Option 2 — A pre-built image artifact¶
For a host with no repository clone, build a self-contained OCI archive:
This produces dist/edumatcher-log-gui-<version>.tar.gz containing the Node
runtime, the compiled frontend and the bridge — no external dependencies. On
the target host:
# Podman
podman load --input edumatcher-log-gui-<version>.tar.gz
podman run -d --name log-gui -p 8091:8091 \
-e LOG_SRV_HOST=logsrv.example \
-v /srv/logsrv-data:/app/logsrv-data:ro \
-v /srv/log-ui-acks:/app/ack-data \
edumatcher-log-gui:<version>
# Docker — identical, substituting `docker` for `podman`
No Node.js or npm required
The archive is fully self-contained. The host needs only Podman or Docker.
Option 3 — Local development¶
cd web-apps/log-gui
make install # npm ci from the lockfile
make dev # bridge (8091) + web dev server (5178) together
Open http://127.0.0.1:5178. The Vite dev server hot-reloads on save and
proxies both /api/* and /ws/* to the bridge, so you only ever open the
5178 URL.
For separate logs, run the two halves in two terminals:
This assumes pm-log-srv is running locally on its default ports and that
log.db exists. The bridge resolves log.db's location by mirroring
edumatcher.config._resolve_data_dir()'s priority order exactly —
EDUMATCHER_DATA_DIR, then <repo>/src/data, then the installed-package home
directory fallback — so in a normal checkout neither side needs configuring to
agree with the other. It logs the resolved absolute paths at startup; check
that line first if a view is unexpectedly empty.
A known npm dedup bug
If package-lock.json is ever deleted and regenerated, some npm versions
(observed on 11.17.0) crash with TypeError: Invalid Version: from
@npmcli/arborist's dedup step. make install already works around this
by running npm install --no-dedupe for the first resolution. If you hit
the crash by calling npm install directly, rerun with --no-dedupe.
See web-apps/log-gui/README.md for the full explanation.
Developer commands¶
make test # Vitest: log-query, fingerprinting, issue index, ack store
make typecheck # type-check every workspace
make build # production frontend bundle
make lint # type-check (lint alias)
make format # Prettier
make clean # remove build artifacts and dist/
A tour of the interface¶
The window is a fixed shell: a top bar carrying global state, a six-item navigation rail on the left, and the active view filling the rest.
┌────────────────────────────────────────────────────────────────────┐
│ pm-log-ui v0.1.0 ● log-srv up ⚠ 3 unacked ☾ │
├──────┬─────────────────────────────────────────────────────────────┤
│ ▣ Dash│ │
│ ▤ Logs│ │
│ ⚠ Ale.│ active view │
│ ◈ Proc│ │
│ ⚕ Diag│ │
│ ♥ Heal│ │
└──────┴─────────────────────────────────────────────────────────────┘
The top bar is deliberately sparse — three things, always visible:
- Source state dot — green log-srv up, yellow reconnecting…, red log server down or log.db unavailable. The two red states are distinct because they mean different things and have different fixes.
- Unacked count — a link to Alerts, styled red when non-zero. Also mirrored as a badge on the Alerts rail item.
- Theme toggle — light/dark. The icon shows the theme a click switches to, not the one currently active.
📷 Figure 1 — The main window
Screenshot placeholder. Capture the console in dark theme on the
Dashboard with at least one unacknowledged issue, so the red banner,
the top-bar unacked link and the Alerts rail badge are all visible
together. Suggested file: images/log-gui/fig-01-main-window.png.
The six views at a glance¶
| View | Answers | Refresh |
|---|---|---|
| Dashboard | Is anything wrong right now? | Live counters + 15–30 s polls |
| Logs | What exactly happened? | Live tail or on-demand query |
| Alerts | What is outstanding, and who has it? | 10 s |
| Processes | Is everything still reporting in? | 10 s |
| Diagnostics | What would pm-log-cli diagnose say? |
60 s |
| Health | Which component is degraded? | 10–30 s |
Dashboard¶
The at-a-glance view, designed to be readable from across a room.
Top of the view is an alert banner, shown only when unacknowledged issues exist. It pulses, names the most recent affected process, and links straight to Alerts. When nothing is outstanding the banner is absent entirely — the absence of red is the signal.
Below it, five meter tiles: total events, current error rate per minute, warnings in the last 60 s, process count, and log-server state. Then two charts — events over time (1-minute buckets over the last hour) and top processes by error count — and a compact list of the five most recent errors.
The error-rate tile is banded — it carries a colour and a word (normal,
elevated, high, SEVERE) rather than a bare number, because "18 errors a
minute" means nothing without knowing what this venue considers busy. The
boundaries are the ERROR_RATE_* thresholds.
The error rate and the 60-second warning count come from the live
WebSocket stream and update every second. Everything else is polled from
log.db. This is why the tiles can briefly disagree during a burst: the live
counters have already seen rows the aggregate query has not yet been re-run
for.
📷 Figure 2 — Dashboard with an active alert
Screenshot placeholder. Capture the full Dashboard during a period with
errors, so the pulsing banner, all five meter tiles with real numbers,
both charts populated, and the recent-errors list are visible.
Suggested file: images/log-gui/fig-02-dashboard.png.
Logs (the Explorer)¶
The workhorse view: a virtualised log table that runs in one of two modes, toggled by the ▶ LIVE / history button.
- LIVE streams from the WebSocket into an in-memory ring buffer of the most recent 2 000 rows, newest first. Rows flash briefly as they arrive.
- history queries
log.dbthrough the bridge and shows a match count.
The filter bar applies to both modes, and every filter is kept in the URL query string — so a filtered view is a link you can paste into a ticket or a chat message and a colleague opens exactly what you were looking at.
| Control | Effect |
|---|---|
message contains… |
Substring match on the message body |
| Level dropdown | Minimum severity (WARNING+, ERROR+, …) |
exceptions only |
Restrict to rows carrying a traceback |
| ▶ LIVE / history | Switch between the live buffer and log.db |
Scrolling down in LIVE mode pauses the tail. This is intentional — you cannot read a fast-moving log that keeps jumping. A banner appears showing how many rows arrived while paused; clicking it jumps back to the top and resumes. The rows are not lost, they simply were not rendered.
Delivery adapts to the rate
Under LIVE_BATCH_THRESHOLD_PER_SEC rows per second, each row is sent as
its own WebSocket frame, which is what makes the tail feel immediate. Above
it the bridge coalesces rows into batched frames flushed every 100 ms.
This is automatic in both directions and needs no operator action. It matters during a storm: at a few thousand rows a second, one frame per row is thousands of encodes, socket writes and React updates per second per tab, and the browser stops keeping up long before the bridge does. Ordering is preserved unconditionally — once anything is buffered, later rows queue behind it even if the rate drops back.
Clicking any row opens a detail pane on the right with the full LOG
header field set — logger, module and line, pid and host, session and sequence
number — plus the untruncated message. Two actions there:
- Copy — the message text to the clipboard.
- Filter to this process — rewrites the URL filter, which is usually the next thing you want after spotting one bad row.
📷 Figure 3 — The Explorer in live mode with a row selected
Screenshot placeholder. Capture the Logs view in LIVE mode with a level
filter applied, several severity-coloured rows visible (at least one
ERROR with the ⚠ exception marker), and the right-hand detail pane open
on a row with a traceback. Suggested file:
images/log-gui/fig-03-explorer.png.
Alerts¶
Where the console earns its keep. WARNING-and-above rows are fingerprinted
— normalised so that the same fault recurring a thousand times collapses into
one entry with a count, rather than a thousand lines to scroll past.
Each issue card carries the level, process and logger, the occurrence count, a sample message, and first/last-seen times. Three tabs filter by state: unacked, acked, all.
The acknowledgement model:
- Acknowledge marks the issue handled, attributed to the name in the operator field, with an optional free-text note recording what was done.
- Un-ack reverses it.
recurred since ackis flagged prominently when a fingerprint fires again after being acknowledged. This is the case that matters most: it means the fix did not hold, and it is visually distinct from a fresh issue.
Acknowledgements are shared, not per-browser
Acks live in the bridge's own SQLite file, so every operator sees the same
state. That is the entire point — it is how the console prevents two
people investigating the same error in parallel. It also means an ack is
a statement to your colleagues, not a personal "hide this" toggle.
Set your name in the operator field before acknowledging anything;
unnamed acks are attributed to unknown and are much less useful during
a post-mortem.
Issues with no activity beyond the retention window (7 days by default) are
pruned hourly. The index is rebuilt from log.db at startup, so restarting the
bridge loses nothing except issues that had already aged out.
📷 Figure 4 — Alerts with a recurred-since-ack issue
Screenshot placeholder. Capture the Alerts view on the all tab
showing a mix: one unacknowledged CRITICAL, one acknowledged issue with
its "✓ acked … by … — note" line, and ideally one carrying the
recurred since ack chip. Suggested file:
images/log-gui/fig-04-alerts.png.
Processes¶
A registry of every process that has ever connected, with a status glyph:
| Glyph | Meaning |
|---|---|
● |
Connected and logging |
⚠ |
Connected but silent for PROCESS_SILENCE_SEC or more (30 s by default) |
○ |
Disconnected |
The ⚠ state is the one to watch. A process that connected and then went
quiet is not obviously broken from any single log line — there is nothing to
see, which is precisely the problem. Alongside each row: pid, host, connect
time, idle time, total log count and error count.
The threshold is PROCESS_SILENCE_SEC (30 s by default) and is stated in the
legend above the table, so the glyph is never ambiguous.
Clicking any row navigates to the Explorer pre-filtered to that process.
📷 Figure 5 — The Processes board
Screenshot placeholder. Capture the table with a mix of all three status
glyphs — at least one live process, one silent (⚠), and one
disconnected (○) — with non-zero error counts on at least one row.
Suggested file: images/log-gui/fig-05-processes.png.
Diagnostics¶
Surfaces pm-log-cli's rule-based heuristics on a 60-second schedule, with a
manual Re-run button.
Each finding shows the heuristic name, what was found, a recommendation, and —
importantly — the exact pm-log-cli command to reproduce it. Heuristics
that found nothing are listed compactly at the bottom, because "we checked and
it was fine" is information too.
This view calls pm-log-cli, it does not reimplement it
GET /api/diagnostics shells out to pm-log-cli --format json diagnose
so there is exactly one implementation of the heuristics to keep correct.
The consequence: pm-log-cli must be installed and on PATH wherever
the bridge runs — which, in the container deployment, it is not by
default. If it is missing, this one view reports it and every other view
keeps working. Override the command with LOG_CLI_COMMAND, e.g.
LOG_CLI_COMMAND="poetry run pm-log-cli".
See diagnose for
what each heuristic actually checks.
📷 Figure 6 — Diagnostics with findings
Screenshot placeholder. Capture the view with at least two findings of
differing severity (one error-bordered, one warning-bordered), each
showing its recommendation and repro command, plus the "✓ No findings
for:" line. Suggested file: images/log-gui/fig-06-diagnostics.png.
Health¶
Four panels for four components that fail independently, kept separate rather than merged into a single status light:
| Panel | Shows |
|---|---|
pm-log-srv |
Up/unknown, server name, lifetime events, connections, truncations, errors sent |
| LALF-PS | State, subscriber count, active backfills, last sequence, inbox drops, default lease |
log.db |
Path, size, row count, oldest retained row |
| Bridge | WebSocket client count, fingerprints indexed, acks stored, sub_id, log.db reachability |
Start here when something looks wrong but you do not yet know what. The
resolved log.db path in particular answers the most common
misconfiguration — a bridge reading a different database than the one
pm-log-srv is writing.
inbox dropped is worth a periodic glance: a rising count means pm-log-srv
is shedding inbound LALF-PS messages, which is a capacity signal rather than a
console problem.
📷 Figure 7 — The Health view
Screenshot placeholder. Capture all four panels populated against a
running server, with the resolved log.db path and the bridge sub_id
legible. Suggested file: images/log-gui/fig-07-health.png.
Exporting¶
GET /api/logs/export returns the current filter's matches as CSV (default)
or JSON:
curl -o logs.csv \
'http://localhost:8091/api/logs/export?minLevel=ERROR&processes=pm-engine&format=csv'
The CSV carries seq,client_ts,level,process,logger,message. Export is capped
at EXPORT_MAX_ROWS (1 000 000 by default), and the whole result set is
materialised in memory before it is sent — a full-cap export is a large
allocation in the bridge, so prefer a narrowed filter.
If the cap is reached the response carries X-Export-Truncated: true and
X-Export-Max-Rows, and the bridge logs a warning. Check that header before
treating an export as complete: a silently short file is the failure mode that
matters here, because nothing about the CSV itself reveals that rows are
missing. For anything larger — or for scripted extraction —
use pm-log-cli query --format json
instead; it reads the same database with no HTTP layer in between.
Configuration reference¶
All bridge configuration is environment variables.
Network and serving¶
| Variable | Default | Purpose |
|---|---|---|
HOST |
127.0.0.1 |
Bind address. The container sets 0.0.0.0. |
PORT |
8091 |
HTTP/WebSocket port |
CORS_ORIGIN |
* |
Allowed origin. Restrict this when exposed beyond localhost. |
STATIC_DIR |
(unset) | Serve the built frontend from here (single-container mode) |
LOG_LEVEL |
info |
The bridge's own logging verbosity |
Sources¶
| Variable | Default | Purpose |
|---|---|---|
LOG_SRV_HOST |
127.0.0.1 |
Where pm-log-srv is reachable |
LOG_SRV_PUB_PORT |
5601 |
LALF-PS PUB port |
LOG_SRV_PULL_PORT |
5602 |
LALF-PS PULL port |
SUB_ID_PREFIX |
pm-log-bridge |
Subscriber id prefix; -<pid> is appended |
LEASE_SEC |
30 |
Subscription lease; renewed at half this |
LOG_DB_PATH |
resolved data dir /log.db |
The collector's database, read-only |
ACK_STORE_PATH |
resolved data dir /log-ui-acks.db |
This project's own store, read-write |
LOG_CLI_COMMAND |
pm-log-cli |
Command backing the Diagnostics view |
Issues¶
| Variable | Default | Purpose |
|---|---|---|
ISSUES_RETENTION_DAYS |
7 |
How long an inactive issue is kept, and how far back the startup scan reaches |
ISSUES_MIN_LEVEL |
WARNING |
Minimum level that becomes an issue — applied identically to the startup scan and to live rows |
ISSUES_ALERT_LEVEL |
ERROR |
Minimum issue level counted as "unacked" in the top bar and the Alerts badge |
Both level variables are validated against the real log-level enum, so a typo
yields the default rather than a silently broken filter. Setting
ISSUES_ALERT_LEVEL=WARNING makes the top-bar count include warnings; setting
ISSUES_MIN_LEVEL=ERROR stops warnings being fingerprinted at all, which also
empties them from Alerts.
The two levels are independent, and order matters
ISSUES_MIN_LEVEL decides what is tracked; ISSUES_ALERT_LEVEL decides
what is counted as demanding attention. Setting ISSUES_ALERT_LEVEL
below ISSUES_MIN_LEVEL is legal but pointless — nothing below the
tracking floor exists to be counted.
Thresholds¶
| Variable | Default | Purpose |
|---|---|---|
PROCESS_SILENCE_SEC |
30 |
Seconds of silence after which a connected process is flagged ⚠ on the Processes board |
ERROR_RATE_NORMAL_PER_MIN |
5 |
Error rate at which the Dashboard meter leaves normal |
ERROR_RATE_ELEVATED_PER_MIN |
20 |
…and enters high |
ERROR_RATE_SEVERE_PER_MIN |
100 |
…and enters severe |
Three thresholds, four bands. Each is the rate at which the next band begins, so they read as lower bounds:
| Errors/min | Band | Meter colour |
|---|---|---|
< 5 |
normal |
default |
5 – 19 |
elevated |
warning |
20 – 99 |
high |
error |
>= 100 |
severe |
critical |
The classification lives in @edumatcher/log-types (classifyErrorRate) so
the bridge and the browser cannot drift apart on where a band starts.
Limits¶
| Variable | Default | Purpose |
|---|---|---|
QUERY_MAX_ROWS |
5000 |
Hard cap on any single history query |
EXPORT_MAX_ROWS |
1000000 |
Hard cap on an export |
LIVE_BATCH_THRESHOLD_PER_SEC |
50 |
Rows/sec above which live delivery switches from per-row frames to coalesced batches |
Invalid numeric values fall back to the default rather than failing startup.
How the frontend learns these¶
GET /api/ui-config returns alertLevel, issuesMinLevel,
processSilenceSec and the three error-rate thresholds. The browser fetches it
once and holds it for the session — it has no local defaults, deliberately,
because a fallback constant in the UI is exactly how these settings came to be
ignored in the first place. Views render a neutral placeholder until the
config arrives rather than guessing.
One visible consequence: for the first moment after a page load the Processes
board shows no ⚠ glyphs, because flagging every healthy process as silent
would be worse than a fraction of a second's delay.
Security notes¶
There is no authentication
pm-log-ui has no login, no user accounts, and no authorisation.
Anyone who can reach the port can read every log line the exchange has
produced — which routinely includes internal hostnames, gateway
identifiers, order ids and tracebacks. Treat the console as an internal
operations tool:
- Bind to
127.0.0.1(the default) or an internal interface only. - Put it behind an authenticating reverse proxy if it must be reachable beyond the host.
- Set
CORS_ORIGINto your specific site origin rather than leaving*. - Never expose port
8091to an untrusted network.
Acknowledgement attribution is a convention, not an identity claim — the operator name is free text typed by whoever is at the keyboard. It is there to help a post-mortem reconstruct who looked at what, not to prove it.
Beyond that the posture is deliberately narrow: Helmet security headers are
applied, the only writable resource is the ack store, and log.db is opened
read-only — enforced by the :ro mount in the container deployment.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| Top bar shows "log server down", history works | pm-log-srv is stopped, unreachable, or its heartbeats stopped |
Start the server; check LOG_SRV_HOST and that 5601/5602 are reachable from the bridge (inside the container, if containerised). |
| Top bar shows "log.db unavailable", live tail works | The bridge cannot open the database file | Compare the path in Health → log.db with where pm-log-srv writes. Set LOG_DB_PATH explicitly. |
| Every view is empty, no error shown | Bridge is reading a different, valid log.db |
Check the resolved-paths line the bridge logs at startup — a valid but empty database looks exactly like a working one. |
| Live tail stuck, history fine | Lease not being renewed; slow-joiner drop | Check Health → LALF-PS state and sub_id. Restart the bridge; the subscribe path retries automatically but a wedged socket does not recover. |
| Live tail stopped after scrolling | Expected — scrolling pauses the tail | Click the "n new rows — jump to top" banner. |
| "Diagnostics unavailable" | pm-log-cli not on PATH where the bridge runs |
Expected in the default container. Set LOG_CLI_COMMAND, or run the bridge where the CLI is installed. Every other view is unaffected. |
| Acks vanish after a restart | ACK_STORE_PATH points at ephemeral storage |
Mount a persistent volume for the ack store — it is the only state the console owns. |
| Acks are not shared between operators | Each is running their own bridge | Run one bridge and have everyone open the same URL. Acks live in that bridge's store. |
make up fails: neither podman nor docker found |
No container runtime | Install one, or use local development. |
make up on macOS: podman machine not running |
The Podman VM is not started | make up starts it automatically; otherwise podman machine init && podman machine start. |
Container cannot reach pm-log-srv on localhost |
localhost inside a container is the container |
Use host.docker.internal (Docker Desktop) or host.containers.internal (Podman), or a real hostname. |
npm install crashes: TypeError: Invalid Version: |
npm dedup bug | Use make install, or rerun with --no-dedupe. |
Port 8091 or 5178 already in use |
Another process holds it | Change PORT, or the server.port in web-apps/log-gui/apps/web/vite.config.ts. |
| Blank page in production, API responds | Frontend not built or STATIC_DIR wrong |
make build, then point STATIC_DIR at an absolute path to apps/web/dist. |
| A client route 404s in production | Static host with no SPA fallback | Let the bridge serve the UI via STATIC_DIR; it falls back to index.html. |
| Explorer shows fewer rows than expected | QUERY_MAX_ROWS cap (5 000) |
Narrow the filter, use a time range, or export instead. |
| An export looks complete but is short | EXPORT_MAX_ROWS cap reached |
Check for X-Export-Truncated: true on the response. Narrow the filter, raise the cap, or use pm-log-cli query. |
| Top-bar unacked count differs from the Alerts list | Expected — the badge counts ISSUES_ALERT_LEVEL and above; Alerts lists WARNING and above |
Set ISSUES_ALERT_LEVEL=WARNING if you want them to agree. |
| Warnings never appear in Alerts | ISSUES_MIN_LEVEL raised above WARNING |
Lower it. Note it gates tracking, so raising it also empties existing warnings on restart. |
| Numbers on the Dashboard briefly disagree | Live counters update at 1 Hz; aggregates poll at 15–30 s | Expected. Health and the Explorer are authoritative. |
Keeping the console in sync with pm-log-srv¶
Three files re-implement pieces of pm-log-srv's Python surface in
TypeScript. Each carries a maintenance comment naming its counterpart, and all
three must be revisited when the server's schema, LALF-PS message set, or query
shape changes:
| TypeScript | Mirrors |
|---|---|
packages/log-query/src/filter-to-sql.ts |
edumatcher.log_srv.pubsub.LogFilter.sql_where() / edumatcher.log_cli.queries.query_events() |
apps/bridge/src/lalf-ps-uplink.ts |
The LALF-PS wire messages implemented in edumatcher.log_srv.pubsub |
apps/bridge/src/config.ts |
edumatcher.config._resolve_data_dir()'s priority order |
apps/bridge/src/fingerprint.ts has no Python counterpart — issue
fingerprinting is new surface this project introduces. Run make test after
any of these changes.
See Also¶
- Centralized Log Server —
pm-log-srvandpm-log-cli, the data this console displays - Appendix: LALF Protocol Reference — normative wire spec for the producer protocol
- Message Reference — LALF-PS messages — normative field tables for every message the bridge sends and receives
- Configuration — Configuring pm-log-srv — the
log_server:block, including the LALF-PS ports - Processes — where
pm-log-srvsits among the other processes - Configuration GUI (
config-gui) — the sibling browser application, same deployment pattern - Trader Information Terminal (TapeDeck) — the other operator-facing UI