Monitoring and Troubleshooting
Diagnose from the outside in. A process can be alive while its database, Redis, worker, or downstream service is unavailable.
flowchart TD
A[User symptom] --> B{Public /healthcheck/readiness}
B -->|Unavailable| C{Public /healthcheck liveness}
C -->|Down| D[Proxy, container, process]
C -->|Up| E[Database, Redis, storage, service, or worker]
B -->|Ready| F{Client sync health}
F -->|Failed| G[Session, cursor, conflict, realtime]
F -->|Healthy| H[Feature gate or data-specific path]
Health layers
| Layer | Check | What it proves |
|---|---|---|
| Public readiness | GET /healthcheck/readiness |
Every required service, dependency, storage path, and worker is ready |
| Public liveness | GET /healthcheck |
The gateway process and public route respond; not safe for acceptance |
| Service liveness | Internal /healthcheck |
The individual process/event loop responds |
| Service readiness | Internal /healthcheck/readiness |
The service can reach its required database, Redis, or storage |
| Stack state | srn-server status / docker compose ps |
Container lifecycle and Docker health |
| Server aggregate | Admin Server tab or srn-admin status |
Per-sibling readiness and response time |
| Client data path | Manual sync and a second client | Authentication, encrypted sync, reconciliation, and local persistence |
| MCP | standard_red_notes_status |
Bridge sign-in and background-sync health |
The public access-key gate exempts health paths so infrastructure probes remain usable. Docker and LXC acceptance use aggregate readiness; keep liveness for diagnosis only. Even successful readiness does not authenticate a user or prove an end-to-end encrypted client sync.
Files readiness checks read/write access and available filesystem blocks for
local storage. S3 storage uses the authenticated, non-mutating HeadBucket
probe; its credential must grant s3:ListBucket, which is also required by the
existing file-list/quota path.
First-response sequence
- Record the exact time, user, client version, server URL, and action.
- Preserve the error text and request/correlation identifiers.
- Check public health.
- Check aggregate/internal readiness.
- Inspect bounded logs around the recorded time.
- Query admin/security audit events for relevant configuration changes.
- Reproduce with a non-destructive read or test account.
- Back up affected state before repair.
Avoid restarts until evidence is captured. A restart can remove the failure signal and complicate a partially applied write.
Logs
Use bounded queries first:
srn-server logs server --tail 200
docker compose logs --timestamps --since 15m server
docker compose exec server srn-admin logs --service auth --level error --tail 200
docker compose exec server srn-admin logs --service api-gateway --search ws-sync --since 2026-10-14T20:58:00Z --tail 200
docker compose exec server srn-admin logs --service auth --tail 200 --order asc
docker compose exec server srn-admin audit --limit 100
In Preferences → Admin → Logs, use the service, level, time window and literal text filters to narrow an incident. Newest first is the default; oldest first presents the retained results chronologically. Pause the 15-second automatic refresh when reading or exporting a result. Exports use the displayed order and filters. The API and CLI share a bounded reader: at most 512 KiB from each current log file and 500 returned entries, rather than an unbounded search of all historical or rotated files.
Service logger entries and startup dependency checks record UTC timestamps.
Legacy lines without a reliable timestamp remain explicitly unavailable;
their event time is not reconstructed from a file modification time. Startup
checks name the dependency, report waiting at startup and every 30 seconds,
and record when its listener becomes ready instead of printing every raw
nc failure. The first wait is informational; a wait lasting 30 seconds is
reported at warning level.
info is a verbosity threshold, not a request to invent events that the code
does not emit. Sync failures now report the operation, failed phase, outcome
and elapsed milliseconds. These are bounded operational fields; command
payloads, credentials and account identifiers are excluded. Socket metric
summaries still aggregate successful traffic instead of writing a line for
each keystroke. Failure context helps identify a stuck operation; it does
not itself repair the underlying socket failure.
In Preferences → Admin → Diagnostics, run all checks, an individual check or the checks belonging to a section. Account and browser sections refresh their readings separately. Cancel stops the remaining checks and ignores late results; supported requests are aborted. A sync or other server action already dispatched may still complete, and cancelling does not roll it back. Temporary file-transfer diagnostics still perform their cleanup.
Worker programs do not expose dedicated health ports. Inspect their logs for event backlog, retry, mail, backup, or scheduler failures.
Never paste unredacted logs containing tokens, email addresses, IP addresses, provider responses, or request bodies into a public issue.
Safe operational logging
Security-sensitive server auth, gateway, WebSocket, sync, event, and worker paths—and the migrated app API, encryption, mobile, SNJS, and utility packages—emit allowlisted diagnostics rather than raw request, response, error, or payload objects.
| Kept for diagnosis | Removed or bounded |
|---|---|
| Event/action name | Access, refresh, offline, feature, subscription, and WebSocket tokens |
| HTTP method, status, and safe error code/type | Authorization and cookie headers |
| URL origin and path | URL user info, query values, and fragments |
| Query-parameter count | Email addresses, passwords, PKCE values, API keys, and session identifiers |
| Explicit user, request, or ephemeral connection ID | Request/response bodies, provider payloads, encrypted content, and exception messages |
The sanitizer is defensive against nested and circular objects, accessors,
hostile proxies, oversized strings, and oversized collections. It does not
invoke getters while preparing a log entry. Internal subscription validation
also carries its credential in x-subscription-token; the token is not placed
in the request path.
Client-safe 4xx responses retain their established status, content type, and allowlisted error tag. Thrown or untrusted 5xx failures return a stable generic service error rather than reflecting an upstream body, header, or exception message.
Before merging changes to these surfaces, run the source regression gate. The
contracts CI lane runs it on every push as the yarn ci:safe-logging leg of
yarn ci:contracts, and its tests with yarn test:ci-tools:
node scripts/validate-safe-logging.mjs
node scripts/validate-safe-logging.mjs --report-allowlist
node --test scripts/validate-safe-logging.test.mjs
The gate scans authored runtime JavaScript and TypeScript across CLI, MCP,
OpenClaw, server packages, and these exact app package roots: api,
encryption, mobile, snjs, and utils. Its machine-readable scope is
guardedRuntimeRoots in the validator. It complements runtime tests by
rejecting raw-token, raw-object, session-in-log, credential-in-path,
native-bridge-message, and crypto-error logging patterns. Allowlist entries are
exact and stale-checked: changing or removing an intentional match without
updating the review record fails the gate.
A call through optional chaining (this.logger?.error(…)) is a log call like
any other. A helper that takes the log message as a parameter and hands it on
is declared in messageForwardingWrappers, and the gate then checks each
caller’s message instead: it must be a literal, or a choice between two
literals. Run from a git work tree, the gate checks the files git tracks; an
untracked scratch file in a shared working tree is not part of the repository
it guards.
Other app package roots are not claimed by this gate yet. Their coverage must be enabled atomically with the corresponding consumer migrations and tests.
The reviewed-residual list holds three entries. Two are realtime gateway refusal
lines whose message is built only from closed sets of constants (a
SocketRejectionCode, SyncUnavailabilityReason values, and event names the
gateway itself passes). The third is the syncing-server line that names an item
withheld from integrity reporting together with the column value that made it
unmappable: it reads only identifier, content-type and timestamp columns, and
that value is what the operator must repair. --report-allowlist prints each
with its reason.
WebSocket bridges record originExcluded as structured boolean metadata, never
the underlying session UUID, so those diagnostics need no exception.
Realtime health in the admin panel
Settings → Admin → Diagnostics reads the same health snapshot that
/healthcheck/readiness reports, but splits it into rows an operator can act on.
Everything in its Realtime health section is informational. Readiness is
deliberately not gated on any of it, because a container that restarts itself on
a Redis blip turns ten seconds of degradation into an outage. The panel’s job is
to make a degradation visible, not to act on it.
The verdict and the checks
The pane opens on one headline, and every section, row and finding under it uses the same set of statuses:
| Status | Means |
|---|---|
| OK | It was checked and it works. |
| Degraded | It works, with something lost. The first entry under What needs attention says what. |
| Not working | It does not work. |
| Unknown | A reading that should have arrived did not. Not a pass: it keeps the headline off OK. |
| Not tested | A check settles it and that check has not been run. The row says which one. |
| N/A | Nothing to judge here, such as a setting this topology never reads. |
| Info | Context. It carries no verdict and never changes the headline. |
The headline reads OK only when nothing under it is not working, degraded, unknown or untested. A healthy deployment therefore opens on Not tested and reaches OK after Run all checks, because two of the checks write something and are never run for you.
Opening the pane writes nothing. The read-only checks run when it opens: the capability descriptor, an authenticated control-plane read (reported with the lane that carried it and how long it took), one ordinary sync (reported with the lane that carried it), a revision-history read, and a comparison of the web app’s build with the API server’s. Two checks run only from Run all checks or from the Checks tab, and the pane says what they do before they do it:
- Socket ticket mints one short-lived, single-use socket ticket. It is never redeemed and expires on its own.
- File transfer round trip uploads one throwaway 16 KiB object of random bytes, reads it back in ranged requests and, where this tab’s socket negotiated the file lane, over the socket as well, compares every byte, and deletes it. No file item is created, so nothing appears in your files and none of your own files is read, changed or removed. The delete is attempted even when an earlier step fails, and a delete that fails is reported.
Refresh re-reads everything the pane is told and re-runs the read-only checks. It writes nothing.
Some of what the pane reports is a fact about the tab or the browser it is open in, which the server cannot know:
- A stale tab. The web bundle carries the commit it was built from, and the pane compares it with the build the server serves now. A tab left open across a deployment reads Degraded until it is reloaded.
- Web app and server from different builds. The two images are built separately and nothing forces them to match.
- Content-Security-Policy refusals. Counted per directive since the page loaded. The blocked address is never recorded; the browser console has it.
- Which tab holds the socket. One tab per account holds the realtime socket and the others stand by on HTTP by design. A second tab says so and still reads OK.
- Reduced motion, as the browser reports it, so “the panel does not animate” is not mistaken for a fault.
The Copyable report opens with the same summary and carries every row with its status. It holds variable names, booleans, closed status codes, counts, durations and build revisions, and no URL, host, port, token or key.
Boot gate versus attach outcome
The Boot gate section shows separate verdicts rather than one combined
answer: whether the socket transport came up, whether SYNC_ITEMS was advertised
or withheld, whether ticket minting is answering right now, and a Gateway
chip reading Attached or Not attached. The lane and SYNC_ITEMS are split
because they became two decisions — a durable-backend condition withholds
SYNC_ITEMS without taking the socket down — and one combined verdict would
either hide a live lane or hide a missing operation.
The gateway chip is not a restatement of the lane verdict. The gate records a
decision to build the lane; the composition root records the outcome of
attaching a gateway. They come from different places and can disagree. An invalid
WEBSOCKET_REDIS_NAMESPACE produces exactly that disagreement: the gate passes,
the host then declines to attach rather than publish on a sibling stack’s
channels, and tickets mint while nothing is ever delivered. Showing only the
decision is how this panel once reported a working lane over a gateway that was
never there.
The six realtime health rows
When a gateway is attached, the panel renders the gateway’s own view of itself as six rows:
| Row | Reads | What a bad value means |
|---|---|---|
| Gateway | attached / not attached | Unattached means nothing reaches a client over a socket, whatever the boot gate decided. |
| Push bridge | redis, in-process, or none, with readiness |
Both redis and in-process count as bound and healthy; a single process that holds every socket needs no Redis to deliver a push. Only none is a fault, and it has two causes: a process asked for a Redis-backed plane with no reachable host, or a server build older than the in-process plane, which needs an upgrade rather than a setting. Bound but not ready is a reconnect window that recovers on its own. |
| Queue consumer | running / not running | Expected to be idle where pushes arrive through the bridge alone; on a stack that provisions the websocket queue it means those events are not being drained. |
| Collaboration relay | healthy / unhealthy | Unhealthy still leaves collaboration working between clients on the same replica, which is why it fails quietly on a multi-replica deployment. |
| Sync lane | up / down | Down means the gateway would refuse a client on /sockets/sync right now; the live refusal reasons above the rows say why. |
| Pushes dispatched | a count since this gateway attached | It resets on every restart. A count that stays at zero on a busy deployment is the signature of a delivery path that never fires. |
When the server reports no realtime snapshot at all the panel prints no rows and says so without guessing between the two causes: either no gateway is attached to the process that answered, or the build predates the snapshot. The boot-gate section distinguishes them.
Two findings the rows cannot state on their own
Two combinations are raised as explicit findings because every other panel on the screen would look healthy:
- The lane is enabled but no attached gateway was recorded. The gate built the lane and the host recorded no successful attach, so tickets mint while nothing is delivered. On a server older than the attach-outcome record the field is simply never set, and the line then means only that it was not reported.
- The socket is attached with no push bridge. The lane accepts clients, but
nothing carries server-side change notifications to them, so a save on one
device never reaches another until that device syncs on its own. This is a
misconfiguration rather than a topology: a process that was asked for a
Redis-backed plane without a reachable Redis host. A deployment that simply has
no Redis reports an in-process bridge instead and is healthy, and a
multi-container one reports
redis; onlynoneis a fault. The other way to reach it is a server build older than the in-process plane, which an upgrade fixes rather than any setting.
Symptom guide
Cannot sign in
- Confirm the exact server URL and shared access key.
- Check auth readiness and database/Redis.
- Distinguish wrong password, MFA challenge, account lockout, ban, suspension, unconfirmed email, and registration policy.
- Review pending push-MFA approvals and trusted devices.
- Do not reset MFA as a generic password-recovery action.
Sync is stale
- Confirm the client is signed in and sync is not still running.
- Check syncing-server readiness.
- Disable network filters temporarily in a controlled test.
- Manual sync; then verify on a second client.
- Check for conflict copies and repeated cursor/session errors.
- Realtime may be disabled while manual sync remains functional.
Realtime updates are missing
- Confirm ordinary sync works first.
- Read
/healthcheck/readiness. Itsgateway.realtimeblock says whether the gateway is attached, which push bridge it uses, whether that bridge and the SQS consumer are running, whether the collaboration relay is healthy, and how many pushes it has dispatched. It is informational and never fails the healthcheck, so a greenstatuswith a degraded realtime block is expected. - Read
/v1/sockets/sync/capabilities. An empty list means the socket transport is closed; the three causes are a missingWEB_SOCKET_CONNECTION_TOKEN_SECRET,WEBSOCKET_SYNC_ENABLED=false, and unreachable Redis. An unbound gRPC command port withholds only theSYNC_ITEMSoperation and leaves the list populated. - Read the gateway’s own log lines. The realtime gateway runs in-process inside
the api-gateway, and supervisord writes that program’s output to
/var/lib/server/logs/api-gateway.log(with.erralongside), sodocker compose logs servershows only supervisord bookkeeping. Boot lines to look for name the unmet preconditions and theSYNC_ITEMSdecision. - Check token issuance, reverse-proxy upgrade headers, and client connection
logs. There is no separate gateway container and no gateway
/health: through the front door,/healthis the web app’s own static response. - Confirm the user’s Live sync (
LIVE_SYNC_ENABLED) setting. A user with it off is refusedSYNC_ITEMSwith the codeLIVE_SYNC_DISABLED. - Do not treat WebSocket failure as item loss until manual sync is tested.
Files fail but notes sync
- Check the files service readiness and encrypted blob storage.
- Confirm upload-size and user storage limits.
- Inspect the file metadata item separately from the blob.
- Test a small file and a known existing file.
Admin console returns 403
- Confirm the user’s effective
ADMIN_USERrole. - Sign out and back in after a recent role grant.
- Check the server route and cross-service token, not just client visibility.
- Use
srn-admin user <account>for independent evidence.
MCP responds but data is old
Call standard_red_notes_status. Three or more consecutive sync failures mark
the signed-in bridge unhealthy. Check the local data directory, server URL,
token revocation, and lastSyncError, then restart only after preserving the
error.
Email is queued, late, or not delivered.
- First identify the topology. The full Redis-backed deployment has relay, queue, and log controls. The single/home in-memory deployment uses direct SMTP compatibility, and Redis Cluster deliberately does the same because a node-local AOF acknowledgement cannot be proven.
- In Settings → Admin → Server → Email delivery, confirm at least one
valid profile is enabled, save, wait up to five seconds for readiness to
refresh, and run Send test.
501means the advanced capability is not present in this topology;503means it is present but temporarily unavailable. - Use Refresh queue to inspect
ready,leased, anddeadrecords. Retry a dead or otherwise eligible record only after correcting the relay problem. A leased record is in flight and cannot be retried or discarded. - If reminder delivery was disabled, published-reminder jobs are settled before the relay boundary. The account opt-out endpoint remains available while gates are off and erases stored publication history/destination after cancellation. An in-flight refusal is intentional: retry opt-out after the bounded provider call finishes rather than claiming that a provider request was revoked.
- Use Refresh logs to filter attempt metadata by relay and outcome. Compare
rate-limited,transient-failure, andpermanent-failureresults with relay order, fallback policy, and each profile’smaxperwindowsetting. Logs and queue views intentionally omit recipients, subjects, bodies, attachments, credentials, and raw provider responses. - Check the gateway’s redacted
EmailDeliveryReadiness,EmailDeliveryRedisCapacity, worker-batch, and queue-producer diagnostics. Do not enable payload logging during a test. A missing readiness marker means the worker is stopped, no valid enabled relay exists, Redis capacity is below its safety floor, or the configuration could not be decrypted. - Verify Redis AOF is enabled and that the instance supports local
WAITAOF. The supplied Compose deployment defaults toappendfsync everysec; each new job is nevertheless reported as accepted only after its explicit localWAITAOFacknowledgement. The default encrypted-byte caps are 25 MiB per job and 64 MiB total; Redis also needs at least 64 MiB of additional headroom. - After a server-key rotation, old relay settings and queue payloads cannot be
decrypted. Restore the matching protected settings, Redis state, and key as one
recovery set. The admin UI intentionally refuses to overwrite an envelope it
cannot authenticate; re-entering credentials is therefore not a safe recovery
shortcut. For an intentional rotation, drain and verify the queue first, retain
the old recovery set, export the operator-known relay values, save an empty
profile list while the old key can still authenticate it, verify the envelope
is absent, the non-secret
relayConfigurationManagedmarker istrue, and readiness expires; then rotate the key and recreate/test the relays. An explicitly empty managed configuration does not resurrect legacy environment SMTP.
Delivery is at least once. If a provider accepted a message but its response was lost, retry can produce a duplicate even though deterministic job identifiers prevent duplicate queue insertion. Treat an ambiguous timeout as an unknown provider outcome, not proof that nothing was sent.
Backups are missing
- Confirm the server master switch and per-user settings.
- Check scheduler/worker logs.
- Validate the selected email relay or WebDAV connectivity without exposing credentials.
- Check the destination’s retention and quota.
- Run a restore drill rather than relying on a successful upload message.
Safe service recovery
Restart the narrowest failed component. Afterward:
- wait for readiness;
- verify the public health endpoint;
- sign in with a test account;
- synchronize a test note on two clients;
- upload and download a small file;
- inspect worker logs; and
- confirm the incident symptom is resolved.
If database or file integrity is in doubt, stop writes and follow Backups and Recovery instead of repeatedly restarting.
Escalation bundle
Prepare:
- deployment profile and component versions;
- sanitized Compose configuration;
- public and internal health results;
- bounded, redacted service and worker logs;
- relevant audit events;
- reproduction steps and affected/non-affected data paths;
- last known good time; and
- backup/restore status.
This evidence distinguishes an edge, dependency, authorization, sync, feature-gate, and data-integrity problem before code changes begin.