Gateway: optimize ws logs in normal mode

This commit is contained in:
Peter Steinberger
2025-12-18 13:27:45 +00:00
parent 0b8e8144af
commit d406d3a058
4 changed files with 160 additions and 9 deletions

55
docs/logging.md Normal file
View File

@@ -0,0 +1,55 @@
# Logging
Clawdis has two log “surfaces”:
- **Console output** (what you see in the terminal / Debug UI).
- **File logs** (JSON lines) written by the internal logger.
## File-based logger
Clawdis uses a file logger backed by `tslog` (`src/logging.ts`).
- Default rolling log file is under `/tmp/clawdis/` (one file per day): `clawdis-YYYY-MM-DD.log`
- The log file path and level can be configured via `~/.clawdis/clawdis.json`:
- `logging.file`
- `logging.level`
The file format is one JSON object per line.
## Console capture
The CLI entrypoint enables console capture (`src/index.ts` calls `enableConsoleCapture()`).
That means every `console.log/info/warn/error/debug/trace` is also written into the file logs,
while still behaving normally on stdout/stderr.
## Gateway WebSocket logs
The gateway prints WebSocket protocol logs in two modes:
- **Normal mode (no `--verbose`)**: only “interesting” RPC results are printed:
- errors (`ok=false`)
- slow calls (default threshold: `>= 50ms`)
- parse errors
- **Verbose mode (`--verbose`)**: prints all WS request/response traffic.
### WS log style
`clawdis gateway` supports a per-gateway style switch:
- `--ws-log auto` (default): normal mode is optimized; verbose mode uses compact output
- `--ws-log compact`: compact output (paired request/response) when verbose
- `--ws-log full`: full per-frame output when verbose
- `--compact`: alias for `--ws-log compact`
Examples:
```bash
# optimized (only errors/slow)
clawdis gateway
# show all WS traffic (paired)
clawdis gateway --verbose --ws-log compact
# show all WS traffic (full meta)
clawdis gateway --verbose --ws-log full
```

View File

