You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: buffer connection logs and flush them on failure (#1100)
Implements RFC requirement 13 / DEVEX-669: buffer connection debug logs
in memory below the current log level and flush them on a genuine
connection failure, so a support bundle captures the detail leading up
to the failure without the user having enabled debug logging beforehand.
## What this does
- Adds a `BufferingLogger` decorator (`src/logging/logBuffer.ts`) that
wraps the "Coder" output channel and keeps a bounded, in-memory ring of
the entries that sit **below** the channel's current level, which it
would otherwise drop. Only below-level entries are buffered, so nothing
already written is duplicated. Entries are formatted (via
`safeStringify`) at record time.
- On a connection failure, `flush(reason)` re-emits the captured entries
into the output channel at the least-verbose level the channel still
persists. The first physical line of each entry carries a `[buffered]`
marker with its original ISO timestamp and level; continuation lines
carry the bare marker. Capture is best-effort — the channel writes on
its own schedule — but a later failure flush replays anything a bundle
missed.
- Collecting a support bundle flushes the buffer
(`flush("support_bundle", { retain: true })`) before the CLI runs, and
**retains** the ring so a later failure flush still has the entries.
- When the channel is at **Off**, flush keeps the entries buffered
instead of discarding them.
- The buffer is bounded by both entry count and characters
(`MAX_BUFFERED_CHARS = 2_000_000`); `trim()` enforces both budgets with
oldest-eviction, and `flush()` replays in chunks of 100 to avoid a
single oversized write.
- Wires the buffer into `ServiceContainer` and adds the
`coder.connectionLogBuffer.size` setting (default `1000`, capped at
`10000`, `0` disables). The setting readers live in
`src/settings/logger.ts` and the container reads/watches the size
through a single `readSize()` helper via `watchConfigurationChanges`.
The setting is included in `COLLECTED_SETTINGS`.
- Flushes only on genuine connection failures, gated by an explicit
`failure?: boolean` on the connection-log reason (not on transient
reconnects, a handshake `401`, or intentional teardown):
- a reconnecting WebSocket terminal failure (`unrecoverable_close`,
`unrecoverable_http` where the status is not `401`, or
`certificate_error`);
- a failure while opening a workspace (canceled build, missing agent,
timeout, or CLI/certificate error), funnelled through
`Remote.closeRemote()`.
- Connection-failure flushes are funnelled through
`BufferingLogger.onConnectionFailure(reason, route)` (part of
`ConnectionLogBuffer`), so the extension and remote paths flush
`${reason} ${route}` identically; the socket option defaults to a noop
when no observer is supplied.
- The flush reason carries the failing **route** for attribution; the
route is seeded on the socket so even a first-connect failure logs a
real route instead of `unknown`.
- Handshake status is parsed by a shared `handshakeStatus(error)`
(`src/websocket/utils.ts`) covering both `ws` (`Unexpected server
response: <code>`) and `eventsource` (`Non-200 status code (<code>)`),
so a host/port such as `127.0.0.1:4040` is no longer misread as HTTP 404
and the SSE path is handled. `CoderApi.is404Error` compares against
`HttpStatusCode.NOT_FOUND`.
- Redacts `registration_access_token` in HTTP body logging alongside the
other sensitive fields.
- Documents the behavior, config, hard-kill/OOM loss limitation, and SSH
log scope in `CONTRIBUTING.md`.
## WebSocket event fix
`OneWayWebSocket` now registers `open`/`close`/`error` via DOM-style
`addEventListener`, so `close` consumers receive a real `CloseEvent`
with `.code`/`.reason`. This makes `unrecoverable_close` reachable in
production (message events still use `ws.on("message", ...)` for JSON
parsing). Server-initiated normal closes (`1000`/`1001`) now go through
`scheduleReconnect` rather than parking the socket, so the
now-unreachable `normal_close` reason is dropped from the telemetry
unions and `EVENTS.md`.
## Scope notes
- Extension SSH debug logs that pass through the shared logger are
buffered; the CLI `ProxyCommand` file logs under
`coder.proxyLogDirectory` are not, since support bundles already collect
them from disk.
- The buffer lives in memory, so a hard kill or out-of-memory event
loses it (documented).
- Flushing after N consecutive failed reconnect attempts against an
unreachable server is deferred to follow-up #1112.
- Reading the handshake status from the libraries' public event APIs
(instead of parsing their error strings, which a real-library test now
guards) is deferred to follow-up #1118.
## Testing
- `pnpm typecheck`, `pnpm format:check`, and `pnpm lint` are clean.
- Affected/dependent unit suites pass (`logBuffer`, `settings/logger`,
`formatters`, `reconnectingWebSocket`, `oneWayWebSocket`, `coderApi`,
`workspaceMonitor`, `workspaceStateMachine`, `remote`,
`commands.supportBundle`, `instrumentation/websocket`).
<details>
<summary>Implementation plan & design decisions</summary>
### Design
- **Buffer:** bounded by entry count and characters; captures only calls
whose severity is below the channel's current level; formats entries at
record time; oldest-eviction; live-resizable via config; replays in
chunks.
- **Flush target (D3):** replay into the existing "Coder" output channel
at a level that still persists, with a `[buffered]` marker plus original
level/timestamp, chronologically next to the real failure logs. Support
bundles already collect the on-disk VS Code logs and also trigger a
flush before appending them, so no separate sink is needed. At Off,
entries are retained rather than discarded.
- **Flush reasons (D4):** genuine, surfaced connection failures only,
gated by an explicit `failure?: boolean` — reconnecting-socket terminal
failures (except a `401` handshake) and a workspace-open failure
funnelled through `closeRemote()`. Never on transient `retrying` drops
or intentional teardown (`manual_disconnect`, `replaced`,
dispose/deactivate/reload). The callback carries the failing route.
- **SSH scope (D5):** buffer extension SSH debug passing through the
shared `Logger`; do not buffer CLI `ProxyCommand` file logs already
handled via `coder.proxyLogDirectory`.
### Decisions
- D1: buffer all below-level session logs.
- D2: bound by entry count (and a character budget to cap memory).
- D3: replay into the existing Coder output channel at a persisted level
with `[buffered]` marker/original level/timestamp; retain at Off; also
flush on support-bundle collection.
- D4: flush only on genuine connection failure; not transient, not a
`401`, and not intentional teardown.
- D5: buffer extension SSH debug through the shared `Logger`; not CLI
ProxyCommand file logs.
</details>
---
🤖 Generated with Coder Agents. Reviewed and authored on behalf of
@aqandrew.
---------
Co-authored-by: Ehab Younes <ehab.alyounes@gmail.com>
Copy file name to clipboardExpand all lines: package.json
+7Lines changed: 7 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -215,6 +215,13 @@
215
215
"minimum": 0,
216
216
"default": 250
217
217
},
218
+
"coder.connectionLogBuffer.size": {
219
+
"markdownDescription": "Maximum number of log entries below the Coder output channel's log level to keep in memory. When a connection fails terminally or you collect a support bundle, the extension replays them into the channel so a bundle can carry the detail leading up to the failure without debug logging enabled beforehand. Capture is best-effort, since the channel writes on its own schedule. Set to `0` to disable.",
220
+
"type": "number",
221
+
"minimum": 0,
222
+
"maximum": 10000,
223
+
"default": 1000
224
+
},
218
225
"coder.httpClientLogLevel": {
219
226
"markdownDescription": "Controls the verbosity of HTTP client logging. This affects what details are logged for each HTTP request and response.",
0 commit comments