Refactor router pipeline and update related components

This commit is contained in:
musistudio
2026-07-15 20:12:32 +08:00
parent adadb1679f
commit b1fb8e3331
19 changed files with 5653 additions and 183 deletions
@@ -115,6 +115,7 @@ class GatewayService {
return this.status;
}
await this.rawTraceSynchronizer.start();
await this.listen(config);
if (this.server) {
const proxyStatus = await proxyService.attach(config, this.server);
@@ -184,6 +185,7 @@ class GatewayService {
if (server) {
await closeServer(server);
}
await this.rawTraceSynchronizer.stop();
await proxyService.stop(options.proxyRestoreTimeoutMs);
await pluginService.stop();
+10 -3
View File
@@ -2,7 +2,11 @@ import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { Readable } from "node:stream";
import type { ApiKeyConfig, AppConfig, RequestRouteTraceChange } from "@ccr/core/contracts/app";
import { createSseErrorDetector, recordGatewayRequestLog } from "@ccr/core/observability/request-log-store";
import {
createSseErrorDetector,
markGatewayRequestLogDropped,
recordGatewayRequestLog
} from "@ccr/core/observability/request-log-store";
import { recordGatewayUsageCapture, type UsageCaptureInput } from "@ccr/core/usage/store";
import { ClaudeCodeRouterPlugin } from "@ccr/core/gateway/claude-code-router-plugin";
import { adaptRouteRequestBody, restoreRouteRequestBody } from "@ccr/core/routing/protocol-adapter";
@@ -220,11 +224,14 @@ export class GatewayRequestPipeline {
const successful = statusCode >= 200 && statusCode < 400 && !error;
const successSampleRate = config.observability.requestLogSuccessSampleRate ?? 1;
if (successful && !requestLogSampled(requestId, successSampleRate)) {
markGatewayRequestLogDropped(requestId, "sampled");
return;
}
const bodyCapture = config.observability.requestLogBodyCapture ?? "all";
const captureBody = bodyCapture === "all" || (bodyCapture === "errors" && !successful);
recordGatewayRequestLog({
captureBody: bodyCapture === "all" || (bodyCapture === "errors" && !successful),
bodyCapturePolicy: bodyCapture,
captureBody,
client,
completedAt: new Date().toISOString(),
durationMs: Date.now() - startedAt,
@@ -239,7 +246,7 @@ export class GatewayRequestPipeline {
requestBody: shouldSendBody(method) ? bodyToForward ?? Buffer.alloc(0) : Buffer.alloc(0),
requestHeaders: headers,
requestId,
routeTrace: routeTrace?.finish(),
routeTrace: routeTrace?.finish({ captureBodyValues: captureBody }),
responseBodyText,
responseBodySizeBytes,
responseBodyTruncated,
+10 -4
View File
@@ -300,10 +300,16 @@ export async function fetchUpstreamWithFallback(input: {
});
const hasNextAttempt = index < attempts.length - 1;
const attemptUrl = rewriteRouteModelInUrl(input.upstreamUrl, attempt.model);
const attemptHeaders = withCoreGatewayAuthHeader(
omitLocalObservabilityHeaders(attempt.headers ?? input.headers),
input.coreAuthToken
);
const attemptHeaders = {
...withCoreGatewayAuthHeader(
omitLocalObservabilityHeaders(attempt.headers ?? input.headers),
input.coreAuthToken
),
// Core raw traces use a unique request id for every fallback attempt,
// while turnKey identifies the outer gateway request. Keep both and mark
// the attempt so only the final response may refine the stored outcome.
"x-ccr-route-attempt": String(attemptNumber)
};
const attemptProvider = attempt.logicalProvider ?? (
attempt.target?.kind === "provider" ? attempt.target.provider.name : undefined
);
+5
View File
@@ -160,6 +160,11 @@ export function bundledToolHubMcpEntryPathCandidates(): string[] {
pathJoin(resourcesPath, "app", "dist", "main", TOOL_HUB_MCP_RUNTIME_FILE_NAME)
]
: []),
// Core tests bundle runtime entry points here without producing any
// packages/*/dist artifacts. Do not prefer test artifacts in normal runs.
...(process.env.NODE_TEST_CONTEXT
? [pathJoin(process.cwd(), ".test-dist", "core", "runtime", TOOL_HUB_MCP_RUNTIME_FILE_NAME)]
: []),
pathJoin(process.cwd(), "packages", "electron", "dist", "main", TOOL_HUB_MCP_RUNTIME_FILE_NAME),
pathJoin(process.cwd(), "packages", "cli", "dist", "main", TOOL_HUB_MCP_RUNTIME_FILE_NAME),
pathJoin(process.cwd(), "packages", "core", "dist", "main", TOOL_HUB_MCP_RUNTIME_FILE_NAME),
+45 -1
View File
@@ -44,13 +44,50 @@ let catalog: PriceCatalog | undefined;
let catalogPromise: Promise<PriceCatalog> | undefined;
export async function estimateUsageCostUsd(input: UsageCostInput): Promise<UsageCostEstimate | undefined> {
if (!hasBillableUsage(input)) {
return undefined;
}
const model = input.model?.trim();
if (!model || model === "unknown") {
return undefined;
}
const prices = await getPriceCatalog();
const price = findModelPrice(prices.index, model, input.provider);
return estimateUsageCostFromIndex(input, prices.index, model);
}
/**
* Loads the remote catalog without holding a caller's database transaction.
* Callers that own a write transaction can then use the cache-only estimator.
*/
export async function preloadUsagePriceCatalog(): Promise<void> {
await getPriceCatalog();
}
export function usagePriceCatalogNeedsRefresh(): boolean {
return !catalog || Date.now() - catalog.loadedAt >= catalogTtlMs;
}
/** Never performs I/O. Returns undefined when no fresh catalog is loaded. */
export function estimateUsageCostUsdFromLoadedCatalog(
input: UsageCostInput
): UsageCostEstimate | undefined {
if (!hasBillableUsage(input)) {
return undefined;
}
const model = input.model?.trim();
if (!model || model === "unknown" || usagePriceCatalogNeedsRefresh() || !catalog) {
return undefined;
}
return estimateUsageCostFromIndex(input, catalog.index, model);
}
function estimateUsageCostFromIndex(
input: UsageCostInput,
index: PriceIndex,
model: string
): UsageCostEstimate | undefined {
const price = findModelPrice(index, model, input.provider);
if (!price) {
return undefined;
}
@@ -76,6 +113,13 @@ export async function estimateUsageCostUsd(input: UsageCostInput): Promise<Usage
};
}
function hasBillableUsage(input: UsageCostInput): boolean {
return normalizeCount(input.inputTokens) +
normalizeCount(input.outputTokens) +
normalizeCount(input.cacheReadTokens) +
normalizeCount(input.cacheWriteTokens) > 0;
}
async function getPriceCatalog(): Promise<PriceCatalog> {
if (catalog && Date.now() - catalog.loadedAt < catalogTtlMs) {
return catalog;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,418 @@
import { existsSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
import {
createBetterSqliteDatabase,
type BetterSqliteDatabase,
type BetterSqliteStatement
} from "@ccr/core/storage/sqlite-native";
export type RequestLogAdmission = {
accepted: boolean;
bodyCapturePolicy: "all" | "errors" | "none";
bodyCaptureMaxBytes: number;
reason?: string;
recordedAt: number;
state: "committed" | "pending" | "rejected";
};
export type RawTraceAdmissionResolution = RequestLogAdmission | "pending";
const runtimeLeaseMs = 30_000;
const terminalAdmissionRetentionMs = 48 * 60 * 60 * 1_000;
const requestLogsReconnectCooldownMs = 1_000;
export class RequestLogAdmissionStore {
private readonly database: BetterSqliteDatabase;
private readonly pendingInsertStatement: BetterSqliteStatement;
private readonly pendingReadStatement: BetterSqliteStatement;
private readonly readStatement: BetterSqliteStatement;
private requestLogsDatabase?: BetterSqliteDatabase;
private readonly requestLogsDbFile: string;
private requestLogsReadStatement?: BetterSqliteStatement;
private requestLogsUnavailableUntil = 0;
private readonly runtimeId: string;
private readonly upsertStatement: BetterSqliteStatement;
constructor(dbFile: string, requestLogsDbFile: string, runtimeId: string) {
mkdirSync(dirname(dbFile), { recursive: true });
const database = createBetterSqliteDatabase(dbFile);
try {
database.pragma("journal_mode = WAL");
database.pragma("synchronous = NORMAL");
// Admission operations run on the gateway thread. Fail immediately on a
// competing writer; RequestLogRuntime retains and retries the exact
// operation asynchronously without blocking the event loop.
database.pragma("busy_timeout = 0");
database.exec(`
CREATE TABLE IF NOT EXISTS request_log_admissions (
request_id TEXT PRIMARY KEY,
state TEXT NOT NULL,
runtime_id TEXT NOT NULL,
body_capture_policy TEXT NOT NULL DEFAULT 'all',
body_capture_max_bytes INTEGER NOT NULL DEFAULT 0,
reason TEXT NOT NULL DEFAULT '',
recorded_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS request_log_admissions_state_runtime_idx
ON request_log_admissions(state, runtime_id);
CREATE INDEX IF NOT EXISTS request_log_admissions_recorded_at_idx
ON request_log_admissions(recorded_at);
CREATE TABLE IF NOT EXISTS request_log_admission_runtimes (
runtime_id TEXT PRIMARY KEY,
owner_pid INTEGER NOT NULL,
heartbeat_at INTEGER NOT NULL,
lease_expires_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS request_log_admission_runtimes_lease_idx
ON request_log_admission_runtimes(lease_expires_at);
CREATE TABLE IF NOT EXISTS request_log_raw_admission_pending (
request_id TEXT PRIMARY KEY,
first_seen_at INTEGER NOT NULL,
last_seen_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS request_log_raw_admission_pending_seen_idx
ON request_log_raw_admission_pending(first_seen_at);
`);
ensureAdmissionBodyCapturePolicyColumn(database);
// Older builds marked the whole logical request consumed after the first
// raw bundle. A logical request can own several fallback bundles.
database.exec(`
UPDATE request_log_admissions
SET state = 'committed', reason = ''
WHERE state = 'consumed'
`);
this.database = database;
this.requestLogsDbFile = requestLogsDbFile;
this.runtimeId = runtimeId;
this.readStatement = database.prepare(`
SELECT state, body_capture_policy, body_capture_max_bytes, reason, recorded_at
FROM request_log_admissions
WHERE request_id = ?
LIMIT 1
`);
this.upsertStatement = database.prepare(`
INSERT INTO request_log_admissions (
request_id,
state,
runtime_id,
body_capture_policy,
body_capture_max_bytes,
reason,
recorded_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(request_id) DO UPDATE SET
state = excluded.state,
runtime_id = excluded.runtime_id,
body_capture_policy = excluded.body_capture_policy,
body_capture_max_bytes = excluded.body_capture_max_bytes,
reason = excluded.reason,
recorded_at = excluded.recorded_at
`);
this.pendingReadStatement = database.prepare(`
SELECT first_seen_at
FROM request_log_raw_admission_pending
WHERE request_id = ?
`);
this.pendingInsertStatement = database.prepare(`
INSERT INTO request_log_raw_admission_pending (request_id, first_seen_at, last_seen_at)
VALUES (?, ?, ?)
ON CONFLICT(request_id) DO NOTHING
`);
this.heartbeat(runtimeId);
this.prune();
} catch (error) {
database.close();
throw error;
}
}
close(): void {
try {
this.database.prepare("DELETE FROM request_log_admission_runtimes WHERE runtime_id = ?")
.run(this.runtimeId);
} finally {
this.closeRequestLogsDatabase();
this.database.close();
}
}
heartbeat(runtimeId: string): void {
const now = Date.now();
this.database.prepare(`
INSERT INTO request_log_admission_runtimes (
runtime_id,
owner_pid,
heartbeat_at,
lease_expires_at
) VALUES (?, ?, ?, ?)
ON CONFLICT(runtime_id) DO UPDATE SET
owner_pid = excluded.owner_pid,
heartbeat_at = excluded.heartbeat_at,
lease_expires_at = excluded.lease_expires_at
`).run(runtimeId, process.pid, now, now + runtimeLeaseMs);
reconcileInterruptedAdmissions(this.database, this.requestLogsDbFile, runtimeId);
}
markCommitted(requestId: string, runtimeId: string): void {
this.database.prepare(`
UPDATE request_log_admissions
SET state = 'committed', reason = '', recorded_at = ?
WHERE request_id = ? AND state = 'pending' AND runtime_id = ?
`).run(Date.now(), requestId, runtimeId);
}
prune(now = Date.now()): void {
this.database.prepare(`
DELETE FROM request_log_admissions
WHERE state IN ('committed', 'rejected') AND recorded_at < ?
`).run(now - terminalAdmissionRetentionMs);
this.database.prepare(`
DELETE FROM request_log_raw_admission_pending
WHERE first_seen_at < ?
`).run(now - terminalAdmissionRetentionMs);
this.database.prepare(`
DELETE FROM request_log_admission_runtimes
WHERE lease_expires_at < ? AND runtime_id <> ?
`).run(now - terminalAdmissionRetentionMs, this.runtimeId);
}
read(requestId: string): RequestLogAdmission | undefined {
const row = this.readStatement.get(requestId) as Record<string, unknown> | undefined;
return row ? admissionFromRow(row) : undefined;
}
remember(input: {
accepted: boolean;
bodyCapturePolicy?: "all" | "errors" | "none";
bodyCaptureMaxBytes: number;
reason?: string;
requestId: string;
runtimeId: string;
}): void {
this.database.exec("BEGIN IMMEDIATE");
try {
this.upsertStatement.run(
input.requestId,
input.accepted ? "pending" : "rejected",
input.runtimeId,
input.bodyCapturePolicy ?? "all",
nonNegativeInteger(input.bodyCaptureMaxBytes),
input.reason ?? "",
Date.now()
);
this.database.prepare("DELETE FROM request_log_raw_admission_pending WHERE request_id = ?")
.run(input.requestId);
this.database.exec("COMMIT");
} catch (error) {
if (this.database.inTransaction) this.database.exec("ROLLBACK");
throw error;
}
}
resolveForRawTrace(requestId: string, pendingTtlMs: number): RawTraceAdmissionResolution {
const existing = this.read(requestId);
if (existing && existing.state !== "pending") return existing;
const committed = this.readCommittedRequestLogAdmission(requestId);
if (committed.state === "found") return committed.admission;
if (committed.state === "unavailable") return "pending";
if (existing) return existing;
const now = Date.now();
const pending = this.pendingReadStatement.get(requestId) as Record<string, unknown> | undefined;
const firstSeenAt = pending ? nonNegativeInteger(pending.first_seen_at) : now;
if (pending && now - firstSeenAt < Math.max(1, pendingTtlMs)) {
// Polling an already-pending admission is read-only. In particular, do
// not update last_seen_at on every raw-trace replay.
return "pending";
}
this.database.exec("BEGIN IMMEDIATE");
try {
const racedAdmission = this.read(requestId);
if (racedAdmission) {
this.database.exec("COMMIT");
return racedAdmission;
}
const racedPending = this.pendingReadStatement.get(requestId) as Record<string, unknown> | undefined;
const racedFirstSeenAt = racedPending ? nonNegativeInteger(racedPending.first_seen_at) : firstSeenAt;
if (racedPending && now - racedFirstSeenAt >= Math.max(1, pendingTtlMs)) {
this.upsertStatement.run(requestId, "rejected", this.runtimeId, "none", 0, "record_missing", now);
this.database.prepare("DELETE FROM request_log_raw_admission_pending WHERE request_id = ?")
.run(requestId);
this.database.exec("COMMIT");
return this.read(requestId)!;
}
this.pendingInsertStatement.run(requestId, racedFirstSeenAt, now);
this.database.exec("COMMIT");
return "pending";
} catch (error) {
if (this.database.inTransaction) this.database.exec("ROLLBACK");
throw error;
}
}
private closeRequestLogsDatabase(): void {
try {
this.requestLogsDatabase?.close();
} catch {
// Closing the primary admission database must still be attempted.
}
this.requestLogsDatabase = undefined;
this.requestLogsReadStatement = undefined;
}
private readCommittedRequestLogAdmission(requestId: string): CommittedAdmissionLookup {
if (!existsSync(this.requestLogsDbFile)) return { state: "missing" };
if (Date.now() < this.requestLogsUnavailableUntil) return { state: "unavailable" };
try {
if (!this.requestLogsDatabase) {
this.requestLogsDatabase = createBetterSqliteDatabase(this.requestLogsDbFile, {
fileMustExist: true,
readonly: true
});
this.requestLogsDatabase.pragma("busy_timeout = 0");
this.requestLogsReadStatement = this.requestLogsDatabase.prepare(`
SELECT
gateway_body_capture_policy,
gateway_body_capture_max_bytes,
completed_at
FROM request_logs
WHERE request_id = ?
ORDER BY id DESC
LIMIT 1
`);
}
this.requestLogsUnavailableUntil = 0;
const row = this.requestLogsReadStatement!.get(requestId) as Record<string, unknown> | undefined;
if (!row) return { state: "missing" };
return {
admission: {
accepted: true,
bodyCapturePolicy: normalizeBodyCapturePolicy(row.gateway_body_capture_policy),
bodyCaptureMaxBytes: nonNegativeInteger(row.gateway_body_capture_max_bytes),
recordedAt: dateMs(row.completed_at),
state: "committed"
},
state: "found"
};
} catch {
this.closeRequestLogsDatabase();
this.requestLogsUnavailableUntil = Date.now() + requestLogsReconnectCooldownMs;
return { state: "unavailable" };
}
}
}
function reconcileInterruptedAdmissions(
database: BetterSqliteDatabase,
requestLogsDbFile: string,
runtimeId: string
): void {
const now = Date.now();
const owners = database.prepare(`
SELECT
admissions.runtime_id,
runtimes.lease_expires_at
FROM request_log_admissions AS admissions
LEFT JOIN request_log_admission_runtimes AS runtimes
ON runtimes.runtime_id = admissions.runtime_id
WHERE admissions.state = 'pending' AND admissions.runtime_id <> ?
GROUP BY admissions.runtime_id
`).all(runtimeId) as Array<Record<string, unknown>>;
const interruptedRuntimeIds = owners.filter((owner) => {
const leaseExpiresAt = nonNegativeInteger(owner.lease_expires_at);
return leaseExpiresAt <= now;
}).map((owner) => String(owner.runtime_id ?? "")).filter(Boolean);
if (interruptedRuntimeIds.length === 0) return;
let attached = false;
try {
database.prepare("ATTACH DATABASE ? AS request_logs_db").run(requestLogsDbFile);
attached = true;
for (const interruptedRuntimeId of interruptedRuntimeIds) {
database.prepare(`
UPDATE request_log_admissions
SET
state = CASE
WHEN EXISTS (
SELECT 1
FROM request_logs_db.request_logs
WHERE request_logs.request_id = request_log_admissions.request_id
) THEN 'committed'
ELSE 'rejected'
END,
reason = CASE
WHEN EXISTS (
SELECT 1
FROM request_logs_db.request_logs
WHERE request_logs.request_id = request_log_admissions.request_id
) THEN ''
ELSE 'writer_unavailable'
END,
recorded_at = ?
WHERE state = 'pending' AND runtime_id = ?
`).run(now, interruptedRuntimeId);
}
} catch (error) {
// ATTACH/query/I/O failures do not prove that the request log is absent.
// Keep admissions pending so the next heartbeat can reconcile them.
console.warn(`[request-log] Admission reconciliation deferred: ${formatError(error)}`);
} finally {
if (attached) database.exec("DETACH DATABASE request_logs_db");
}
}
type CommittedAdmissionLookup = {
admission: RequestLogAdmission;
state: "found";
} | {
state: "missing" | "unavailable";
};
function admissionFromRow(row: Record<string, unknown>): RequestLogAdmission {
const state = normalizeState(row.state);
return {
accepted: state === "committed" || state === "pending",
bodyCapturePolicy: normalizeBodyCapturePolicy(row.body_capture_policy),
bodyCaptureMaxBytes: nonNegativeInteger(row.body_capture_max_bytes),
reason: stringValue(row.reason),
recordedAt: nonNegativeInteger(row.recorded_at),
state
};
}
function ensureAdmissionBodyCapturePolicyColumn(database: BetterSqliteDatabase): void {
const columns = database.prepare("PRAGMA table_info('request_log_admissions')").all() as Array<Record<string, unknown>>;
if (!columns.some((column) => column.name === "body_capture_policy")) {
database.exec("ALTER TABLE request_log_admissions ADD COLUMN body_capture_policy TEXT NOT NULL DEFAULT 'all'");
}
}
function normalizeBodyCapturePolicy(value: unknown): RequestLogAdmission["bodyCapturePolicy"] {
return value === "errors" || value === "none" ? value : "all";
}
function normalizeState(value: unknown): RequestLogAdmission["state"] {
if (value === "committed" || value === "pending") return value;
return "rejected";
}
function nonNegativeInteger(value: unknown): number {
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0;
}
function stringValue(value: unknown): string | undefined {
return typeof value === "string" && value ? value : undefined;
}
function dateMs(value: unknown): number {
const parsed = Date.parse(String(value ?? ""));
return Number.isFinite(parsed) ? parsed : Date.now();
}
function formatError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
@@ -26,15 +26,23 @@ import {
resolveRawTraceBodyLimit
} from "@ccr/core/observability/request-log-limits";
import { compactBase64ImagePayloads } from "@ccr/core/observability/request-log-body";
import {
RequestLogAdmissionStore,
type RequestLogAdmission
} from "@ccr/core/observability/request-log-admission-store";
import { suppressRouteTraceBodyValues } from "@ccr/core/observability/route-trace";
import { isSensitiveRequestLogHeaderName } from "@ccr/core/observability/sensitive-headers";
export type RequestLogEnqueueResult = {
accepted: boolean;
degraded: boolean;
reason?: "body_removed" | "closed" | "queue_full" | "writer_unavailable";
reason?: "body_removed" | "closed" | "queue_full" | "record_dropped" | "record_pending" | "writer_unavailable";
};
export type RequestLogRuntimeMetrics = {
accepted: number;
admissionOverlayItems: number;
admissionPendingOperations: number;
committed: number;
degraded: number;
dropped: number;
@@ -45,24 +53,33 @@ export type RequestLogRuntimeMetrics = {
};
export type RequestLogRuntimeOptions = {
admissionDbFile?: string;
admissionMaxPendingOperations?: number;
admissionOperationMaxAgeMs?: number;
admissionOverlayMaxEntries?: number;
admissionOverlayTtlMs?: number;
batchMaxBytes?: number;
batchMaxItems?: number;
batchMaxWaitMs?: number;
dbFile: string;
queueMaxBytes?: number;
queueMaxItems?: number;
pendingAdmissionTtlMs?: number;
rawTraceSpoolDir?: string;
workerFile?: string;
};
type ResolvedRuntimeOptions = Required<Omit<RequestLogRuntimeOptions, "rawTraceSpoolDir" | "workerFile">> & {
type ResolvedRuntimeOptions = Required<Omit<RequestLogRuntimeOptions, "admissionDbFile" | "rawTraceSpoolDir" | "workerFile">> & {
admissionDbFile: string;
rawTraceSpoolDir: string;
workerFile: string;
};
type QueuedCommand = RequestLogStoreWriteCommand & {
batchBytes: number;
isolated?: boolean;
sizeBytes: number;
writeAttempts: number;
};
type InFlightBatch = {
@@ -75,7 +92,8 @@ type WorkerResponse = {
error?: string;
requestId?: number;
result?: unknown;
type: "ack" | "batch-error" | "ready" | "response";
type: "ack" | "batch-error" | "maintenance" | "ready" | "response";
updated?: number;
};
type PendingRpc = {
@@ -83,22 +101,42 @@ type PendingRpc = {
resolve: (value: unknown) => void;
};
type AdmissionOperation = {
attempts: number;
createdAt: number;
key?: string;
overlayRequestId?: string;
overlayVersion?: number;
run: (store: RequestLogAdmissionStore) => void;
};
type AdmissionOverlayEntry = RequestLogAdmission & {
version: number;
};
// One raw-trace event can contain both a maximum-sized request and response.
// Keep room for exactly that case while retaining byte-based backpressure for
// concurrent events.
const defaultQueueMaxBytes = 128 * 1024 * 1024;
const sensitiveQueueHeaderNames = new Set([
"authorization",
"cookie",
"proxy-authorization",
"set-cookie",
"x-api-key",
"x-auth-api-key-id",
"x-auth-sub"
]);
const maxCommandWriteAttempts = 3;
const admissionRetryMaxDelayMs = 5_000;
const defaultAdmissionMaxPendingOperations = 20_000;
const defaultAdmissionOperationMaxAgeMs = 10 * 60 * 1_000;
const defaultAdmissionOverlayMaxEntries = 10_000;
const defaultAdmissionOverlayTtlMs = 10 * 60 * 1_000;
const admissionDrainTimeSliceMs = 5;
const admissionDrainMaxOperations = 100;
export class RequestLogRuntime {
private accepted = 0;
private admissionHeartbeatTimer?: NodeJS.Timeout;
private admissionLastPrunedAt = 0;
private admissionLastWarningAt = 0;
private readonly admissionOperationKeys = new Set<string>();
private readonly admissionOperations = new Map<number, AdmissionOperation>();
private readonly admissionOverlay = new Map<string, AdmissionOverlayEntry>();
private admissionRetryTimer?: NodeJS.Timeout;
private admissionStore?: RequestLogAdmissionStore;
private batchId = 0;
private closed = false;
private committed = 0;
@@ -108,6 +146,8 @@ export class RequestLogRuntime {
private flushTimer?: NodeJS.Timeout;
private readonly inFlight = new Map<number, InFlightBatch>();
private nextRequestId = 0;
private nextAdmissionOperationId = 0;
private nextAdmissionOverlayVersion = 0;
private nextSequence = 0;
private readonly options: ResolvedRuntimeOptions;
private outstandingBytes = 0;
@@ -115,6 +155,8 @@ export class RequestLogRuntime {
private queryWorker?: Worker;
private queryWorkerReady?: Promise<void>;
private readonly queue: QueuedCommand[] = [];
private revision = 0;
private readonly runtimeId = randomUUID();
private readonly writerRequests = new Map<number, PendingRpc>();
private writerRestartCount = 0;
private writerWorker?: Worker;
@@ -122,10 +164,28 @@ export class RequestLogRuntime {
constructor(options: RequestLogRuntimeOptions) {
this.options = {
admissionDbFile: options.admissionDbFile ?? `${options.dbFile}.admissions.sqlite`,
admissionMaxPendingOperations: positiveInteger(
options.admissionMaxPendingOperations,
defaultAdmissionMaxPendingOperations
),
admissionOperationMaxAgeMs: positiveInteger(
options.admissionOperationMaxAgeMs,
defaultAdmissionOperationMaxAgeMs
),
admissionOverlayMaxEntries: positiveInteger(
options.admissionOverlayMaxEntries,
defaultAdmissionOverlayMaxEntries
),
admissionOverlayTtlMs: positiveInteger(
options.admissionOverlayTtlMs,
defaultAdmissionOverlayTtlMs
),
batchMaxBytes: positiveInteger(options.batchMaxBytes, 4 * 1024 * 1024),
batchMaxItems: positiveInteger(options.batchMaxItems, 50),
batchMaxWaitMs: positiveInteger(options.batchMaxWaitMs, 10),
dbFile: options.dbFile,
pendingAdmissionTtlMs: positiveInteger(options.pendingAdmissionTtlMs, 5 * 60 * 1_000),
queueMaxBytes: positiveInteger(options.queueMaxBytes, defaultQueueMaxBytes),
queueMaxItems: positiveInteger(options.queueMaxItems, 2_000),
rawTraceSpoolDir: options.rawTraceSpoolDir ?? RAW_TRACE_SPOOL_DIR,
@@ -138,7 +198,18 @@ export class RequestLogRuntime {
const ordinarySuccess = input.statusCode >= 200 && input.statusCode < 400 && !input.error;
if (pressure >= 0.95 && ordinarySuccess) {
this.dropped += 1;
return { accepted: false, degraded: false, reason: "queue_full" };
const result: RequestLogEnqueueResult = {
accepted: false,
degraded: false,
reason: "queue_full"
};
this.rememberRecordAdmission(
input.requestId,
result,
0,
resolveRecordBodyCapturePolicy(input)
);
return result;
}
const prepared = prepareRecordForQueue(input, pressure);
const sizeBytes = estimateRecordBytes(prepared.input);
@@ -148,32 +219,86 @@ export class RequestLogRuntime {
kind: "record",
sequence: ++this.nextSequence,
batchBytes: sizeBytes,
sizeBytes
sizeBytes,
writeAttempts: 0
};
const result = this.enqueue(command, prepared.degraded ? "body_removed" : undefined);
this.rememberRecordAdmission(
input.requestId,
result,
prepared.bodyCaptureMaxBytes,
resolveRecordBodyCapturePolicy(input)
);
if (result.accepted && prepared.degraded) this.degraded += 1;
return result;
}
rejectRecord(requestId: string | undefined, reason = "sampled"): void {
this.rememberRecordAdmission(requestId, {
accepted: false,
degraded: false,
reason: "record_dropped"
}, 0, "none", reason);
}
enqueueRawTrace(
input: RequestLogRawTraceUpdateInput,
rawTraceFiles?: RequestLogRawTraceFiles
): RequestLogEnqueueResult {
const maxBodyBytes = resolveRawTraceBodyLimit(rawTraceFiles?.maxBodyBytes);
const prepared = prepareRawTraceForQueue(input, maxBodyBytes);
const recordAdmission = input.deferOutcomeUntilRecord
? this.resolveRecordAdmission(input.requestId)
: this.readRecordAdmission(input.requestId);
if (recordAdmission === "pending" ||
(input.deferOutcomeUntilRecord && recordAdmission?.state === "pending") ||
(recordAdmission === undefined && input.deferOutcomeUntilRecord)) {
return {
accepted: false,
degraded: false,
reason: "record_pending"
};
}
if (recordAdmission && !recordAdmission.accepted) {
this.dropped += 1;
return {
accepted: false,
degraded: false,
reason: "record_dropped"
};
}
const ordinarySuccess = input.statusCode !== undefined &&
input.statusCode >= 200 && input.statusCode < 400;
if (!recordAdmission && this.pressureRatio() >= 0.95 && ordinarySuccess) {
this.dropped += 1;
return { accepted: false, degraded: false, reason: "queue_full" };
}
const configuredMaxBodyBytes = resolveRawTraceBodyLimit(rawTraceFiles?.maxBodyBytes);
const maxBodyBytes = Math.min(
configuredMaxBodyBytes,
recordAdmission?.bodyCaptureMaxBytes ?? configuredMaxBodyBytes
);
const bodyPolicyDegraded = maxBodyBytes < configuredMaxBodyBytes;
const admittedInput = recordAdmission
? { ...input, bodyCapturePolicy: recordAdmission.bodyCapturePolicy }
: input;
const policyInput = maxBodyBytes === 0
? suppressRequestLogRawTraceBodies(admittedInput)
: admittedInput;
const queuedRawTraceFiles = constrainRawTraceFiles(rawTraceFiles, maxBodyBytes);
const prepared = prepareRawTraceForQueue(policyInput, maxBodyBytes);
const sizeBytes = Math.max(
estimateRawTraceBytes(prepared, rawTraceFiles),
rawTraceFileBytes(rawTraceFiles, maxBodyBytes)
estimateRawTraceBytes(prepared, queuedRawTraceFiles),
rawTraceFileBytes(queuedRawTraceFiles, maxBodyBytes)
);
const command: QueuedCommand = {
input: prepared,
kind: "raw-trace-update",
rawTraceFiles,
rawTraceFiles: queuedRawTraceFiles,
sequence: ++this.nextSequence,
batchBytes: sizeBytes,
sizeBytes
sizeBytes,
writeAttempts: 0
};
return this.enqueue(command);
return this.enqueue(command, bodyPolicyDegraded ? "body_removed" : undefined);
}
async list(filter?: RequestLogListFilter): Promise<RequestLogPage> {
@@ -193,8 +318,11 @@ export class RequestLogRuntime {
}
metrics(): RequestLogRuntimeMetrics {
this.pruneAdmissionOverlay();
return {
accepted: this.accepted,
admissionOverlayItems: this.admissionOverlay.size,
admissionPendingOperations: this.admissionOperations.size,
committed: this.committed,
degraded: this.degraded,
dropped: this.dropped,
@@ -234,6 +362,21 @@ export class RequestLogRuntime {
]);
this.queryWorker = undefined;
this.writerWorker = undefined;
if (this.admissionHeartbeatTimer) clearInterval(this.admissionHeartbeatTimer);
this.admissionHeartbeatTimer = undefined;
this.drainAdmissionOperations();
await waitUntil(() => this.admissionOperations.size === 0, options.timeoutMs);
if (this.admissionRetryTimer) clearTimeout(this.admissionRetryTimer);
this.admissionRetryTimer = undefined;
this.admissionOperations.clear();
this.admissionOperationKeys.clear();
this.admissionOverlay.clear();
try {
this.admissionStore?.close();
} catch (error) {
console.warn(`[request-log] Failed to close admission persistence: ${formatRuntimeError(error)}`);
}
this.admissionStore = undefined;
}
private enqueue(command: QueuedCommand, degradedReason?: RequestLogEnqueueResult["reason"]): RequestLogEnqueueResult {
@@ -274,6 +417,239 @@ export class RequestLogRuntime {
return this.queue.reduce((total, command) => total + command.batchBytes, 0);
}
private rememberRecordAdmission(
requestId: string | undefined,
result: RequestLogEnqueueResult,
bodyCaptureMaxBytes: number,
bodyCapturePolicy: "all" | "errors" | "none",
persistedReason: string | undefined = result.reason
): void {
const normalized = requestId?.trim();
if (!normalized) return;
const overlayVersion = this.setAdmissionOverlay(normalized, {
accepted: result.accepted,
bodyCapturePolicy,
bodyCaptureMaxBytes,
reason: persistedReason,
recordedAt: Date.now(),
state: result.accepted ? "pending" : "rejected"
});
this.submitAdmissionOperation({
attempts: 0,
createdAt: Date.now(),
overlayRequestId: normalized,
overlayVersion,
run: (store) => store.remember({
accepted: result.accepted,
bodyCapturePolicy,
bodyCaptureMaxBytes,
reason: persistedReason,
requestId: normalized,
runtimeId: this.runtimeId
})
});
}
private readRecordAdmission(requestId: string): RequestLogAdmission | undefined {
const normalized = requestId.trim();
if (!normalized) return undefined;
this.pruneAdmissionOverlay();
const overlay = this.admissionOverlay.get(normalized);
if (overlay) return overlay;
return this.useAdmissionStore((store) => store.read(normalized));
}
private resolveRecordAdmission(requestId: string): RequestLogAdmission | "pending" | undefined {
const normalized = requestId.trim();
if (!normalized) return undefined;
this.pruneAdmissionOverlay();
const overlay = this.admissionOverlay.get(normalized);
if (overlay) return overlay;
return this.useAdmissionStore((store) =>
store.resolveForRawTrace(normalized, this.options.pendingAdmissionTtlMs));
}
private markRecordAdmissionsCommitted(commands: QueuedCommand[]): void {
for (const command of commands) {
if (command.kind !== "record") continue;
const requestId = command.input.requestId?.trim();
if (!requestId) continue;
const existing = this.admissionOverlay.get(requestId);
const overlayVersion = this.setAdmissionOverlay(requestId, {
accepted: true,
bodyCaptureMaxBytes: existing?.bodyCaptureMaxBytes ?? nonNegativeInteger(command.input.maxBodyBytes),
bodyCapturePolicy: existing?.bodyCapturePolicy ?? resolveRecordBodyCapturePolicy(command.input),
recordedAt: Date.now(),
state: "committed"
});
this.submitAdmissionOperation({
attempts: 0,
createdAt: Date.now(),
overlayRequestId: requestId,
overlayVersion,
run: (store) => store.markCommitted(requestId, this.runtimeId)
});
}
}
private useAdmissionStore<T>(operation: (store: RequestLogAdmissionStore) => T): T | undefined {
try {
const store = this.ensureAdmissionStore();
return operation(store);
} catch (error) {
this.handleAdmissionFailure(error);
return undefined;
}
}
private ensureAdmissionStore(): RequestLogAdmissionStore {
this.admissionStore ??= new RequestLogAdmissionStore(
this.options.admissionDbFile,
this.options.dbFile,
this.runtimeId
);
this.ensureAdmissionHeartbeat();
return this.admissionStore;
}
private submitAdmissionOperation(operation: AdmissionOperation): void {
if (operation.key && this.admissionOperationKeys.has(operation.key)) return;
const operationId = ++this.nextAdmissionOperationId;
this.admissionOperations.set(operationId, operation);
if (operation.key) this.admissionOperationKeys.add(operation.key);
while (this.admissionOperations.size > this.options.admissionMaxPendingOperations) {
const oldest = this.admissionOperations.entries().next().value as
[number, AdmissionOperation] | undefined;
if (!oldest) break;
this.settleAdmissionOperation(oldest[0], oldest[1]);
this.warnAdmissionBound("pending operation capacity");
}
this.drainAdmissionOperations();
}
private drainAdmissionOperations(): void {
if (this.admissionRetryTimer) return;
const startedAt = Date.now();
let processed = 0;
while (this.admissionOperations.size > 0) {
if (processed >= admissionDrainMaxOperations || Date.now() - startedAt >= admissionDrainTimeSliceMs) {
this.scheduleAdmissionDrain(0);
return;
}
const next = this.admissionOperations.entries().next().value as
[number, AdmissionOperation] | undefined;
if (!next) return;
const [operationId, operation] = next;
if (Date.now() - operation.createdAt >= this.options.admissionOperationMaxAgeMs) {
this.settleAdmissionOperation(operationId, operation);
this.warnAdmissionBound("pending operation TTL");
processed += 1;
continue;
}
try {
operation.run(this.ensureAdmissionStore());
this.settleAdmissionOperation(operationId, operation);
processed += 1;
} catch (error) {
operation.attempts += 1;
this.handleAdmissionFailure(error);
const delayMs = Math.min(
admissionRetryMaxDelayMs,
25 * (2 ** Math.min(8, operation.attempts - 1))
);
this.scheduleAdmissionDrain(delayMs);
return;
}
}
}
private scheduleAdmissionDrain(delayMs: number): void {
if (this.admissionRetryTimer) return;
this.admissionRetryTimer = setTimeout(() => {
this.admissionRetryTimer = undefined;
this.drainAdmissionOperations();
}, delayMs);
this.admissionRetryTimer.unref?.();
}
private handleAdmissionFailure(error: unknown): void {
if (!isTransientSqliteLock(error)) {
try {
this.admissionStore?.close();
} catch {
// The original persistence failure is the actionable error.
}
this.admissionStore = undefined;
}
const now = Date.now();
if (now - this.admissionLastWarningAt >= 30_000) {
this.admissionLastWarningAt = now;
console.warn(`[request-log] Admission persistence operation queued for retry: ${formatRuntimeError(error)}`);
}
}
private setAdmissionOverlay(
requestId: string,
admission: RequestLogAdmission
): number {
this.pruneAdmissionOverlay(admission.recordedAt);
const version = ++this.nextAdmissionOverlayVersion;
this.admissionOverlay.delete(requestId);
this.admissionOverlay.set(requestId, { ...admission, version });
while (this.admissionOverlay.size > this.options.admissionOverlayMaxEntries) {
const oldestRequestId = this.admissionOverlay.keys().next().value as string | undefined;
if (!oldestRequestId) break;
this.admissionOverlay.delete(oldestRequestId);
this.warnAdmissionBound("overlay capacity");
}
return version;
}
private pruneAdmissionOverlay(now = Date.now()): void {
const cutoff = now - this.options.admissionOverlayTtlMs;
for (const [requestId, admission] of this.admissionOverlay) {
if (admission.recordedAt > cutoff) break;
this.admissionOverlay.delete(requestId);
}
}
private settleAdmissionOperation(operationId: number, operation: AdmissionOperation): void {
this.admissionOperations.delete(operationId);
if (operation.key) this.admissionOperationKeys.delete(operation.key);
if (!operation.overlayRequestId || operation.overlayVersion === undefined) return;
const overlay = this.admissionOverlay.get(operation.overlayRequestId);
if (overlay?.version === operation.overlayVersion) {
this.admissionOverlay.delete(operation.overlayRequestId);
}
}
private warnAdmissionBound(bound: string): void {
const now = Date.now();
if (now - this.admissionLastWarningAt < 30_000) return;
this.admissionLastWarningAt = now;
console.warn(`[request-log] Admission ${bound} reached; oldest fail-closed state was released.`);
}
private ensureAdmissionHeartbeat(): void {
if (this.admissionHeartbeatTimer || this.closed) return;
this.admissionHeartbeatTimer = setInterval(() => {
this.submitAdmissionOperation({
attempts: 0,
createdAt: Date.now(),
key: "heartbeat",
run: (store) => {
store.heartbeat(this.runtimeId);
const now = Date.now();
if (now - this.admissionLastPrunedAt >= 60 * 60 * 1_000) {
store.prune(now);
this.admissionLastPrunedAt = now;
}
}
});
}, 10_000);
this.admissionHeartbeatTimer.unref?.();
}
private schedulePump(immediate = false): void {
if (this.inFlight.size > 0 || this.queue.length === 0) return;
if (immediate) {
@@ -298,10 +674,14 @@ export class RequestLogRuntime {
}
const commands: QueuedCommand[] = [];
let bytes = 0;
const isolatedBatch = Boolean(this.queue[0]?.isolated);
while (this.queue.length > 0 && commands.length < this.options.batchMaxItems) {
const next = this.queue[0];
if (commands.length > 0 && bytes + next.batchBytes > this.options.batchMaxBytes) break;
commands.push(this.queue.shift()!);
if (commands.length > 0 &&
(isolatedBatch || next.isolated || bytes + next.batchBytes > this.options.batchMaxBytes)) break;
const command = this.queue.shift()!;
command.writeAttempts += 1;
commands.push(command);
bytes += next.batchBytes;
}
if (commands.length === 0 || !this.writerWorker) return;
@@ -351,17 +731,60 @@ export class RequestLogRuntime {
this.inFlight.delete(message.batchId);
this.outstandingBytes = Math.max(0, this.outstandingBytes - batch.bytes);
this.committed += batch.commands.length;
this.revision += batch.commands.length;
this.markRecordAdmissionsCommitted(batch.commands);
this.scheduleRawTraceCleanup(batch.commands);
this.schedulePump(true);
return;
}
if (message.type === "batch-error") {
this.handleWriterFailure(this.writerWorker, new Error(message.error || "request log batch failed"));
this.handleBatchError(message);
return;
}
if (message.type === "maintenance" && (message.updated ?? 0) > 0) {
this.revision += 1;
return;
}
settleRpc(this.writerRequests, message);
}
private handleBatchError(message: WorkerResponse): void {
if (message.batchId === undefined) {
this.handleWriterFailure(this.writerWorker, new Error(message.error || "request log batch failed"));
return;
}
const batch = this.inFlight.get(message.batchId);
if (!batch) return;
this.inFlight.delete(message.batchId);
if (batch.commands.length > 1) {
this.queue.unshift(...batch.commands.map((command) => ({ ...command, isolated: true })));
this.schedulePump(true);
return;
}
const command = batch.commands[0];
if (command.writeAttempts < maxCommandWriteAttempts) {
this.queue.unshift({ ...command, isolated: true });
} else {
this.outstandingBytes = Math.max(0, this.outstandingBytes - command.sizeBytes);
this.dropped += 1;
if (command.kind === "record") {
this.rememberRecordAdmission(command.input.requestId, {
accepted: false,
degraded: false,
reason: "writer_unavailable"
}, 0, resolveRecordBodyCapturePolicy(command.input));
this.scheduleRawTraceCleanup([command]);
}
console.warn(
`[request-log] ${command.kind === "raw-trace-update" ? "Retaining" : "Dropping"} ` +
`${command.kind} sequence ${command.sequence} after ` +
`${command.writeAttempts} failed write attempts: ${message.error || "request log batch failed"}`
);
}
this.schedulePump(true);
}
private handleWriterFailure(worker: Worker | undefined, error: Error): void {
if (!worker || worker !== this.writerWorker) return;
this.writerWorker = undefined;
@@ -453,7 +876,7 @@ export class RequestLogRuntime {
await this.ensureQueryWorker();
if (!this.queryWorker) throw new Error("Request log query worker is unavailable.");
return await rpc<T>(this.queryWorker, this.queryRequests, ++this.nextRequestId, method, args, {
revision: this.committed
revision: this.revision
});
}
@@ -484,11 +907,20 @@ export function createRequestLogRuntime(options: RequestLogRuntimeOptions): Requ
return new RequestLogRuntime(options);
}
function prepareRecordForQueue(input: RequestLogRecordInput, pressure: number): { degraded: boolean; input: RequestLogRecordInput } {
function prepareRecordForQueue(
input: RequestLogRecordInput,
pressure: number
): { bodyCaptureMaxBytes: number; degraded: boolean; input: RequestLogRecordInput } {
const maxBodyBytes = Math.max(0, Math.min(maxRequestLogBodyBytes, input.maxBodyBytes ?? defaultRequestLogBodyBytes));
const ordinarySuccess = input.statusCode >= 200 && input.statusCode < 400 && !input.error;
const removeBodies = input.captureBody === false || (pressure >= 0.7 && ordinarySuccess);
const bodyCapturePolicy = resolveRecordBodyCapturePolicy(input);
const admissionBodyCaptureMaxBytes = bodyCapturePolicy === "none" ||
(pressure >= 0.7 && ordinarySuccess)
? 0
: maxBodyBytes;
const removeTrace = pressure >= 0.85 && ordinarySuccess;
const suppressTraceBodyValues = (removeBodies || maxBodyBytes === 0) && input.routeTrace !== undefined;
const compactedRequest = removeBodies
? { buffer: Buffer.alloc(0), compacted: false }
: compactBase64ImagePayloads(input.requestBody);
@@ -502,17 +934,25 @@ function prepareRecordForQueue(input: RequestLogRecordInput, pressure: number):
const responseBodySizeBytes = input.responseBodySizeBytes ?? Buffer.byteLength(input.responseBodyText ?? "");
const responseBodyCapturedBytes = Buffer.byteLength(responseBodyText);
return {
degraded: removeBodies || removeTrace || compactedRequest.compacted || compactedResponse.compacted ||
bodyCaptureMaxBytes: admissionBodyCaptureMaxBytes,
degraded: removeBodies || removeTrace || suppressTraceBodyValues || compactedRequest.compacted || compactedResponse.compacted ||
requestBody.byteLength < compactedRequest.buffer.byteLength ||
responseBodyCapturedBytes < responseBodySizeBytes,
input: {
...input,
bodyCapturePolicy,
captureBody: !removeBodies,
maxBodyBytes: admissionBodyCaptureMaxBytes,
requestBody,
requestBodySizeBytes: input.requestBodySizeBytes ?? input.requestBody.byteLength,
requestBodyTruncated: removeBodies || compactedRequest.compacted || Boolean(input.requestBodyTruncated) ||
requestBody.byteLength < compactedRequest.buffer.byteLength,
requestHeaders: plainHeaderRecord(input.requestHeaders),
routeTrace: removeTrace ? undefined : input.routeTrace,
routeTrace: removeTrace
? undefined
: (suppressTraceBodyValues && input.routeTrace
? suppressRouteTraceBodyValues(input.routeTrace)
: input.routeTrace),
responseBodyText,
responseBodySizeBytes,
responseBodyTruncated: removeBodies || compactedResponse.compacted || Boolean(input.responseBodyTruncated) ||
@@ -522,6 +962,15 @@ function prepareRecordForQueue(input: RequestLogRecordInput, pressure: number):
};
}
function resolveRecordBodyCapturePolicy(
input: RequestLogRecordInput
): "all" | "errors" | "none" {
if (input.bodyCapturePolicy === "errors" || input.bodyCapturePolicy === "none") {
return input.bodyCapturePolicy;
}
return input.bodyCapturePolicy === "all" || input.captureBody !== false ? "all" : "none";
}
function prepareRawTraceForQueue(
input: RequestLogRawTraceUpdateInput,
maxBodyBytes: number
@@ -557,6 +1006,46 @@ function prepareRawTraceForQueue(
};
}
function constrainRawTraceFiles(
files: RequestLogRawTraceFiles | undefined,
maxBodyBytes: number
): RequestLogRawTraceFiles | undefined {
if (!files) return undefined;
const {
requestBody: _requestBody,
responseBody: _responseBody,
...metadata
} = files;
return {
...metadata,
maxBodyBytes,
...(maxBodyBytes > 0 && files.requestBody ? { requestBody: files.requestBody } : {}),
...(maxBodyBytes > 0 && files.responseBody ? { responseBody: files.responseBody } : {})
};
}
export function suppressRequestLogRawTraceBodies(
input: RequestLogRawTraceUpdateInput
): RequestLogRawTraceUpdateInput {
const requestSize = input.requestBodySizeBytes ??
(input.requestBodyText === undefined ? undefined : Buffer.byteLength(input.requestBodyText));
const responseSize = input.responseBodySizeBytes ??
(input.responseBodyText === undefined ? undefined : Buffer.byteLength(input.responseBodyText));
return {
...input,
...(requestSize === undefined ? {} : {
requestBodySizeBytes: requestSize,
requestBodyText: "",
requestBodyTruncated: Boolean(input.requestBodyTruncated) || requestSize > 0
}),
...(responseSize === undefined ? {} : {
responseBodySizeBytes: responseSize,
responseBodyText: "",
responseBodyTruncated: Boolean(input.responseBodyTruncated) || responseSize > 0
})
};
}
function boundedBuffer(value: Buffer, maxBytes: number): Buffer {
return value.byteLength <= maxBytes ? value : Buffer.from(value.subarray(0, maxBytes));
}
@@ -577,7 +1066,7 @@ function plainHeaderRecord(value: Headers | Record<string, string | string[] | u
: Object.entries(value).filter((entry): entry is [string, string | string[]] => entry[1] !== undefined);
return Object.fromEntries(entries.map(([key, headerValue]) => [
key,
sensitiveQueueHeaderNames.has(key.toLowerCase()) ? "[redacted]" : headerValue
isSensitiveRequestLogHeaderName(key) ? "[redacted]" : headerValue
]));
}
@@ -609,7 +1098,13 @@ function jsonBytes(value: unknown): number {
}
function withoutSize(command: QueuedCommand): RequestLogStoreWriteCommand {
const { batchBytes: _batchBytes, sizeBytes: _sizeBytes, ...output } = command;
const {
batchBytes: _batchBytes,
isolated: _isolated,
sizeBytes: _sizeBytes,
writeAttempts: _writeAttempts,
...output
} = command;
return output;
}
@@ -617,6 +1112,10 @@ function positiveInteger(value: number | undefined, fallback: number): number {
return Number.isFinite(value) && Number(value) > 0 ? Math.floor(Number(value)) : fallback;
}
function nonNegativeInteger(value: number | undefined): number {
return Number.isFinite(value) ? Math.max(0, Math.floor(Number(value))) : 0;
}
function rpc<T>(
worker: Worker,
requests: Map<number, PendingRpc>,
@@ -651,6 +1150,12 @@ function errorCode(error: unknown): string | undefined {
: undefined;
}
function isTransientSqliteLock(error: unknown): boolean {
const code = errorCode(error);
return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED" ||
(error instanceof Error && /database is (?:busy|locked)/i.test(error.message));
}
function formatRuntimeError(error: unknown): string {
return error instanceof Error ? error.stack || error.message : String(error);
}
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ import {
} from "@ccr/core/observability/request-log-store";
import { resolveRawTraceBodyLimit } from "@ccr/core/observability/request-log-limits";
import { compactBase64ImagePayloads } from "@ccr/core/observability/request-log-body";
import { preloadUsagePriceCatalog } from "@ccr/core/models/pricing-service";
type WorkerConfiguration = {
dbFile: string;
@@ -31,7 +32,10 @@ type WorkerMessage = {
const configuration = workerData as WorkerConfiguration;
const store = new RequestLogStore(configuration.dbFile);
let chain = Promise.resolve();
let pricingBackfillActive = false;
let pricingRefreshPromise: Promise<void> | undefined;
let queryRevision = -1;
let shuttingDown = false;
void store.initialize().then(() => {
parentPort?.postMessage({ type: "ready" });
@@ -53,8 +57,9 @@ async function handleMessage(message: WorkerMessage): Promise<void> {
if (message.type === "batch") {
if (configuration.mode !== "writer") throw new Error("Query worker cannot process writes.");
const commands = message.commands ?? [];
await store.writeBatch(commands.map(reviveCommand));
const result = await store.writeBatch(commands.map(reviveCommand));
parentPort?.postMessage({ batchId: message.batchId, type: "ack" });
if (result.pricingRefreshNeeded) schedulePricingRefresh();
return;
}
@@ -82,6 +87,7 @@ async function handleMessage(message: WorkerMessage): Promise<void> {
result = await store.list(args[0] as Parameters<RequestLogStore["list"]>[0]);
break;
case "shutdown":
shuttingDown = true;
await store.close();
result = true;
break;
@@ -92,6 +98,44 @@ async function handleMessage(message: WorkerMessage): Promise<void> {
if (message.method === "shutdown") parentPort?.close();
}
function schedulePricingRefresh(): void {
if (shuttingDown || pricingRefreshPromise) return;
pricingRefreshPromise = preloadUsagePriceCatalog()
.then(() => {
if (shuttingDown) return;
schedulePricingBackfillPage();
})
.catch((error) => {
console.warn(`[request-log] Failed to refresh pricing catalog: ${formatError(error)}`);
})
.finally(() => {
pricingRefreshPromise = undefined;
});
}
function schedulePricingBackfillPage(beforeId?: number): void {
if (shuttingDown || (pricingBackfillActive && beforeId === undefined)) return;
pricingBackfillActive = true;
chain = chain.then(async () => {
if (shuttingDown) {
pricingBackfillActive = false;
return;
}
const page = await store.backfillMissingUsageCostsPage({ beforeId });
if (page.updated > 0) parentPort?.postMessage({ type: "maintenance", updated: page.updated });
if (page.nextBeforeId === undefined) {
pricingBackfillActive = false;
return;
}
// Yield between pages so normal log writes already queued by the parent can
// run before the next maintenance batch.
setImmediate(() => schedulePricingBackfillPage(page.nextBeforeId));
}).catch((error) => {
pricingBackfillActive = false;
console.warn(`[request-log] Failed to backfill usage costs: ${formatError(error)}`);
});
}
function reviveCommand(command: RequestLogStoreWriteCommand): RequestLogStoreWriteCommand {
if (command.kind === "raw-trace-update") {
const input = { ...command.input };
+35 -6
View File
@@ -7,6 +7,7 @@ import type {
RequestRouteTracePhase,
RequestRouteTraceTarget
} from "@ccr/core/contracts/app";
import { isSensitiveRequestLogHeaderName } from "@ccr/core/observability/sensitive-headers";
export type RouteTraceObservation = {
attempt?: number;
@@ -115,15 +116,18 @@ export class RequestRouteTraceRecorder implements RouteTraceObserver {
this.pushHop(hop);
}
finish(): RequestRouteTrace {
finish(options: { captureBodyValues?: boolean } = {}): RequestRouteTrace {
if (this.finished) {
return this.finished;
}
const hops = options.captureBodyValues === false
? this.hops.map(suppressRouteTraceHopBodyValues)
: this.hops;
this.finished = {
attemptCount: this.attempts.size,
complete: true,
hopCount: this.hops.length,
hops: this.hops,
hopCount: hops.length,
hops,
truncated: this.truncated,
version: 2
};
@@ -172,10 +176,31 @@ export class RequestRouteTraceRecorder implements RouteTraceObserver {
}
}
export function suppressRouteTraceBodyValues(trace: RequestRouteTrace): RequestRouteTrace {
const hops = trace.hops.map(suppressRouteTraceHopBodyValues);
return hops.every((hop, index) => hop === trace.hops[index])
? trace
: { ...trace, hops };
}
function boundedObservationValue<T extends object>(value: T): T {
return previewValue(value).value as T;
}
function suppressRouteTraceHopBodyValues(hop: RequestRouteTraceHop): RequestRouteTraceHop {
if (!hop.changes.some((change) => change.scope === "body")) {
return hop;
}
return {
...hop,
changes: hop.changes.map((change) => {
if (change.scope !== "body") return change;
const { after: _after, before: _before, ...metadata } = change;
return metadata;
})
};
}
function sanitizeReportedChange(change: RequestRouteTraceChange): RequestRouteTraceChange {
const path = normalizePath(change.path);
const redacted = Boolean(change.redacted) || pathContainsSensitiveName(path);
@@ -207,7 +232,7 @@ function sanitizeUrlValue(value: unknown): unknown {
try {
const url = new URL(value, "http://127.0.0.1");
for (const key of [...url.searchParams.keys()]) {
if (sensitiveNames.test(key) || /^(?:key|token)$/i.test(key)) {
if (isSensitiveName(key) || /^(?:key|token)$/i.test(key)) {
url.searchParams.set(key, redactedDisplayValue);
}
}
@@ -239,7 +264,7 @@ function boundedPreview(
budget.truncated = true;
return truncatedDisplayValue;
}
if (key && sensitiveNames.test(key)) {
if (key && isSensitiveName(key)) {
budget.remaining -= redactedDisplayValue.length;
return redactedDisplayValue;
}
@@ -323,7 +348,11 @@ function pathContainsSensitiveName(path: string): boolean {
.split("/")
.filter(Boolean)
.map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"))
.some((part) => sensitiveNames.test(part));
.some(isSensitiveName);
}
function isSensitiveName(value: string): boolean {
return sensitiveNames.test(value) || isSensitiveRequestLogHeaderName(value);
}
function normalizePath(value: string): string {
@@ -0,0 +1,23 @@
export const sensitiveRequestLogHeaderNames: ReadonlySet<string> = new Set([
"api-key",
"authorization",
"cookie",
"ocp-apim-subscription-key",
"proxy-authorization",
"set-cookie",
"x-api-key",
"x-auth-api-key-id",
"x-auth-sub",
"x-goog-api-key"
]);
// Provider plugins may use arbitrary authentication header names. Match
// security-bearing name segments in addition to the compatibility allowlist
// so a newly introduced x-*-token/secret/key header is fail-closed by default.
const sensitiveRequestLogHeaderPattern =
/(?:^|[-_.])(?:auth(?:orization)?|bearer|cookie|credential|csrf|jwt|key|pass(?:word|wd)?|secret|signature|token)(?:$|[-_.])/i;
export function isSensitiveRequestLogHeaderName(value: string): boolean {
const normalized = value.trim().toLowerCase();
return sensitiveRequestLogHeaderNames.has(normalized) || sensitiveRequestLogHeaderPattern.test(normalized);
}
+12 -3
View File
@@ -6,15 +6,24 @@ export type {
Statement as BetterSqliteStatement
} from "better-sqlite3";
export type BetterSqliteDatabaseOptions = {
fileMustExist?: boolean;
readonly?: boolean;
timeout?: number;
};
const requireFromHere = createRequire(__filename);
let resolvedNativeBinding: string | undefined;
let nativeBindingResolved = false;
export function createBetterSqliteDatabase(filename: string): BetterSqliteDatabase {
export function createBetterSqliteDatabase(
filename: string,
options: BetterSqliteDatabaseOptions = {}
): BetterSqliteDatabase {
const nativeBinding = resolveBetterSqliteNativeBinding();
return nativeBinding
? new DatabaseConstructor(filename, { nativeBinding })
: new DatabaseConstructor(filename);
? new DatabaseConstructor(filename, { ...options, nativeBinding })
: new DatabaseConstructor(filename, options);
}
function resolveBetterSqliteNativeBinding(): string | undefined {
@@ -0,0 +1,136 @@
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { preloadUsagePriceCatalog } from "@ccr/core/models/pricing-service.ts";
import { RequestLogStore } from "@ccr/core/observability/request-log-store.ts";
import { createBetterSqliteDatabase } from "@ccr/core/storage/sqlite-native.ts";
test("RequestLogStore commits while a background pricing refresh is stalled", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-pricing-lock-test-"));
const dbFile = path.join(dir, "request-logs.sqlite");
const first = new RequestLogStore(dbFile);
const second = new RequestLogStore(dbFile);
const previousFetch = globalThis.fetch;
let releaseFetch;
let signalFetchStarted;
const fetchGate = new Promise((resolve) => {
releaseFetch = resolve;
});
const fetchStarted = new Promise((resolve) => {
signalFetchStarted = resolve;
});
globalThis.fetch = async () => {
signalFetchStarted();
await fetchGate;
return new Response(JSON.stringify({
"pricing-lock-model": {
input_cost_per_token: 0.000001,
litellm_provider: "pricing-lock-provider",
output_cost_per_token: 0.000002
}
}), {
headers: { "content-type": "application/json" },
status: 200
});
};
let pricingRefresh;
let fetchStartTimeout;
try {
pricingRefresh = preloadUsagePriceCatalog();
await Promise.race([
fetchStarted,
new Promise((_, reject) => {
fetchStartTimeout = setTimeout(() => reject(new Error("pricing fetch did not start")), 5_000);
})
]);
clearTimeout(fetchStartTimeout);
fetchStartTimeout = undefined;
const pricedWrite = first.writeBatch([{
eventId: "priced-event",
input: createRecord("priced-request", '{"usage":{"input_tokens":1,"output_tokens":1}}'),
kind: "record",
sequence: 1
}]);
const pricedResult = await Promise.race([
pricedWrite,
new Promise((_, reject) => {
fetchStartTimeout = setTimeout(() => reject(new Error("request log write waited for pricing")), 1_000);
})
]);
assert.equal(pricedResult.pricingRefreshNeeded, true);
clearTimeout(fetchStartTimeout);
fetchStartTimeout = undefined;
// The pricing request is deliberately stalled. Both writers can commit,
// and cost can be filled after the shared catalog refresh completes.
const zeroTokenResult = await second.writeBatch([{
eventId: "zero-token-event",
input: createRecord("zero-token-request", "{}"),
kind: "record",
sequence: 2
}]);
assert.equal(zeroTokenResult.pricingRefreshNeeded, false);
const page = await first.list({ pageSize: 25 });
assert.equal(page.items.length, 2);
releaseFetch();
await pricingRefresh;
// Fill the newest page with models that cannot be priced. The older known
// model must still be reached by pagination.
const database = createBetterSqliteDatabase(dbFile);
try {
const insert = database.prepare(`
INSERT INTO request_logs (
created_at,
method,
path,
model,
provider,
input_tokens,
total_tokens
) VALUES (?, 'POST', '/v1/messages', 'unknown-price-model', 'unknown-provider', 1, 1)
`);
database.transaction(() => {
for (let index = 0; index < 1_001; index += 1) {
insert.run(new Date().toISOString());
}
})();
} finally {
database.close();
}
assert.equal(await first.backfillMissingUsageCosts(), 1);
const updated = await first.getDetail({ id: page.items.find((item) => item.requestId === "priced-request").id });
assert.equal(updated.costUsd, 0.000003);
} finally {
clearTimeout(fetchStartTimeout);
releaseFetch?.();
await pricingRefresh?.catch(() => undefined);
globalThis.fetch = previousFetch;
await Promise.all([first.close(), second.close()]);
rmSync(dir, { force: true, recursive: true });
}
});
function createRecord(requestId, responseBodyText) {
const now = new Date().toISOString();
return {
completedAt: now,
durationMs: 10,
method: "POST",
model: "pricing-lock-model",
path: "/v1/messages",
providerName: "pricing-lock-provider",
requestBody: Buffer.from('{"model":"pricing-lock-model"}'),
requestHeaders: { "content-type": "application/json" },
requestId,
responseBodyText,
responseHeaders: { "content-type": "application/json" },
startedAt: now,
statusCode: 200,
url: "http://127.0.0.1:3456/v1/messages"
};
}
File diff suppressed because it is too large Load Diff
@@ -13,6 +13,104 @@ import { createBetterSqliteDatabase } from "@ccr/core/storage/sqlite-native.ts";
const execFileAsync = promisify(execFile);
const isBoundedHeapWorker = process.env.CCR_REQUEST_LOG_BOUNDED_HEAP_WORKER === "1";
test("RequestLogStore resumes interrupted gateway migrations across bounded batches", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-paged-migration-test-"));
const dbFile = path.join(dir, "request-logs.sqlite");
let store;
try {
const legacy = createBetterSqliteDatabase(dbFile);
try {
legacy.exec(`
CREATE TABLE request_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL,
event_id TEXT NOT NULL DEFAULT '',
status_code INTEGER NOT NULL DEFAULT 0,
ok INTEGER NOT NULL DEFAULT 0,
error TEXT NOT NULL DEFAULT '',
gateway_status_code INTEGER NOT NULL DEFAULT 0,
gateway_ok INTEGER NOT NULL DEFAULT 0,
gateway_error TEXT NOT NULL DEFAULT '',
gateway_final_attempt INTEGER NOT NULL DEFAULT 1,
response_headers TEXT NOT NULL DEFAULT '{}'
);
CREATE TABLE request_log_schema_migrations (
migration TEXT PRIMARY KEY,
last_id INTEGER NOT NULL DEFAULT 0,
completed INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
`);
const insert = legacy.prepare(`
INSERT INTO request_logs (created_at, event_id, status_code, ok, error, response_headers)
VALUES (?, ?, 503, 0, 'legacy failure', ?)
`);
const createdAt = new Date().toISOString();
legacy.transaction(() => {
for (let index = 0; index < 1_205; index += 1) {
insert.run(
createdAt,
`legacy-event-${index}`,
JSON.stringify({ "x-ccr-fallback-attempts": String((index % 4) + 1) })
);
}
})();
legacy.exec(`
UPDATE request_logs
SET gateway_final_attempt = ((id - 1) % 4) + 1
WHERE id <= 500;
INSERT INTO request_log_schema_migrations (migration, last_id, completed, updated_at)
VALUES ('gateway-final-attempt-v1', 500, 0, 1);
`);
} finally {
legacy.close();
}
store = new RequestLogStore(dbFile);
await store.list({ pageSize: 1 });
const migrated = createBetterSqliteDatabase(dbFile);
try {
const rows = migrated.prepare(`
SELECT gateway_final_attempt AS attempt, COUNT(*) AS total
FROM request_logs
GROUP BY gateway_final_attempt
ORDER BY gateway_final_attempt
`).all();
assert.deepEqual(rows.map((row) => [row.attempt, row.total]), [
[1, 302],
[2, 301],
[3, 301],
[4, 301]
]);
const outcome = migrated.prepare(`
SELECT COUNT(*) AS total
FROM request_logs
WHERE gateway_status_code = 503
AND gateway_ok = 0
AND gateway_error = 'legacy failure'
`).get();
assert.equal(outcome.total, 1_205);
const migrations = migrated.prepare(`
SELECT migration, completed
FROM request_log_schema_migrations
ORDER BY migration
`).all();
assert.deepEqual(migrations, [
{ completed: 1, migration: "gateway-final-attempt-v1" },
{ completed: 1, migration: "gateway-outcome-v1" }
]);
const indexes = migrated.prepare("PRAGMA index_list(request_logs)").all();
assert.equal(indexes.some((index) => index.name === "request_logs_request_id_idx"), true);
} finally {
migrated.close();
}
} finally {
await store?.close();
rmSync(dir, { force: true, recursive: true });
}
});
async function recordLargeAgentRequests(store, dbFile, { paddingBytes, requestCount, sessionId }) {
const padding = "x".repeat(paddingBytes);
const startedAt = new Date().toISOString();
@@ -232,17 +330,23 @@ test("RequestLogStore redacts secrets and records CCR metadata", async () => {
requestBody: Buffer.from(JSON.stringify({ model: "gpt-test", stream: true }), "utf8"),
requestHeaders: {
accept: "text/event-stream",
"api-key": "azure-request-secret",
authorization: "Bearer request-secret",
cookie: "session=request-secret",
"content-type": "application/json",
"ocp-apim-subscription-key": "bing-request-secret",
"x-amz-security-token": "aws-request-secret",
"x-auth-token": "custom-request-secret",
"x-ccr-provider-credential-chain": "cred-a, cred-b",
"x-ccr-provider-credential-id": "cred-a"
"x-ccr-provider-credential-id": "cred-a",
"x-goog-api-key": "google-request-secret"
},
requestId: "request-log-metadata-test",
responseBodyText: "",
responseHeaders: {
"content-type": "text/event-stream",
"x-api-key": "response-secret",
"x-company-client-secret": "custom-response-secret",
"x-ccr-provider-credential-saturated": "true",
"x-gateway-billing-cache-read-tokens": "10",
"x-gateway-billing-input-tokens": "100",
@@ -258,9 +362,15 @@ test("RequestLogStore redacts secrets and records CCR metadata", async () => {
const detail = await store.getDetail({ id: page.items[0].id });
assert.ok(detail);
assert.equal(detail.requestHeaders["api-key"], "[redacted]");
assert.equal(detail.requestHeaders.authorization, "[redacted]");
assert.equal(detail.requestHeaders.cookie, "[redacted]");
assert.equal(detail.requestHeaders["ocp-apim-subscription-key"], "[redacted]");
assert.equal(detail.requestHeaders["x-amz-security-token"], "[redacted]");
assert.equal(detail.requestHeaders["x-auth-token"], "[redacted]");
assert.equal(detail.requestHeaders["x-goog-api-key"], "[redacted]");
assert.equal(detail.responseHeaders["x-api-key"], "[redacted]");
assert.equal(detail.responseHeaders["x-company-client-secret"], "[redacted]");
assert.equal(detail.credentialId, "cred-a");
assert.deepEqual(detail.credentialChain, ["cred-a", "cred-b"]);
assert.equal(detail.credentialSaturated, true);
@@ -313,6 +423,257 @@ test("RequestLogStore marks interrupted successful-status streams as errors", as
}
});
test("RequestLogStore keeps an authoritative gateway failure when a later raw trace reports HTTP 200", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-raw-authority-test-"));
const store = new RequestLogStore(path.join(dir, "request-logs.sqlite"));
const startedAt = new Date().toISOString();
try {
await store.record({
completedAt: startedAt,
durationMs: 25,
error: "Client connection closed before response completed.",
method: "POST",
path: "/v1/messages",
providerName: "gateway-provider",
requestBody: Buffer.from('{"model":"gateway-model","stream":true}'),
requestHeaders: { "content-type": "application/json" },
requestId: "gateway-failure-before-raw",
responseBodyText: "gateway-captured-error-body",
responseHeaders: { "content-type": "text/event-stream" },
startedAt,
statusCode: 499,
url: "http://127.0.0.1:3456/v1/messages"
});
await store.updateFromRawTrace({
bodyCapturePolicy: "errors",
isStream: true,
requestId: "gateway-failure-before-raw",
responseBodySizeBytes: 128,
responseBodyText: "",
responseBodyTruncated: true,
statusCode: 200
});
const page = await store.list({ pageSize: 25 });
const detail = await store.getDetail({ id: page.items[0].id });
assert.equal(detail.statusCode, 499);
assert.equal(detail.ok, false);
assert.equal(detail.error, "Client connection closed before response completed.");
assert.equal(detail.responseBody.text, "gateway-captured-error-body");
} finally {
await store.close();
rmSync(dir, { force: true, recursive: true });
}
});
test("RequestLogStore keeps an errorless gateway HTTP 500 authoritative over raw HTTP 200", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-status-authority-test-"));
const store = new RequestLogStore(path.join(dir, "request-logs.sqlite"));
const startedAt = new Date().toISOString();
try {
await store.record({
completedAt: startedAt,
durationMs: 25,
method: "POST",
path: "/v1/messages",
providerName: "gateway-provider",
requestBody: Buffer.from('{"model":"gateway-model"}'),
requestHeaders: { "content-type": "application/json" },
requestId: "gateway-status-failure-before-raw",
responseBodyText: '{"type":"gateway_failure"}',
responseHeaders: { "content-type": "application/json" },
startedAt,
statusCode: 500,
url: "http://127.0.0.1:3456/v1/messages"
});
await store.updateFromRawTrace({
requestId: "gateway-status-failure-before-raw",
responseBodyText: '{"type":"upstream_success"}',
statusCode: 200
});
const page = await store.list({ pageSize: 25 });
const detail = await store.getDetail({ id: page.items[0].id });
assert.equal(detail.statusCode, 500);
assert.equal(detail.ok, false);
} finally {
await store.close();
rmSync(dir, { force: true, recursive: true });
}
});
test("RequestLogStore consumes fallback bundles by unique bundle id and only final attempt mutates outcome", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-fallback-bundles-test-"));
const dbFile = path.join(dir, "request-logs.sqlite");
const store = new RequestLogStore(dbFile);
const startedAt = new Date().toISOString();
try {
for (const requestId of ["fallback-final-success", "fallback-final-failure"]) {
await store.record({
completedAt: startedAt,
durationMs: 25,
method: "POST",
path: "/v1/messages",
providerName: "gateway-provider",
requestBody: Buffer.from('{"model":"gateway-model"}'),
requestHeaders: { "content-type": "application/json" },
requestId,
responseBodyText: "gateway-body",
responseHeaders: {
"content-type": "application/json",
"x-ccr-fallback-attempts": "2"
},
startedAt,
statusCode: 200,
url: "http://127.0.0.1:3456/v1/messages"
});
}
await store.writeBatch([
{
input: {
attempt: 2,
bundleId: "success-final-bundle",
requestId: "fallback-final-success",
responseBodyText: "final-success-body",
statusCode: 200
},
kind: "raw-trace-update",
sequence: 1
},
{
input: {
attempt: 1,
bundleId: "success-intermediate-bundle",
requestId: "fallback-final-success",
responseBodyText: "intermediate-failure-body",
statusCode: 500
},
kind: "raw-trace-update",
sequence: 2
},
{
input: {
attempt: 2,
bundleId: "failure-final-bundle",
requestId: "fallback-final-failure",
responseBodyText: "final-failure-body",
statusCode: 502
},
kind: "raw-trace-update",
sequence: 3
},
{
input: {
attempt: 1,
bundleId: "failure-intermediate-bundle",
requestId: "fallback-final-failure",
responseBodyText: "intermediate-success-body",
statusCode: 200
},
kind: "raw-trace-update",
sequence: 4
},
{
input: {
attempt: 2,
bundleId: "success-final-bundle",
requestId: "fallback-final-success",
responseBodyText: "duplicate-must-not-apply",
statusCode: 503
},
kind: "raw-trace-update",
sequence: 5
}
]);
const page = await store.list({ pageSize: 25 });
const success = await store.getDetail({
id: page.items.find((item) => item.requestId === "fallback-final-success").id
});
const failure = await store.getDetail({
id: page.items.find((item) => item.requestId === "fallback-final-failure").id
});
assert.equal(success.statusCode, 200);
assert.equal(success.ok, true);
assert.equal(success.responseBody.text, "final-success-body");
assert.equal(failure.statusCode, 502);
assert.equal(failure.ok, false);
assert.equal(failure.responseBody.text, "final-failure-body");
const database = createBetterSqliteDatabase(dbFile);
try {
const count = database.prepare("SELECT COUNT(*) AS total FROM request_log_raw_trace_events").get();
assert.equal(Number(count.total), 4);
} finally {
database.close();
}
} finally {
await store.close();
rmSync(dir, { force: true, recursive: true });
}
});
test("RequestLogStore detects raw errors before applying errors-only body suppression", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-raw-error-policy-test-"));
const store = new RequestLogStore(path.join(dir, "request-logs.sqlite"));
const startedAt = new Date().toISOString();
try {
for (const requestId of ["raw-http-error", "raw-sse-error"]) {
await store.record({
completedAt: startedAt,
durationMs: 25,
method: "POST",
path: "/v1/messages",
providerName: "gateway-provider",
requestBody: Buffer.from('{"model":"gateway-model"}'),
requestHeaders: { "content-type": "application/json" },
requestId,
responseBodyText: "gateway-success-body",
responseHeaders: { "content-type": "application/json" },
startedAt,
statusCode: 200,
url: "http://127.0.0.1:3456/v1/messages"
});
}
await store.updateFromRawTrace({
bodyCapturePolicy: "errors",
requestId: "raw-http-error",
responseBodyContentType: "application/json",
responseBodyText: '{"error":{"message":"upstream failed"}}',
responseHeaders: { "content-type": "application/json" },
statusCode: 500
});
await store.updateFromRawTrace({
bodyCapturePolicy: "errors",
isStream: true,
requestId: "raw-sse-error",
responseBodyContentType: "text/event-stream",
responseBodyText: 'event: error\ndata: {"error":{"message":"late failure"}}\n\n',
responseHeaders: { "content-type": "text/event-stream" },
statusCode: 200
});
const page = await store.list({ pageSize: 25 });
const httpEntry = page.items.find((item) => item.requestId === "raw-http-error");
const sseEntry = page.items.find((item) => item.requestId === "raw-sse-error");
const httpDetail = await store.getDetail({ id: httpEntry.id });
const sseDetail = await store.getDetail({ id: sseEntry.id });
assert.equal(httpDetail.ok, false);
assert.equal(httpDetail.statusCode, 500);
assert.match(httpDetail.responseBody.text, /upstream failed/);
assert.equal(sseDetail.ok, false);
assert.match(sseDetail.error, /late failure/);
assert.match(sseDetail.responseBody.text, /late failure/);
} finally {
await store.close();
rmSync(dir, { force: true, recursive: true });
}
});
test("RequestLogStore applies raw trace updates to existing request logs", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-raw-trace-test-"));
try {
@@ -344,7 +705,13 @@ test("RequestLogStore applies raw trace updates to existing request logs", async
isStream: true,
model: "trace-model",
provider: "trace-provider",
requestHeaders: { "x-client-name": "codex-cli" },
requestHeaders: {
"api-key": "raw-azure-secret",
"ocp-apim-subscription-key": "raw-bing-secret",
"x-ccr-provider-credential-id": "raw-credential-id",
"x-client-name": "codex-cli",
"x-goog-api-key": "raw-google-secret"
},
requestId: "raw-trace-request",
responseBodyContentType: "text/event-stream",
responseBodyText: errorStream,
@@ -360,11 +727,16 @@ test("RequestLogStore applies raw trace updates to existing request logs", async
assert.ok(detail);
assert.equal(detail.model, "trace-model");
assert.equal(detail.provider, "trace-provider");
assert.equal(detail.credentialId, "raw-credential-id");
assert.equal(detail.ok, false);
assert.equal(detail.isStream, true);
assert.match(detail.error, /rate_limit_error: quota exceeded/);
assert.match(detail.responseBody?.text ?? "", /quota exceeded/);
assert.equal(detail.requestHeaders["api-key"], "[redacted]");
assert.equal(detail.requestHeaders["ocp-apim-subscription-key"], "[redacted]");
assert.equal(detail.requestHeaders["x-ccr-provider-credential-id"], "[redacted]");
assert.equal(detail.requestHeaders["x-client-name"], "codex-cli");
assert.equal(detail.requestHeaders["x-goog-api-key"], "[redacted]");
} finally {
rmSync(dir, { force: true, recursive: true });
}
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import path from "node:path";
import test from "node:test";
import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts";
import {
@@ -6,9 +7,16 @@ import {
BROWSER_AUTOMATION_MCP_PATH,
BROWSER_AUTOMATION_MCP_SERVER_NAME,
browserAutomationMcpEnabled,
bundledToolHubMcpEntryPathCandidates,
toolHubMcpRuntimeConfig
} from "@ccr/core/mcp/toolhub-config.ts";
test("ToolHub runtime candidates include the clean Core test build", () => {
assert.ok(bundledToolHubMcpEntryPathCandidates().includes(
path.join(process.cwd(), ".test-dist", "core", "runtime", "toolhub-mcp.js")
));
});
test("ToolHub runtime includes the built-in browser automation backend", () => {
const config = createDefaultAppConfig({ generatedConfigFile: "/tmp/ccr-gateway.config.json" });
config.toolHub = {
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { Readable } from "node:stream";
@@ -11,6 +11,7 @@ import {
applyRawTraceRequestLogPolicy,
buildRawTraceConfig,
createBodySampler,
readRawTraceRequestLogBundle,
RawTraceSynchronizer
} from "@ccr/core/observability/raw-trace-sync.ts";
@@ -26,7 +27,7 @@ test("raw trace applies metadata-only body privacy while retaining original size
});
assert.equal(policy.action, "enqueue");
assert.equal(policy.captureBodies, false);
assert.equal(policy.bodyDisposition, "suppress");
assert.equal(policy.update.requestBodyText, "");
assert.equal(policy.update.requestBodySizeBytes, 1_024);
assert.equal(policy.update.requestBodyTruncated, true);
@@ -35,15 +36,17 @@ test("raw trace applies metadata-only body privacy while retaining original size
assert.equal(policy.update.responseBodyTruncated, true);
});
test("raw trace uses the same successful-request sampling policy as normal request logs", () => {
test("raw trace defers successful-request sampling to the final request admission", () => {
const config = createConfig();
config.observability.requestLogSuccessSampleRate = 0;
assert.deepEqual(applyRawTraceRequestLogPolicy(config, {
const provisional = applyRawTraceRequestLogPolicy(config, {
requestBodySizeBytes: 128,
requestId: "sampled-request",
statusCode: 200
}), { action: "discard", reason: "sampled" });
});
assert.equal(provisional.action, "enqueue");
assert.equal(provisional.update.deferOutcomeUntilRecord, true);
const errorPolicy = applyRawTraceRequestLogPolicy(config, {
requestBodySizeBytes: 128,
@@ -51,9 +54,29 @@ test("raw trace uses the same successful-request sampling policy as normal reque
statusCode: 429
});
assert.equal(errorPolicy.action, "enqueue");
assert.equal(errorPolicy.update.deferOutcomeUntilRecord, true);
});
test("raw trace captures complete error bodies but suppresses successful bodies in errors-only mode", () => {
test("raw trace defers HTTP 200 stream sampling and errors-only body policy", () => {
const config = createConfig();
config.observability.requestLogBodyCapture = "errors";
config.observability.requestLogSuccessSampleRate = 0;
const policy = applyRawTraceRequestLogPolicy(config, {
isStream: true,
requestId: "stream-with-late-error",
responseBodyText: "event: error\ndata: {\"error\":\"late\"}\n\n",
statusCode: 200
});
assert.equal(policy.action, "enqueue");
assert.equal(policy.bodyDisposition, "defer");
assert.equal(policy.update.deferBodyCaptureUntilRecord, true);
assert.equal(policy.update.deferOutcomeUntilRecord, true);
assert.match(policy.update.responseBodyText, /late/);
});
test("raw trace captures known error bodies and defers provisional 2xx bodies in errors-only mode", () => {
const config = createConfig();
config.observability.requestLogBodyCapture = "errors";
@@ -63,7 +86,8 @@ test("raw trace captures complete error bodies but suppresses successful bodies
statusCode: 200
});
assert.equal(successful.action, "enqueue");
assert.equal(successful.captureBodies, false);
assert.equal(successful.bodyDisposition, "defer");
assert.equal(successful.update.deferOutcomeUntilRecord, true);
const failed = applyRawTraceRequestLogPolicy(config, {
requestBodySizeBytes: 64,
@@ -71,7 +95,25 @@ test("raw trace captures complete error bodies but suppresses successful bodies
statusCode: 500
});
assert.equal(failed.action, "enqueue");
assert.equal(failed.captureBodies, true);
assert.equal(failed.bodyDisposition, "capture");
});
test("raw trace defers body persistence when the upstream status is unknown", () => {
const config = createConfig();
config.observability.requestLogBodyCapture = "errors";
config.observability.requestLogSuccessSampleRate = 0;
const policy = applyRawTraceRequestLogPolicy(config, {
requestBodySizeBytes: 64,
requestBodyText: "private request body",
requestId: "network-failure-without-status"
});
assert.equal(policy.action, "enqueue");
assert.equal(policy.bodyDisposition, "defer");
assert.equal(policy.update.bodyCapturePolicy, "errors");
assert.equal(policy.update.deferBodyCaptureUntilRecord, true);
assert.equal(policy.update.requestBodyText, "private request body");
});
test("raw trace source defaults to the 50 MB hard body ceiling", () => {
@@ -115,15 +157,19 @@ test("stream sampler retains the original response byte size after capture trunc
assert.equal(sampler.sizeBytes(), body.byteLength);
});
test("raw trace sync retains its bundle when the asynchronous queue rejects it", async () => {
test("raw trace sync acknowledges only after durable inbox ownership and retains queue rejections", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-sync-reject-test-"));
const spoolDirectory = path.join(dir, "spool");
const bundleDirectory = path.join(spoolDirectory, "bundle");
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"keep me"}');
let enqueueCalls = 0;
const synchronizer = new RawTraceSynchronizer({
enqueueUpdate: async () => false,
enqueueUpdate: async () => {
enqueueCalls += 1;
return false;
},
getConfig: createConfig,
spoolDirectory
});
@@ -134,6 +180,7 @@ test("raw trace sync retains its bundle when the asynchronous queue rejects it",
originalBytes: 21,
partType: "upstream_request"
}],
requestId: "core-bundle-queue-rejected",
turnKey: "queue-rejected-request"
})]);
request.method = "POST";
@@ -150,9 +197,678 @@ test("raw trace sync retains its bundle when the asynchronous queue rejects it",
try {
await synchronizer.handle(request, response);
assert.equal(result.statusCode, 503);
assert.equal(result.body.accepted, false);
assert.equal(result.statusCode, 202);
assert.equal(result.body.accepted, true);
assert.equal(result.body.durable, true);
assert.equal(result.body.bundleId, "core-bundle-queue-rejected");
assert.equal(existsSync(bundleDirectory), false);
const inbox = path.join(spoolDirectory, ".ccr-inbox");
assert.equal(readdirSync(inbox).length, 1);
assert.equal(existsSync(path.join(inbox, readdirSync(inbox)[0], "upstream_request.json")), true);
await waitFor(() => enqueueCalls === 1);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace sync acknowledges and cleans bundles for terminally dropped records", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-sync-record-dropped-test-"));
const spoolDirectory = path.join(dir, "spool");
const bundleDirectory = path.join(spoolDirectory, "bundle");
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"discard me"}');
const synchronizer = new RawTraceSynchronizer({
enqueueUpdate: async () => ({
accepted: false,
degraded: false,
reason: "record_dropped"
}),
getConfig: createConfig,
spoolDirectory
});
const request = Readable.from([JSON.stringify({
parts: [{
contentType: "application/json",
filePath: bodyFile,
originalBytes: 24,
partType: "upstream_request"
}],
requestId: "core-bundle-terminal-drop",
turnKey: "terminally-dropped-request"
})]);
request.method = "POST";
request.headers = { [rawTraceSyncHeader]: synchronizer.token };
const result = {};
const response = {
end(body) {
result.body = JSON.parse(body);
},
writeHead(statusCode) {
result.statusCode = statusCode;
}
};
try {
await synchronizer.handle(request, response);
assert.equal(result.statusCode, 202);
assert.equal(result.body.durable, true);
await waitFor(() => readdirSync(path.join(spoolDirectory, ".ccr-inbox")).length === 0);
assert.equal(existsSync(bundleDirectory), false);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace startup replay outlives producer retries and applies a pending bundle later", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-replay-test-"));
const spoolDirectory = path.join(dir, "spool");
const bundleDirectory = path.join(spoolDirectory, "producer-bundle");
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"replay me"}');
writeFileSync(path.join(bundleDirectory, "manifest.json"), JSON.stringify({
parts: [{
contentType: "application/json",
filePath: bodyFile,
originalBytes: 23,
partType: "upstream_request"
}],
requestId: "core-bundle-replay",
turnKey: "logical-replay-request",
uploadedAt: new Date().toISOString()
}));
let attempts = 0;
const synchronizer = new RawTraceSynchronizer({
enqueueUpdate: async (_update, files) => {
attempts += 1;
if (attempts === 1) {
return { accepted: false, degraded: false, reason: "record_pending" };
}
rmSync(files.cleanupDirectory, { force: true, recursive: true });
return { accepted: true, degraded: false };
},
getConfig: createConfig,
replayIntervalMs: 10,
retryCooldownMs: 10,
spoolDirectory
});
try {
await synchronizer.start();
await waitFor(() => attempts >= 2 &&
readdirSync(path.join(spoolDirectory, ".ccr-inbox")).length === 0);
assert.equal(existsSync(bundleDirectory), false);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace replay recovers an inbox bundle after the accepting process crashes", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-crash-replay-test-"));
const spoolDirectory = path.join(dir, "spool");
const bundleDirectory = path.join(spoolDirectory, "producer-bundle");
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"survive ack"}');
const first = new RawTraceSynchronizer({
enqueueUpdate: async () => ({ accepted: true, degraded: false }),
getConfig: createConfig,
spoolDirectory
});
const request = Readable.from([JSON.stringify({
parts: [{ filePath: bodyFile, originalBytes: 25, partType: "upstream_request" }],
requestId: "core-bundle-crash",
turnKey: "logical-crash-request"
})]);
request.method = "POST";
request.headers = { [rawTraceSyncHeader]: first.token };
const response = { end() {}, writeHead() {} };
try {
await first.handle(request, response);
const inbox = path.join(spoolDirectory, ".ccr-inbox");
await waitFor(() => readdirSync(inbox).length === 1);
let replayed = 0;
const second = new RawTraceSynchronizer({
enqueueUpdate: async (_update, files) => {
replayed += 1;
rmSync(files.cleanupDirectory, { force: true, recursive: true });
return { accepted: true, degraded: false };
},
getConfig: createConfig,
replayIntervalMs: 10,
retryCooldownMs: 10,
spoolDirectory
});
await second.start();
await waitFor(() => replayed === 1 && readdirSync(inbox).length === 0);
await second.stop();
} finally {
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace durable ACK waits for every part fsync and refuses ACK when fsync fails", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-fsync-test-"));
const spoolDirectory = path.join(dir, "spool");
try {
for (const shouldFail of [false, true]) {
const bundleDirectory = path.join(spoolDirectory, `bundle-${shouldFail}`);
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"fsync me"}');
let partSynced = false;
let responseObservedSync = false;
const synchronizer = new RawTraceSynchronizer({
enqueueUpdate: async () => ({ accepted: false, degraded: false, reason: "record_dropped" }),
getConfig: createConfig,
spoolDirectory,
syncPartFile: async () => {
partSynced = true;
if (shouldFail) throw new Error("injected fsync failure");
}
});
const result = await sendRawTrace(synchronizer, {
parts: [{
filePath: bodyFile,
partType: "upstream_request",
storedBytes: Buffer.byteLength('{"message":"fsync me"}')
}],
requestId: `fsync-bundle-${shouldFail}`,
turnKey: `fsync-request-${shouldFail}`
}, () => {
responseObservedSync = partSynced;
});
assert.equal(partSynced, true);
assert.equal(responseObservedSync, true);
assert.equal(result.statusCode, shouldFail ? 503 : 202);
assert.equal(result.body.durable, shouldFail ? undefined : true);
await synchronizer.stop();
}
} finally {
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace publishes a fully durable staging directory atomically", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-atomic-publish-test-"));
const spoolDirectory = path.join(dir, "spool");
const bundleDirectory = path.join(spoolDirectory, "bundle");
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"atomic"}');
let releaseSync;
const syncGate = new Promise((resolve) => {
releaseSync = resolve;
});
let syncing = false;
const synchronizer = new RawTraceSynchronizer({
enqueueUpdate: async () => ({ accepted: false, degraded: false, reason: "record_pending" }),
getConfig: createConfig,
spoolDirectory,
syncPartFile: async () => {
syncing = true;
await syncGate;
}
});
try {
const upload = sendRawTrace(synchronizer, {
parts: [{ filePath: bodyFile, partType: "upstream_request" }],
requestId: "atomic-publish-bundle",
turnKey: "atomic-publish-request"
});
await waitFor(() => syncing);
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-inbox")).length, 0);
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-staging")).length, 1);
releaseSync();
const result = await upload;
assert.equal(result.statusCode, 202);
const published = readdirSync(path.join(spoolDirectory, ".ccr-inbox"));
assert.equal(published.length, 1);
assert.equal(existsSync(path.join(spoolDirectory, ".ccr-inbox", published[0], ".ccr-delivery.json")), true);
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-staging")).length, 0);
} finally {
releaseSync?.();
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace startup does not wait for backlog delivery", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-background-replay-test-"));
const spoolDirectory = path.join(dir, "spool");
const inboxDirectory = path.join(spoolDirectory, ".ccr-inbox");
const bundleDirectory = path.join(inboxDirectory, "backlog");
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"backlog"}');
writeFileSync(path.join(bundleDirectory, "manifest.json"), JSON.stringify({
parts: [{ filePath: bodyFile, partType: "upstream_request" }],
requestId: "background-replay-bundle",
turnKey: "background-replay-request"
}));
let releaseDelivery;
const deliveryGate = new Promise((resolve) => {
releaseDelivery = resolve;
});
let deliveryStarted = false;
const synchronizer = new RawTraceSynchronizer({
enqueueUpdate: async () => {
deliveryStarted = true;
await deliveryGate;
return { accepted: false, degraded: false, reason: "record_pending" };
},
getConfig: createConfig,
replayIntervalMs: 60_000,
spoolDirectory
});
try {
await synchronizer.start();
await waitFor(() => deliveryStarted);
assert.equal(deliveryStarted, true);
} finally {
releaseDelivery?.();
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace inbox and dead letters stay within configured capacity", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-capacity-test-"));
const spoolDirectory = path.join(dir, "spool");
const synchronizer = new RawTraceSynchronizer({
deadLetterMaxBundles: 1,
deadLetterMaxBytes: 1024 * 1024,
enqueueUpdate: async () => ({ accepted: false, degraded: false, reason: "record_pending" }),
getConfig: createConfig,
inboxMaxBundles: 1,
inboxMaxBytes: 1024 * 1024,
spoolDirectory
});
try {
for (let index = 1; index <= 3; index += 1) {
const bundleDirectory = path.join(spoolDirectory, `bundle-${index}`);
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, JSON.stringify({ index }));
await sendRawTrace(synchronizer, {
parts: [{ filePath: bodyFile, partType: "upstream_request" }],
requestId: `capacity-bundle-${index}`,
turnKey: `capacity-request-${index}`
});
await synchronizer.stop();
}
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-inbox")).length, 1);
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-dead-letter")).length, 1);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace isolates incomplete source bundles under the same bounded retention", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-incomplete-source-test-"));
const spoolDirectory = path.join(dir, "spool");
for (let index = 0; index < 3; index += 1) {
const bundleDirectory = path.join(spoolDirectory, `incomplete-${index}`);
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(path.join(bundleDirectory, "orphaned.part"), "x".repeat(128));
if (index === 1) writeFileSync(path.join(bundleDirectory, "manifest.json"), "{broken-json");
}
await new Promise((resolve) => setTimeout(resolve, 5));
const synchronizer = new RawTraceSynchronizer({
deadLetterMaxBundles: 1,
deadLetterMaxBytes: 1024 * 1024,
getConfig: createConfig,
inboxMaxBundles: 1,
inboxMaxBytes: 1024 * 1024,
replayIntervalMs: 5,
sourceBundleGraceMs: 1,
sourceScanIntervalMs: 1,
spoolDirectory
});
try {
await synchronizer.start();
await waitFor(() => readdirSync(spoolDirectory)
.filter((name) => ![".ccr-inbox", ".ccr-dead-letter", ".ccr-staging"].includes(name))
.length === 0);
const sourceEntries = readdirSync(spoolDirectory)
.filter((name) => ![".ccr-inbox", ".ccr-dead-letter", ".ccr-staging"].includes(name));
assert.deepEqual(sourceEntries, []);
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-inbox")).length, 0);
await waitFor(() => readdirSync(path.join(spoolDirectory, ".ccr-dead-letter")).length === 1);
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-dead-letter")).length, 1);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace keeps an over-capacity streaming source while its files are active", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-active-source-test-"));
const spoolDirectory = path.join(dir, "spool");
const bundleDirectory = path.join(spoolDirectory, "active-stream");
const bodyFile = path.join(bundleDirectory, "upstream_response.sse");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, "data: start\n\n");
const synchronizer = new RawTraceSynchronizer({
getConfig: createConfig,
inboxMaxBytes: 1,
replayIntervalMs: 5,
sourceBundleGraceMs: 30,
sourceScanIntervalMs: 5,
spoolDirectory
});
try {
await synchronizer.start();
for (let index = 0; index < 4; index += 1) {
await new Promise((resolve) => setTimeout(resolve, 15));
appendFileSync(bodyFile, `data: ${index}\n\n`);
}
assert.equal(existsSync(bundleDirectory), true);
await waitFor(() => !existsSync(bundleDirectory));
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-dead-letter")).length, 1);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace ACK measures only the newly admitted bundle after startup indexing", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-incremental-capacity-test-"));
const spoolDirectory = path.join(dir, "spool");
const inboxDirectory = path.join(spoolDirectory, ".ccr-inbox");
mkdirSync(inboxDirectory, { recursive: true });
for (let index = 0; index < 40; index += 1) {
const bundleDirectory = path.join(inboxDirectory, `backlog-${index}`);
const metadataFile = path.join(bundleDirectory, "upstream_response_metadata.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(metadataFile, JSON.stringify({ statusCode: 200 }));
writeFileSync(path.join(bundleDirectory, "manifest.json"), JSON.stringify({
parts: [{ filePath: metadataFile, partType: "upstream_response_metadata" }],
requestId: `backlog-bundle-${index}`,
turnKey: `backlog-request-${index}`
}));
}
const measuredDirectories = [];
const synchronizer = new RawTraceSynchronizer({
enqueueUpdate: async () => ({ accepted: false, degraded: false, reason: "record_pending" }),
getConfig: createConfig,
measureDirectorySize: async (directory) => {
measuredDirectories.push(directory);
return 1;
},
replayIntervalMs: 60_000,
spoolDirectory
});
try {
await synchronizer.start();
await waitFor(() => measuredDirectories.length === 40);
assert.equal(measuredDirectories.length, 40);
measuredDirectories.length = 0;
const producerDirectory = path.join(spoolDirectory, "new-producer-bundle");
const bodyFile = path.join(producerDirectory, "upstream_request.json");
mkdirSync(producerDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"incremental"}');
let result;
const deadline = Date.now() + 2_000;
do {
result = await sendRawTrace(synchronizer, {
parts: [{ filePath: bodyFile, partType: "upstream_request" }],
requestId: "incremental-capacity-bundle",
turnKey: "incremental-capacity-request"
});
if (result.statusCode !== 503) break;
assert.equal(result.body.reason, "initializing");
await new Promise((resolve) => setTimeout(resolve, 5));
} while (Date.now() < deadline);
assert.equal(result.statusCode, 202);
assert.equal(measuredDirectories.length, 1);
assert.equal(measuredDirectories[0].includes("new-producer-bundle"), false);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace moves permanently pending bundles to bounded dead letter after max attempts", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-max-attempt-test-"));
const spoolDirectory = path.join(dir, "spool");
const bundleDirectory = path.join(spoolDirectory, "pending-bundle");
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"pending"}');
writeFileSync(path.join(bundleDirectory, "manifest.json"), JSON.stringify({
parts: [{ filePath: bodyFile, partType: "upstream_request" }],
requestId: "max-attempt-bundle",
turnKey: "max-attempt-request"
}));
let attempts = 0;
const synchronizer = new RawTraceSynchronizer({
bundleMaxAttempts: 1,
enqueueUpdate: async () => {
attempts += 1;
return { accepted: false, degraded: false, reason: "record_pending" };
},
getConfig: createConfig,
replayIntervalMs: 10,
retryCooldownMs: 10,
spoolDirectory
});
try {
await synchronizer.start();
await waitFor(() => readdirSync(path.join(spoolDirectory, ".ccr-dead-letter")).length === 1);
assert.equal(attempts, 1);
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-inbox")).length, 0);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace backs off record-pending retries without repeatedly persisting attempts", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-pending-backoff-test-"));
const spoolDirectory = path.join(dir, "spool");
const bundleDirectory = path.join(spoolDirectory, "pending-backoff-bundle");
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, '{"message":"pending"}');
let attempts = 0;
const synchronizer = new RawTraceSynchronizer({
bundleMaxAttempts: 100,
enqueueUpdate: async () => {
attempts += 1;
return { accepted: false, degraded: false, reason: "record_pending" };
},
getConfig: createConfig,
pendingRetryMaxMs: 1_000,
replayIntervalMs: 5,
retryCooldownMs: 15,
spoolDirectory
});
try {
const result = await sendRawTrace(synchronizer, {
parts: [{ filePath: bodyFile, partType: "upstream_request" }],
requestId: "pending-backoff-bundle-id",
turnKey: "pending-backoff-request"
});
assert.equal(result.statusCode, 202);
await synchronizer.start();
await waitFor(() => attempts >= 1);
const inboxDirectory = path.join(spoolDirectory, ".ccr-inbox");
const storedDirectory = path.join(inboxDirectory, readdirSync(inboxDirectory)[0]);
const deliveryFile = path.join(storedDirectory, ".ccr-delivery.json");
await waitFor(() => JSON.parse(readFileSync(deliveryFile, "utf8")).lastError === "record_pending");
const firstPersistedState = readFileSync(deliveryFile, "utf8");
await new Promise((resolve) => setTimeout(resolve, 80));
assert.ok(attempts >= 2);
assert.ok(attempts <= 3);
assert.equal(readFileSync(deliveryFile, "utf8"), firstPersistedState);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace replay rotates through a bounded number of bundles per pass", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-replay-budget-test-"));
const spoolDirectory = path.join(dir, "spool");
const inboxDirectory = path.join(spoolDirectory, ".ccr-inbox");
mkdirSync(inboxDirectory, { recursive: true });
for (let index = 0; index < 5; index += 1) {
const bundleDirectory = path.join(inboxDirectory, `budget-${index}`);
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, JSON.stringify({ index }));
writeFileSync(path.join(bundleDirectory, "manifest.json"), JSON.stringify({
parts: [{ filePath: bodyFile, partType: "upstream_request" }],
requestId: `budget-bundle-${index}`,
turnKey: `budget-request-${index}`
}));
}
const attemptedBundleIds = [];
const synchronizer = new RawTraceSynchronizer({
bundleMaxAttempts: 100,
enqueueUpdate: async (update) => {
attemptedBundleIds.push(update.bundleId);
return { accepted: false, degraded: false, reason: "record_pending" };
},
getConfig: createConfig,
replayIntervalMs: 50,
replayMaxBundlesPerPass: 2,
replayTimeBudgetMs: 1_000,
retryCooldownMs: 60_000,
spoolDirectory
});
try {
await synchronizer.start();
await waitFor(() => attemptedBundleIds.length >= 2);
await new Promise((resolve) => setTimeout(resolve, 20));
assert.equal(attemptedBundleIds.length, 2);
await waitFor(() => attemptedBundleIds.length === 5);
assert.equal(new Set(attemptedBundleIds).size, 5);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace replay stops when its time budget is exhausted", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-replay-time-budget-test-"));
const spoolDirectory = path.join(dir, "spool");
const inboxDirectory = path.join(spoolDirectory, ".ccr-inbox");
mkdirSync(inboxDirectory, { recursive: true });
for (let index = 0; index < 3; index += 1) {
const bundleDirectory = path.join(inboxDirectory, `timed-${index}`);
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, JSON.stringify({ index }));
writeFileSync(path.join(bundleDirectory, "manifest.json"), JSON.stringify({
parts: [{ filePath: bodyFile, partType: "upstream_request" }],
requestId: `timed-bundle-${index}`,
turnKey: `timed-request-${index}`
}));
}
let attempts = 0;
const synchronizer = new RawTraceSynchronizer({
bundleMaxAttempts: 100,
enqueueUpdate: async () => {
attempts += 1;
await new Promise((resolve) => setTimeout(resolve, 20));
return { accepted: false, degraded: false, reason: "record_pending" };
},
getConfig: createConfig,
replayIntervalMs: 200,
replayMaxBundlesPerPass: 100,
replayTimeBudgetMs: 5,
retryCooldownMs: 60_000,
spoolDirectory
});
try {
await synchronizer.start();
await waitFor(() => attempts >= 1);
await new Promise((resolve) => setTimeout(resolve, 60));
assert.equal(attempts, 1);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("raw trace replay isolates a failing bundle and continues with later bundles", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-isolated-replay-test-"));
const spoolDirectory = path.join(dir, "spool");
for (const name of ["bad", "good"]) {
const bundleDirectory = path.join(spoolDirectory, `${name}-bundle`);
const bodyFile = path.join(bundleDirectory, "upstream_request.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(bodyFile, JSON.stringify({ name }));
writeFileSync(path.join(bundleDirectory, "manifest.json"), JSON.stringify({
parts: [{ filePath: bodyFile, partType: "upstream_request" }],
requestId: `${name}-bundle-id`,
turnKey: `${name}-request`
}));
}
let goodApplied = false;
const synchronizer = new RawTraceSynchronizer({
enqueueUpdate: async (update, files) => {
if (update.bundleId === "bad-bundle-id") throw new Error("permanent bad bundle");
goodApplied = true;
rmSync(files.cleanupDirectory, { force: true, recursive: true });
return { accepted: true, degraded: false };
},
getConfig: createConfig,
replayIntervalMs: 10_000,
spoolDirectory
});
try {
await synchronizer.start();
await waitFor(() => goodApplied);
assert.equal(goodApplied, true);
assert.equal(readdirSync(path.join(spoolDirectory, ".ccr-inbox")).length, 1);
} finally {
await synchronizer.stop();
rmSync(dir, { force: true, recursive: true });
}
});
test("fallback raw bundles keep unique bundle ids while sharing the logical request", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-fallback-id-test-"));
const spoolDirectory = path.join(dir, "spool");
try {
const bundles = [];
for (const attempt of [1, 2]) {
const bundleDirectory = path.join(spoolDirectory, `bundle-${attempt}`);
const clientMetadata = path.join(bundleDirectory, "client_request_metadata.json");
const responseMetadata = path.join(bundleDirectory, "upstream_response_metadata.json");
mkdirSync(bundleDirectory, { recursive: true });
writeFileSync(clientMetadata, JSON.stringify({ headers: { "x-ccr-route-attempt": String(attempt) } }));
writeFileSync(responseMetadata, JSON.stringify({ statusCode: attempt === 1 ? 500 : 200 }));
bundles.push(await readRawTraceRequestLogBundle({
parts: [
{ filePath: clientMetadata, partType: "client_request_metadata" },
{ filePath: responseMetadata, partType: "upstream_response_metadata" }
],
requestId: `core-bundle-${attempt}`,
turnKey: "shared-logical-request"
}, spoolDirectory));
}
assert.deepEqual(bundles.map((bundle) => ({
attempt: bundle.update.attempt,
bundleId: bundle.update.bundleId,
requestId: bundle.update.requestId
})), [
{ attempt: 1, bundleId: "core-bundle-1", requestId: "shared-logical-request" },
{ attempt: 2, bundleId: "core-bundle-2", requestId: "shared-logical-request" }
]);
} finally {
rmSync(dir, { force: true, recursive: true });
}
@@ -165,3 +881,29 @@ function createConfig() {
config.observability.requestLogSuccessSampleRate = 1;
return config;
}
async function waitFor(predicate, timeoutMs = 2_000) {
const deadline = Date.now() + timeoutMs;
while (!predicate()) {
if (Date.now() >= deadline) assert.fail("Timed out waiting for raw trace state.");
await new Promise((resolve) => setTimeout(resolve, 5));
}
}
async function sendRawTrace(synchronizer, manifest, onResponse) {
const request = Readable.from([JSON.stringify(manifest)]);
request.method = "POST";
request.headers = { [rawTraceSyncHeader]: synchronizer.token };
const result = {};
const response = {
end(body) {
result.body = JSON.parse(body);
onResponse?.();
},
writeHead(statusCode) {
result.statusCode = statusCode;
}
};
await synchronizer.handle(request, response);
return result;
}
@@ -1,6 +1,23 @@
import assert from "node:assert/strict";
import test from "node:test";
import { RequestRouteTraceRecorder } from "@ccr/core/observability/route-trace.ts";
import { isSensitiveRequestLogHeaderName } from "@ccr/core/observability/sensitive-headers.ts";
test("request log header redaction fails closed for custom authentication headers", () => {
for (const name of [
"x-auth-token",
"x-amz-security-token",
"x-company-client-secret",
"x-private-key",
"x-signed-request-signature",
"x-custom-credential"
]) {
assert.equal(isSensitiveRequestLogHeaderName(name), true, name);
}
for (const name of ["content-type", "user-agent", "x-request-id"]) {
assert.equal(isSensitiveRequestLogHeaderName(name), false, name);
}
});
test("route trace records actively reported changes and never persists sensitive values", () => {
const startedAt = Date.now();
@@ -11,6 +28,7 @@ test("route trace records actively reported changes and never persists sensitive
{ after: "model-b", before: "model-a", operation: "replace", path: "/body/model", scope: "body" },
{ after: "body-secret-b", before: "body-secret-a", operation: "replace", path: "/body/api_key", scope: "body" },
{ after: "Bearer header-secret-b", before: "Bearer header-secret-a", operation: "replace", path: "/headers/authorization", scope: "headers" },
{ after: "auth-sub-secret-b", before: "auth-sub-secret-a", operation: "replace", path: "/headers/x-auth-sub", scope: "headers" },
{
after: "https://upstream.example/v1/messages?access_token=url-secret-b",
before: "http://127.0.0.1/v1/messages?access_token=url-secret-a",
@@ -36,7 +54,42 @@ test("route trace records actively reported changes and never persists sensitive
assert.ok(trace.hops[1].changes.some((change) => change.path === "/body/model"));
assert.ok(trace.hops[1].changes.some((change) => change.path === "/headers/authorization" && change.redacted));
assert.match(serialized, /\[redacted\]/);
assert.doesNotMatch(serialized, /header-secret|body-secret|url-secret/);
assert.ok(trace.hops[1].changes.some((change) => change.path === "/headers/x-auth-sub" && change.redacted));
assert.doesNotMatch(serialized, /header-secret|auth-sub-secret|body-secret|url-secret/);
});
test("route trace omits body values when request body capture is disabled", () => {
const recorder = new RequestRouteTraceRecorder(Date.now());
recorder.captureIngress();
recorder.capture({
changes: [
{
after: [{ role: "user", content: "after-private-message" }],
before: [{ role: "user", content: "before-private-message" }],
operation: "replace",
path: "/body/messages",
scope: "body"
},
{
after: "diagnostic-value",
operation: "add",
path: "/headers/x-ccr-route-source",
scope: "headers"
}
],
name: "router.policy",
phase: "routing"
});
const trace = recorder.finish({ captureBodyValues: false });
const bodyChange = trace.hops[1].changes[0];
assert.deepEqual(bodyChange, {
operation: "replace",
path: "/body/messages",
scope: "body"
});
assert.equal(trace.hops[1].changes[1].after, "diagnostic-value");
assert.doesNotMatch(JSON.stringify(trace), /private-message/);
});
test("route trace bounds actively reported values without parsing request bodies", () => {