@@ -58,8 +58,8 @@ export function registerGatewayCli(program: Command) {
.option("--verbose", "Verbose logging to stdout/stderr", false)
.option(
"--ws-log <style>",
'Verbose WebSocket log style ("full"|"compact")',
"full",
'WebSocket log style ("auto"|"full"|"compact")',
"auto",
)
.option("--compact", 'Alias for "--ws-log compact"', false)
.action(async (opts) => {
@@ -68,13 +68,18 @@ export function registerGatewayCli(program: Command) {
| string
| undefined;
const wsLogStyle: GatewayWsLogStyle =
wsLogRaw === "compact" ? "compact" : "full";
wsLogRaw === "compact"
? "compact"
: wsLogRaw === "full"
? "full"
: "auto";
if (
wsLogRaw !== undefined &&
wsLogRaw !== "auto" &&
wsLogRaw !== "compact" &&
wsLogRaw !== "full"
) {
defaultRuntime.error('Invalid --ws-log (use "full" or "compact")');
defaultRuntime.error('Invalid --ws-log (use "auto", "full", "compact")');
defaultRuntime.exit(1);
}
setGatewayWsLogStyle(wsLogStyle);

View File

@@ -90,7 +90,10 @@ import { setHeartbeatsEnabled } from "../web/auto-reply.js";
import { sendMessageWhatsApp } from "../web/outbound.js";
import { requestReplyHeartbeatNow } from "../web/reply-heartbeat-wake.js";
import { buildMessageWithAttachments } from "./chat-attachments.js";
import { getGatewayWsLogStyle } from "./ws-logging.js";
import {
DEFAULT_WS_SLOW_MS,
getGatewayWsLogStyle,
} from "./ws-logging.js";
import {
type ConnectParams,
ErrorCodes,
@@ -475,8 +478,13 @@ function logWs(
kind: string,
meta?: Record<string, unknown>,
) {
if (!isVerbose()) return;
if (getGatewayWsLogStyle() === "compact") {
const style = getGatewayWsLogStyle();
if (!isVerbose()) {
logWsOptimized(direction, kind, meta);
return;
}
if (style === "compact" || style === "auto") {
logWsCompact(direction, kind, meta);
return;
}
@@ -560,6 +568,85 @@ type WsInflightEntry = {
const wsInflightCompact = new Map<string, WsInflightEntry>();
let wsLastCompactConnId: string | undefined;
const wsInflightOptimized = new Map<string, number>();
function logWsOptimized(
direction: "in" | "out",
kind: string,
meta?: Record<string, unknown>,
) {
// Keep "normal" mode quiet: only surface errors, slow calls, and parser issues.
const connId = typeof meta?.connId === "string" ? meta.connId : undefined;
const id = typeof meta?.id === "string" ? meta.id : undefined;
const ok = typeof meta?.ok === "boolean" ? meta.ok : undefined;
const method = typeof meta?.method === "string" ? meta.method : undefined;
const inflightKey = connId && id ? `${connId}:${id}` : undefined;
if (direction === "in" && kind === "req" && inflightKey) {
wsInflightOptimized.set(inflightKey, Date.now());
if (wsInflightOptimized.size > 2000) wsInflightOptimized.clear();
return;
}
if (kind === "parse-error") {
const errorMsg =
typeof meta?.error === "string" ? formatForLog(meta.error) : undefined;
console.log(
[
`${chalk.gray("[gws]")} ${chalk.redBright("✗")} ${chalk.bold("parse-error")}`,
errorMsg ? `${chalk.dim("error")}=${errorMsg}` : undefined,
`${chalk.dim("conn")}=${chalk.gray(shortId(connId ?? "?"))}`,
]
.filter((t): t is string => Boolean(t))
.join(" "),
);
return;
}
if (direction !== "out" || kind !== "res") return;
const startedAt = inflightKey ? wsInflightOptimized.get(inflightKey) : undefined;
if (inflightKey) wsInflightOptimized.delete(inflightKey);
const durationMs =
typeof startedAt === "number" ? Date.now() - startedAt : undefined;
const shouldLog =
ok === false ||
(typeof durationMs === "number" && durationMs >= DEFAULT_WS_SLOW_MS);
if (!shouldLog) return;
const statusToken =
ok === undefined
? undefined
: ok
? chalk.greenBright("✓")
: chalk.redBright("✗");
const durationToken =
typeof durationMs === "number" ? chalk.dim(`${durationMs}ms`) : undefined;
const restMeta: string[] = [];
if (meta) {
for (const [key, value] of Object.entries(meta)) {
if (value === undefined) continue;
if (key === "connId" || key === "id") continue;
if (key === "method" || key === "ok") continue;
restMeta.push(`${chalk.dim(key)}=${formatForLog(value)}`);
}
}
const tokens = [
`${chalk.gray("[gws]")} ${chalk.yellowBright("⇄")} ${chalk.bold("res")}`,
statusToken,
method ? chalk.bold(method) : undefined,
durationToken,
...restMeta,
connId ? `${chalk.dim("conn")}=${chalk.gray(shortId(connId))}` : undefined,
id ? `${chalk.dim("id")}=${chalk.gray(shortId(id))}` : undefined,
].filter((t): t is string => Boolean(t));
console.log(tokens.join(" "));
}
function logWsCompact(
direction: "in" | "out",
@@ -2197,6 +2284,8 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
id: req.id,
ok,
method: req.method,
errorCode: error?.code,
errorMessage: error?.message,
...meta,
});
};

View File

@@ -1,6 +1,6 @@
export type GatewayWsLogStyle = "full" | "compact";
export type GatewayWsLogStyle = "auto" | "full" | "compact";
let gatewayWsLogStyle: GatewayWsLogStyle = "full";
let gatewayWsLogStyle: GatewayWsLogStyle = "auto";
export function setGatewayWsLogStyle(style: GatewayWsLogStyle): void {
gatewayWsLogStyle = style;
@@ -9,3 +9,5 @@ export function setGatewayWsLogStyle(style: GatewayWsLogStyle): void {
export function getGatewayWsLogStyle(): GatewayWsLogStyle {
return gatewayWsLogStyle;
}
export const DEFAULT_WS_SLOW_MS = 50;