feat(proxy,cli): emit lifecycle stages with x-ccs-request-id propagation

Wrap proxy server entry edge in withRequestContext so every inbound request
gets a requestId (reused from x-ccs-request-id header when valid UUID-ish,
freshly minted otherwise). messages-route emits 7 stages: intake / auth /
transform / route / dispatch / upstream / respond, each with latencyMs and
structured error metadata on failure.

CCS CLI entry (ccs.ts) wraps main() in runWithRequestId and emits
cli.command.start / complete / failed stages so command lifecycle is
correlatable end-to-end.

Refs #1141, #1138
This commit is contained in:
Tam Nhu Tran committed 2026-04-30 13:00:00 -04:00
1 parent 7a22e89046
commit 4700727915
3 files changed
+112 -22

No files matched your search

+33 -3
View File
@@ -84,7 +84,7 @@ import { execClaude, stripAnthropicRoutingEnv, stripBrowserEnv } from './utils/s
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation'; import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
import { createOpenAICompatLaunchSettings } from './utils/openai-compat-launch-settings'; import { createOpenAICompatLaunchSettings } from './utils/openai-compat-launch-settings';
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning'; import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
import { createLogger } from './services/logging'; import { createLogger, runWithRequestId } from './services/logging';
import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides'; import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides';
import type { ProfileDetectionResult } from './auth/profile-detector'; import type { ProfileDetectionResult } from './auth/profile-detector';
import type { BrowserLaunchOverride } from './utils/browser'; import type { BrowserLaunchOverride } from './utils/browser';
@@ -1683,5 +1683,35 @@ process.on('SIGINT', () => {
} }
}); });
// Run main // Run main inside a per-invocation request context so all backend logging
main().catch(handleError); // emitted during this CLI run shares a single requestId. CLI text output
// (stdout/stderr) is unaffected — the requestId lives in logs only.
const cliEntryStartedAt = Date.now();
const cliEntryLogger = createLogger('cli:entry');
runWithRequestId(() => {
cliEntryLogger.stage('intake', 'cli.command.start', 'CLI invocation started', {
argv: process.argv.slice(2),
});
return main()
.then(() => {
cliEntryLogger.stage(
'respond',
'cli.command.complete',
'CLI invocation completed',
{ exitCode: process.exitCode ?? 0 },
{ latencyMs: Date.now() - cliEntryStartedAt }
);
})
.catch((err) => {
const error =
err instanceof Error
? { name: err.name, message: err.message, stack: err.stack }
: { name: 'Error', message: String(err) };
cliEntryLogger.stage('cleanup', 'cli.command.failed', 'CLI invocation failed', undefined, {
level: 'error',
latencyMs: Date.now() - cliEntryStartedAt,
error,
});
handleError(err);
});
});
+46 -11
View File
@@ -207,11 +207,23 @@ export async function handleProxyMessagesRequest(
insecureDispatcher?: Dispatcher insecureDispatcher?: Dispatcher
): Promise<void> { ): Promise<void> {
const transformer = new ProxySseStreamTransformer(); const transformer = new ProxySseStreamTransformer();
const startedAt = Date.now();
logger.stage('intake', 'request.received', 'Proxy /v1/messages request received', {
method: req.method || 'POST',
remoteAddress: req.socket.remoteAddress || null,
});
if (!validateIncomingProxyAuth(req.headers, expectedAuthToken)) { if (!validateIncomingProxyAuth(req.headers, expectedAuthToken)) {
logger.warn('auth.invalid', 'Rejected proxy message request with invalid auth token', { logger.stage(
remoteAddress: req.socket.remoteAddress || null, 'auth',
}); 'auth.invalid',
'Rejected proxy message request with invalid auth token',
{
remoteAddress: req.socket.remoteAddress || null,
},
{ level: 'warn' }
);
await pipeWebResponseToNode( await pipeWebResponseToNode(
transformer.error(401, 'authentication_error', 'Missing or invalid local proxy token'), transformer.error(401, 'authentication_error', 'Missing or invalid local proxy token'),
res res
@@ -219,11 +231,14 @@ export async function handleProxyMessagesRequest(
return; return;
} }
logger.stage('auth', 'auth.ok', 'Proxy auth validated');
let timeoutMs = REQUEST_TIMEOUT_MS; let timeoutMs = REQUEST_TIMEOUT_MS;
try { try {
const rawBody = await readJsonBody(req); const rawBody = await readJsonBody(req);
logger.stage('transform', 'request.transform.start', 'Transforming inbound proxy body');
const upstream = buildUpstreamRequest(profile, rawBody); const upstream = buildUpstreamRequest(profile, rawBody);
logger.info('request.forward', 'Forwarding Anthropic request to OpenAI-compatible upstream', { logger.stage('route', 'request.routed', 'Resolved proxy upstream route', {
profileName: upstream.route.profile.profileName, profileName: upstream.route.profile.profileName,
provider: upstream.route.profile.provider, provider: upstream.route.profile.provider,
baseUrl: upstream.route.profile.baseUrl, baseUrl: upstream.route.profile.baseUrl,
@@ -240,7 +255,8 @@ export async function handleProxyMessagesRequest(
res, res,
controller, controller,
(source) => { (source) => {
logger.info( logger.stage(
'cleanup',
'request.disconnect', 'request.disconnect',
'Aborting upstream request after local client disconnect', 'Aborting upstream request after local client disconnect',
{ {
@@ -252,28 +268,47 @@ export async function handleProxyMessagesRequest(
); );
try { try {
logger.stage('dispatch', 'upstream.dispatch', 'Dispatching upstream fetch', {
profileName: profile.profileName,
});
const upstreamResponse = await fetch( const upstreamResponse = await fetch(
resolveOpenAIChatCompletionsUrl(upstream.route.profile.baseUrl), resolveOpenAIChatCompletionsUrl(upstream.route.profile.baseUrl),
buildFetchInit(upstream.route.profile, upstream.body, controller.signal, insecureDispatcher) buildFetchInit(upstream.route.profile, upstream.body, controller.signal, insecureDispatcher)
); );
logger.info('response.received', 'Received upstream response', { logger.stage('upstream', 'upstream.response', 'Received upstream response', {
profileName: profile.profileName, profileName: profile.profileName,
routedProfileName: upstream.route.profile.profileName, routedProfileName: upstream.route.profile.profileName,
status: upstreamResponse.status, status: upstreamResponse.status,
}); });
const response = await transformer.transform(upstreamResponse); const response = await transformer.transform(upstreamResponse);
await pipeWebResponseToNode(response, res); await pipeWebResponseToNode(response, res);
logger.stage('respond', 'request.respond', 'Proxy response written', undefined, {
latencyMs: Date.now() - startedAt,
});
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
cleanupDisconnectHandlers(); cleanupDisconnectHandlers();
} }
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Unknown proxy error'; const message = error instanceof Error ? error.message : 'Unknown proxy error';
logger.error('request.failed', 'Proxy message request failed', { const errInfo = {
profileName: profile.profileName, name: error instanceof Error ? error.name : 'Error',
error: message, message,
abort: error instanceof Error && error.name === 'AbortError', };
}); logger.stage(
'cleanup',
'request.failed',
'Proxy message request failed',
{
profileName: profile.profileName,
abort: error instanceof Error && error.name === 'AbortError',
},
{
level: 'error',
latencyMs: Date.now() - startedAt,
error: errInfo,
}
);
const status = const status =
error instanceof Error && error.name === 'AbortError' error instanceof Error && error.name === 'AbortError'
? 502 ? 502
+33 -8
View File
@@ -1,8 +1,9 @@
import * as http from 'http'; import * as http from 'http';
import { randomUUID } from 'crypto';
import { Agent } from 'undici'; import { Agent } from 'undici';
import type { OpenAICompatProfileConfig } from '../profile-router'; import type { OpenAICompatProfileConfig } from '../profile-router';
import { OPENAI_COMPAT_PROXY_SERVICE_NAME } from '../proxy-daemon-paths'; import { OPENAI_COMPAT_PROXY_SERVICE_NAME } from '../proxy-daemon-paths';
import { createLogger } from '../../services/logging'; import { createLogger, withRequestContext } from '../../services/logging';
import { import {
handleProxyMessagesRequest, handleProxyMessagesRequest,
handleProxyModelsRequest, handleProxyModelsRequest,
@@ -10,6 +11,18 @@ import {
} from './messages-route'; } from './messages-route';
import { writeJson } from './http-helpers'; import { writeJson } from './http-helpers';
const REQUEST_ID_HEADER = 'x-ccs-request-id';
// Loose UUID-ish guard: accepts UUIDs and similar opaque ids; rejects empty / control chars.
const REQUEST_ID_PATTERN = /^[A-Za-z0-9._-]{8,128}$/;
function resolveInboundRequestId(headers: http.IncomingHttpHeaders): string {
const raw = headers[REQUEST_ID_HEADER];
if (typeof raw === 'string' && REQUEST_ID_PATTERN.test(raw.trim())) {
return raw.trim();
}
return randomUUID();
}
export interface OpenAICompatProxyServerOptions { export interface OpenAICompatProxyServerOptions {
profile: OpenAICompatProfileConfig; profile: OpenAICompatProfileConfig;
host?: string; host?: string;
@@ -28,13 +41,25 @@ export function startOpenAICompatProxyServer(options: OpenAICompatProxyServerOpt
const insecureDispatcher = options.insecure const insecureDispatcher = options.insecure
? new Agent({ connect: { rejectUnauthorized: false } }) ? new Agent({ connect: { rejectUnauthorized: false } })
: undefined; : undefined;
const server = http.createServer(async (req, res) => { const server = http.createServer((req, res) => {
const method = req.method || 'GET'; const requestId = resolveInboundRequestId(req.headers);
const requestUrl = req.url || '/'; res.setHeader(REQUEST_ID_HEADER, requestId);
const parsedUrl = new URL(requestUrl, 'http://127.0.0.1'); void withRequestContext({ requestId }, async () => {
const pathname = const method = req.method || 'GET';
parsedUrl.pathname.length > 1 ? parsedUrl.pathname.replace(/\/+$/, '') : parsedUrl.pathname; const requestUrl = req.url || '/';
const parsedUrl = new URL(requestUrl, 'http://127.0.0.1');
const pathname =
parsedUrl.pathname.length > 1 ? parsedUrl.pathname.replace(/\/+$/, '') : parsedUrl.pathname;
await handleProxyRequest(req, res, method, pathname);
});
});
async function handleProxyRequest(
req: http.IncomingMessage,
res: http.ServerResponse,
method: string,
pathname: string
): Promise<void> {
if ((method === 'GET' || method === 'HEAD') && pathname === '/health') { if ((method === 'GET' || method === 'HEAD') && pathname === '/health') {
if (method === 'HEAD') { if (method === 'HEAD') {
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -105,7 +130,7 @@ export function startOpenAICompatProxyServer(options: OpenAICompatProxyServerOpt
pathname, pathname,
}); });
writeJson(res, 404, { error: 'Not found' }); writeJson(res, 404, { error: 'Not found' });
}); }
logger.info('server.start', 'OpenAI-compatible proxy server listening', { logger.info('server.start', 'OpenAI-compatible proxy server listening', {
baseUrl: `http://${host}:${options.port}`, baseUrl: `http://${host}:${options.port}`,