Files
OmniRoute/src/lib/logEnv.ts
Markus Hartung d925f6bf73 fix(logging): document CHAT_LOG_MAX_BODY_KB, capture messageCount for Responses API bodies (#10038)
* fix(logging): document CHAT_LOG_MAX_BODY_KB, capture messageCount for Responses API bodies

Extracted from PR #9439 (agentic conversation tracking). Most of the
original scope this commit was cherry-picked from (CHAT_LOG_MAX_BODY_KB
env var support, the estimateSizeFast() earlyExitAt parameterization)
turned out to already be present on the current upstream/release/v3.8.50
tip -- confirmed via diff and by running check-env-doc-sync.test.ts /
tests/unit/chatcore-log-truncation.test.ts against pristine upstream
before making any changes here. Only two genuine gaps remained:

1. CHAT_LOG_MAX_BODY_KB was read by getChatLogMaxBodyBytes() but
   undocumented in .env.example and docs/reference/ENVIRONMENT.md --
   tests/unit/check-env-doc-sync.test.ts flags any env var read in code
   but missing from both doc files. Documented it (both required --
   the same test enforces the pairing).

2. truncateForLog()'s summary only computed messageCount from
   obj.messages (OpenAI-chat/Gemini field name) -- a large /v1/responses
   request (which uses input[], not messages[]) got summarized with no
   count at all, leaving the dashboard's "Full Conversation" panel
   nothing to base its "N messages not shown" placeholder on for any
   Responses-API conversation, even though the same summarization logic
   applies to it.

Test plan:
- TDD: tests/unit/chatcore-log-truncation.test.ts's new regression test
  ("captures a message count for Responses API bodies too") confirmed
  failing against the pre-fix code, passing after.
- tests/unit/check-env-doc-sync.test.ts confirms CHAT_LOG_MAX_BODY_KB no
  longer appears in codeMissingEnv (remaining drift in that test is
  pre-existing/unrelated -- ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS,
  COMMANDCODE_API_URL, OMNIROUTE_STRICT_SYSTEM_PROVIDERS,
  TLS_FINGERPRINT_PROVIDERS -- confirmed identical on a pristine
  upstream/release/v3.8.50 checkout, base-red inherited: #9985).
- tests/unit/chatcore-log-truncation.test.ts -- 19/19 passing.
- npx tsc --noEmit / npm run lint -- clean.

⚠️ base-red inherited: #9985

* docs(logging): consolidate CHAT_LOG_MAX_BODY_KB into a single entry per file

The variable was already documented (with a stale src/lib/chatLogTruncation.ts
reference in .env.example); keep the new richer entries next to the CHAT_LOG_*
family and drop the old duplicates.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-13 04:02:42 -03:00

202 lines
7.9 KiB
TypeScript

import path from "path";
import { resolveDataDir } from "@/lib/dataPaths";
const DEFAULT_APP_LOG_RETENTION_DAYS = 7;
const DEFAULT_CALL_LOG_RETENTION_DAYS = 7;
const DEFAULT_APP_LOG_MAX_SIZE = 50 * 1024 * 1024;
const DEFAULT_APP_LOG_MAX_FILES = 20;
const DEFAULT_CALL_LOG_MAX_ENTRIES = 10000;
const DEFAULT_CALL_LOGS_TABLE_MAX_ROWS = 100000;
const DEFAULT_CALL_LOG_PIPELINE_MAX_SIZE_KB = 512;
const DEFAULT_PROXY_LOGS_TABLE_MAX_ROWS = 100000;
/**
* Default app log path, anchored to DATA_DIR (never `process.cwd()`).
*
* The globally-installed CLI runs from an arbitrary working directory, so anchoring
* the default to cwd made file logging silently no-op under an unrelated directory
* (#6197). Computed lazily so a per-process/per-test `DATA_DIR` override is honoured
* (the env var is read at call time, not at module load). Uses the pure
* `resolveDataDir()` resolver — no directory creation side effects in a path getter.
*/
function getDefaultAppLogPath(): string {
return path.join(resolveDataDir(), "logs", "application", "app.log");
}
function parsePositiveInt(value: string | undefined, fallback: number): number {
if (!value) return fallback;
const parsed = Number.parseInt(value, 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function parseNonNegativeInt(value: string | undefined, fallback: number): number {
if (value === undefined || value === "") return fallback;
const parsed = Number.parseInt(value, 10);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
}
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
if (!value) return fallback;
const normalized = value.trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) return true;
if (["0", "false", "no", "off"].includes(normalized)) return false;
return fallback;
}
export function parseFileSize(raw: string | undefined): number {
if (!raw) return DEFAULT_APP_LOG_MAX_SIZE;
const match = raw.match(/^(\d+)\s*(k|m|g|kb|mb|gb)?$/i);
if (!match) return DEFAULT_APP_LOG_MAX_SIZE;
const num = parseInt(match[1], 10);
const unit = (match[2] || "").toLowerCase();
switch (unit) {
case "k":
case "kb":
return num * 1024;
case "m":
case "mb":
return num * 1024 * 1024;
case "g":
case "gb":
return num * 1024 * 1024 * 1024;
default:
return num;
}
}
export function getAppLogToFile(): boolean {
return process.env.APP_LOG_TO_FILE !== "false";
}
export function getAppLogFilePath(): string {
return process.env.APP_LOG_FILE_PATH || getDefaultAppLogPath();
}
export function getAppLogMaxFileSize(): number {
return parseFileSize(process.env.APP_LOG_MAX_FILE_SIZE);
}
export function getAppLogRetentionDays(): number {
return parsePositiveInt(process.env.APP_LOG_RETENTION_DAYS, DEFAULT_APP_LOG_RETENTION_DAYS);
}
export function getCallLogRetentionDays(): number {
return parsePositiveInt(process.env.CALL_LOG_RETENTION_DAYS, DEFAULT_CALL_LOG_RETENTION_DAYS);
}
/**
* Returns the explicit operator-set retention override (a positive integer), or `null`
* when the env var is unset/empty/invalid. Callers give an explicit env var precedence
* over the dashboard's database retention, while falling back to the dashboard (not the
* hardcoded 7-day default) when the operator did not set the env var. (#4354)
*/
function parsePositiveIntOrNull(value: string | undefined): number | null {
if (value === undefined || value === "") return null;
const parsed = Number.parseInt(value, 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
}
export function getAppLogRetentionDaysOverride(): number | null {
return parsePositiveIntOrNull(process.env.APP_LOG_RETENTION_DAYS);
}
export function getCallLogRetentionDaysOverride(): number | null {
return parsePositiveIntOrNull(process.env.CALL_LOG_RETENTION_DAYS);
}
export function getAppLogMaxFiles(): number {
return parsePositiveInt(process.env.APP_LOG_MAX_FILES, DEFAULT_APP_LOG_MAX_FILES);
}
export function getCallLogMaxEntries(): number {
return parsePositiveInt(process.env.CALL_LOG_MAX_ENTRIES, DEFAULT_CALL_LOG_MAX_ENTRIES);
}
export function getCallLogsTableMaxRows(): number {
return parsePositiveInt(process.env.CALL_LOGS_TABLE_MAX_ROWS, DEFAULT_CALL_LOGS_TABLE_MAX_ROWS);
}
export function getCallLogPipelineCaptureStreamChunks(): boolean {
return parseBoolean(process.env.CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS, false);
}
export function getCallLogPipelineMaxSizeBytes(): number {
return (
parsePositiveInt(
process.env.CALL_LOG_PIPELINE_MAX_SIZE_KB,
DEFAULT_CALL_LOG_PIPELINE_MAX_SIZE_KB
) * 1024
);
}
export function getProxyLogsTableMaxRows(): number {
return parsePositiveInt(process.env.PROXY_LOGS_TABLE_MAX_ROWS, DEFAULT_PROXY_LOGS_TABLE_MAX_ROWS);
}
export function getAppLogLevel(defaultLevel: string): string {
return process.env.APP_LOG_LEVEL || defaultLevel;
}
export function getAppLogFormat(defaultFormat: string): string {
return process.env.APP_LOG_FORMAT || defaultFormat;
}
// ─── Chat log truncation limits ─────────────────────────────────────────────
export function getChatLogTextLimit(): number {
return parsePositiveInt(process.env.CHAT_LOG_TEXT_LIMIT, 64 * 1024);
}
/**
* Was a hardcoded/default 24 — real agentic CLIs with many MCP servers
* routinely declare 40-50+ tools in a single `tools[]` array (a live
* OpenClaw session logged 47), so the tail-24 default silently dropped the
* array's earlier entries behind an `_omniroute_truncated_array` marker —
* including, in one traced case, the tool actually being called
* (`apply_patch`), making its declared shape unrecoverable from the call
* log even though the call itself succeeded. Bumped to comfortably cover
* real large tool lists with headroom; same configurable-override pattern
* as the sibling CHAT_LOG_TEXT_LIMIT/CHAT_LOG_MAX_BODY_KB vars.
*/
export function getChatLogArrayTailItems(): number {
return parsePositiveInt(process.env.CHAT_LOG_ARRAY_TAIL_ITEMS, 128);
}
/**
* Was a hardcoded 6 — trivially too shallow for real Chat Completions tool
* calls: `body.choices[0].message.tool_calls[0].function` alone is already
* 6 levels deep (body→choices→[i]→message→tool_calls→[i]→function), so
* EVERY logged tool call got its `function` field (name + arguments)
* replaced outright with the literal string "[MaxDepth]" before the name/
* arguments one level further in were ever reached — not an edge case, a
* universal truncation of tool-call data in call log artifacts.
*/
export function getChatLogMaxDepth(): number {
return parsePositiveInt(process.env.CHAT_LOG_MAX_DEPTH, 20);
}
export function getChatLogMaxObjectKeys(): number {
return parseNonNegativeInt(process.env.CHAT_LOG_MAX_OBJECT_KEYS, 80);
}
/**
* Cap for a single logged request/response body before it gets replaced by a
* bare {_truncated, _originalBytes, messageCount, ...} summary instead of the
* full clone (open-sse/handlers/chatCore/logTruncation.ts::truncateForLog()).
* Was a hardcoded 8KB — trivially exceeded by any real multi-turn agentic
* conversation, which meant the dashboard's "Full Conversation" transcript
* panel could only ever show a placeholder instead of the actual messages
* for nearly every logged row. Bumped 128x (to 1MB) by default and exposed
* as an operator override for anyone who needs it even larger (or smaller,
* on a memory-constrained box) — see the same "protect memory across many
* call sites per request" reasoning truncateForLog()'s own doc comment
* explains for why some cap must still exist.
*/
export function getChatLogMaxBodyBytes(): number {
return parsePositiveInt(process.env.CHAT_LOG_MAX_BODY_KB, 1024) * 1024;
}
export function isChatDebugFileEnabled(): boolean {
if (parseBoolean(process.env.CHAT_DEBUG_FILE, false)) return true;
return process.env.APP_LOG_LEVEL?.trim().toLowerCase() === "debug";
}