Files
ccs/tests/unit/services/logging/log-context.test.ts
T
Tam Nhu Tran 7a22e89046 feat(logging): add structured contract, ALS context, stage helpers, and redaction coverage
Establish the structured-log foundation that downstream modules consume:
- LogStage union (intake|route|auth|dispatch|upstream|transform|respond|cleanup)
- AsyncLocalStorage-backed log-context for end-to-end requestId propagation
- Logger.stage() emit helper with optional latencyMs and structured metadata
- Extended log-redaction patterns for proxy-authorization, x-goog-api-key,
  oauth-code, refresh-token classes; Bearer/Basic/Token value-shape masking
- Public exports: getRecentLogEntries, withRequestContext, runWithRequestId,
  getRequestId

Refs #1141, #1138
2026-04-30 12:59:42 -04:00

77 lines
2.5 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import {
getRequestContext,
getRequestId,
mergeRequestContext,
runWithRequestId,
withRequestContext,
} from '../../../../src/services/logging/log-context';
describe('log-context (AsyncLocalStorage)', () => {
it('returns undefined outside any context', () => {
expect(getRequestContext()).toBeUndefined();
expect(getRequestId()).toBeUndefined();
});
it('exposes the active context inside withRequestContext', () => {
withRequestContext({ requestId: 'req-1', method: 'POST' }, () => {
const ctx = getRequestContext();
expect(ctx?.requestId).toBe('req-1');
expect(ctx?.method).toBe('POST');
expect(getRequestId()).toBe('req-1');
});
expect(getRequestContext()).toBeUndefined();
});
it('isolates parallel async tasks (no cross-leak)', async () => {
const observed: string[] = [];
await Promise.all([
withRequestContext({ requestId: 'req-A' }, async () => {
await new Promise((r) => setTimeout(r, 5));
observed.push(`A:${getRequestId()}`);
}),
withRequestContext({ requestId: 'req-B' }, async () => {
await new Promise((r) => setTimeout(r, 1));
observed.push(`B:${getRequestId()}`);
}),
]);
expect(observed.sort()).toEqual(['A:req-A', 'B:req-B']);
});
it('runWithRequestId mints a UUID and exposes it inside fn', () => {
let captured: string | undefined;
const { requestId, result } = runWithRequestId(() => {
captured = getRequestId();
return 42;
});
expect(result).toBe(42);
expect(captured).toBe(requestId);
expect(requestId).toMatch(/^[0-9a-f-]{36}$/i);
});
it('mergeRequestContext returns input unchanged outside ALS', () => {
const merged = mergeRequestContext({ foo: 'bar' });
expect(merged).toEqual({ foo: 'bar' });
});
it('mergeRequestContext merges active context, with input overriding ctx', () => {
withRequestContext({ requestId: 'req-X', method: 'GET' }, () => {
const merged = mergeRequestContext({ method: 'POST', extra: 1 });
expect(merged).toEqual({ requestId: 'req-X', method: 'POST', extra: 1 });
});
});
it('preserves emit-time ordering within a single requestId', async () => {
const order: number[] = [];
await withRequestContext({ requestId: 'req-order' }, async () => {
for (let i = 0; i < 5; i++) {
await Promise.resolve();
order.push(i);
}
});
expect(order).toEqual([0, 1, 2, 3, 4]);
});
});