autotel-adapters 0.3.12 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -3
- package/src/cloudflare.test.ts +0 -56
- package/src/cloudflare.ts +0 -186
- package/src/core.test.ts +0 -38
- package/src/core.ts +0 -225
- package/src/express.test.ts +0 -85
- package/src/express.ts +0 -169
- package/src/fastify.test.ts +0 -79
- package/src/fastify.ts +0 -151
- package/src/hono.ts +0 -20
- package/src/index.ts +0 -14
- package/src/next.test.ts +0 -47
- package/src/next.ts +0 -137
- package/src/nitro.test.ts +0 -49
- package/src/nitro.ts +0 -121
- package/src/resolve-adapter-config.test.ts +0 -110
- package/src/tanstack.ts +0 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel-adapters",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"description": "Framework adapters and composable DX helpers for autotel",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -56,12 +56,11 @@
|
|
|
56
56
|
},
|
|
57
57
|
"files": [
|
|
58
58
|
"dist",
|
|
59
|
-
"src",
|
|
60
59
|
"README.md",
|
|
61
60
|
"skills"
|
|
62
61
|
],
|
|
63
62
|
"dependencies": {
|
|
64
|
-
"autotel": "4.
|
|
63
|
+
"autotel": "4.2.0"
|
|
65
64
|
},
|
|
66
65
|
"peerDependencies": {
|
|
67
66
|
"hono": ">=4.12.23",
|
package/src/cloudflare.test.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { useLogger, withAutotelFetch } from './cloudflare';
|
|
3
|
-
|
|
4
|
-
describe('cloudflare adapter', () => {
|
|
5
|
-
it('throws clear error when useLogger is called outside traced context', () => {
|
|
6
|
-
expect(() =>
|
|
7
|
-
useLogger({
|
|
8
|
-
method: 'GET',
|
|
9
|
-
url: 'https://example.com/health',
|
|
10
|
-
headers: { 'x-request-id': 'req-1' },
|
|
11
|
-
}),
|
|
12
|
-
).toThrow('[autotel-adapters/cloudflare] No active trace context.');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('provides request-scoped logger inside withAutotelFetch()', async () => {
|
|
16
|
-
const handler = withAutotelFetch(async (request) => {
|
|
17
|
-
const log = useLogger(request);
|
|
18
|
-
log.set({ worker: 'example' });
|
|
19
|
-
return { ok: true };
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
await expect(
|
|
23
|
-
handler(
|
|
24
|
-
{ method: 'GET', url: 'https://example.com/orders' },
|
|
25
|
-
{},
|
|
26
|
-
{},
|
|
27
|
-
),
|
|
28
|
-
).resolves.toMatchObject({ ok: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('auto-emits one wide event by default', async () => {
|
|
32
|
-
const onEmit = vi.fn();
|
|
33
|
-
const handler = withAutotelFetch(
|
|
34
|
-
async (request) => {
|
|
35
|
-
useLogger(request).set({ worker: 'example' });
|
|
36
|
-
return { ok: true };
|
|
37
|
-
},
|
|
38
|
-
{ requestLoggerOptions: { onEmit } },
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
await handler({ method: 'GET', url: 'https://example.com/orders' }, {}, {});
|
|
42
|
-
expect(onEmit).toHaveBeenCalledTimes(1);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('does not emit when autoEmit is false', async () => {
|
|
46
|
-
const onEmit = vi.fn();
|
|
47
|
-
const handler = withAutotelFetch(async () => ({ ok: true }), {
|
|
48
|
-
autoEmit: false,
|
|
49
|
-
requestLoggerOptions: { onEmit },
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
await handler({ method: 'GET', url: 'https://example.com/x' }, {}, {});
|
|
53
|
-
expect(onEmit).not.toHaveBeenCalled();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
package/src/cloudflare.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createDrainPipeline,
|
|
3
|
-
createStructuredError,
|
|
4
|
-
getRequestLogger,
|
|
5
|
-
parseError,
|
|
6
|
-
trace,
|
|
7
|
-
type DrainPipelineOptions,
|
|
8
|
-
type ParsedError,
|
|
9
|
-
type PipelineDrainFn,
|
|
10
|
-
type RequestLogger,
|
|
11
|
-
type RequestLoggerOptions,
|
|
12
|
-
type StructuredError,
|
|
13
|
-
type StructuredErrorInput,
|
|
14
|
-
} from 'autotel';
|
|
15
|
-
import { createAdapterToolkit, createUseLogger, getHeader } from './core';
|
|
16
|
-
|
|
17
|
-
export interface CloudflareRequestLike {
|
|
18
|
-
method?: string;
|
|
19
|
-
url?: string;
|
|
20
|
-
headers?:
|
|
21
|
-
| { get(name: string): string | null }
|
|
22
|
-
| Record<string, string | undefined>;
|
|
23
|
-
cf?: Record<string, unknown>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface CloudflareExecutionContextLike {
|
|
27
|
-
waitUntil?: (promise: Promise<unknown>) => void;
|
|
28
|
-
passThroughOnException?: () => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface CloudflareWithAutotelOptions<TEnv = unknown> {
|
|
32
|
-
spanName?: string | ((request: CloudflareRequestLike, env: TEnv) => string);
|
|
33
|
-
requestLoggerOptions?: RequestLoggerOptions;
|
|
34
|
-
enrich?: (
|
|
35
|
-
request: CloudflareRequestLike,
|
|
36
|
-
env: TEnv,
|
|
37
|
-
ctx: CloudflareExecutionContextLike,
|
|
38
|
-
) => Record<string, unknown> | undefined;
|
|
39
|
-
/** Emit one wide event automatically when the handler settles. Default `true`. */
|
|
40
|
-
autoEmit?: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const requestLoggers = new WeakMap<object, RequestLogger>();
|
|
44
|
-
|
|
45
|
-
function enrichFromRequest(
|
|
46
|
-
request?: CloudflareRequestLike,
|
|
47
|
-
): Record<string, unknown> | undefined {
|
|
48
|
-
if (!request) return undefined;
|
|
49
|
-
|
|
50
|
-
let route = '/';
|
|
51
|
-
if (request.url) {
|
|
52
|
-
try {
|
|
53
|
-
route = new URL(request.url).pathname;
|
|
54
|
-
} catch {
|
|
55
|
-
route = request.url;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const requestId =
|
|
60
|
-
getHeader(request.headers, 'x-request-id') ??
|
|
61
|
-
getHeader(request.headers, 'cf-ray');
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
...(request.method ? { 'http.request.method': request.method } : {}),
|
|
65
|
-
...(request.url ? { 'url.full': request.url } : {}),
|
|
66
|
-
...(route ? { 'http.route': route } : {}),
|
|
67
|
-
...(requestId ? { 'http.request.id': requestId } : {}),
|
|
68
|
-
...(request.cf?.country ? { 'cloudflare.country': request.cf.country } : {}),
|
|
69
|
-
...(request.cf?.colo ? { 'cloudflare.colo': request.cf.colo } : {}),
|
|
70
|
-
...(request.cf?.city ? { 'cloudflare.city': request.cf.city } : {}),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const baseUseLogger = createUseLogger<CloudflareRequestLike>({
|
|
75
|
-
adapterName: 'cloudflare',
|
|
76
|
-
enrich: enrichFromRequest,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
export function useLogger(
|
|
80
|
-
request?: CloudflareRequestLike,
|
|
81
|
-
requestLoggerOptions?: RequestLoggerOptions,
|
|
82
|
-
): RequestLogger {
|
|
83
|
-
if (request) {
|
|
84
|
-
const existing = requestLoggers.get(request as object);
|
|
85
|
-
if (existing) return existing;
|
|
86
|
-
}
|
|
87
|
-
return baseUseLogger(request, requestLoggerOptions);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function withAutotelFetch<
|
|
91
|
-
TEnv,
|
|
92
|
-
TRequest extends CloudflareRequestLike,
|
|
93
|
-
TContext extends CloudflareExecutionContextLike,
|
|
94
|
-
TReturn,
|
|
95
|
-
>(
|
|
96
|
-
handler: (
|
|
97
|
-
request: TRequest,
|
|
98
|
-
env: TEnv,
|
|
99
|
-
ctx: TContext,
|
|
100
|
-
) => TReturn | Promise<TReturn>,
|
|
101
|
-
options?: CloudflareWithAutotelOptions<TEnv>,
|
|
102
|
-
): (
|
|
103
|
-
request: TRequest,
|
|
104
|
-
env: TEnv,
|
|
105
|
-
ctx: TContext,
|
|
106
|
-
) => Promise<TReturn> {
|
|
107
|
-
return async (
|
|
108
|
-
request: TRequest,
|
|
109
|
-
env: TEnv,
|
|
110
|
-
executionContext: TContext,
|
|
111
|
-
): Promise<TReturn> => {
|
|
112
|
-
const spanName =
|
|
113
|
-
typeof options?.spanName === 'function'
|
|
114
|
-
? options.spanName(request, env)
|
|
115
|
-
: (options?.spanName ?? `cloudflare.${request.method ?? 'request'}`);
|
|
116
|
-
|
|
117
|
-
const wrapped = trace(
|
|
118
|
-
{ name: spanName },
|
|
119
|
-
(ctx) => async (
|
|
120
|
-
innerRequest: TRequest,
|
|
121
|
-
innerEnv: TEnv,
|
|
122
|
-
innerExecutionContext: TContext,
|
|
123
|
-
) => {
|
|
124
|
-
const log = getRequestLogger(ctx, options?.requestLoggerOptions);
|
|
125
|
-
const auto = enrichFromRequest(innerRequest);
|
|
126
|
-
if (auto && Object.keys(auto).length > 0) {
|
|
127
|
-
log.set(auto);
|
|
128
|
-
}
|
|
129
|
-
const custom = options?.enrich?.(
|
|
130
|
-
innerRequest,
|
|
131
|
-
innerEnv,
|
|
132
|
-
innerExecutionContext,
|
|
133
|
-
);
|
|
134
|
-
if (custom && Object.keys(custom).length > 0) {
|
|
135
|
-
log.set(custom);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
requestLoggers.set(innerRequest as object, log);
|
|
139
|
-
try {
|
|
140
|
-
return await handler(innerRequest, innerEnv, innerExecutionContext);
|
|
141
|
-
} finally {
|
|
142
|
-
if (options?.autoEmit !== false) {
|
|
143
|
-
log.emitNow();
|
|
144
|
-
}
|
|
145
|
-
requestLoggers.delete(innerRequest as object);
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
return await wrapped(request, env, executionContext);
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function createCloudflareAdapter<TEnv = unknown>(
|
|
155
|
-
options?: CloudflareWithAutotelOptions<TEnv>,
|
|
156
|
-
) {
|
|
157
|
-
return {
|
|
158
|
-
withAutotelFetch: <
|
|
159
|
-
TRequest extends CloudflareRequestLike,
|
|
160
|
-
TContext extends CloudflareExecutionContextLike,
|
|
161
|
-
TReturn,
|
|
162
|
-
>(
|
|
163
|
-
handler: (
|
|
164
|
-
request: TRequest,
|
|
165
|
-
env: TEnv,
|
|
166
|
-
ctx: TContext,
|
|
167
|
-
) => TReturn | Promise<TReturn>,
|
|
168
|
-
) => withAutotelFetch(handler, options),
|
|
169
|
-
useLogger,
|
|
170
|
-
parseError: (error: unknown): ParsedError => parseError(error),
|
|
171
|
-
createStructuredError: (
|
|
172
|
-
input: StructuredErrorInput,
|
|
173
|
-
): StructuredError => createStructuredError(input),
|
|
174
|
-
createDrainPipeline: <T = unknown>(
|
|
175
|
-
drainOptions?: DrainPipelineOptions<T>,
|
|
176
|
-
): ((batchDrain: (batch: T[]) => void | Promise<void>) => PipelineDrainFn<T>) =>
|
|
177
|
-
createDrainPipeline(drainOptions),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export const cloudflareToolkit = createAdapterToolkit<CloudflareRequestLike>({
|
|
182
|
-
adapterName: 'cloudflare',
|
|
183
|
-
enrich: enrichFromRequest,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
export { parseError, createDrainPipeline, createStructuredError };
|
package/src/core.test.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { createUseLogger, getHeader } from './core';
|
|
3
|
-
|
|
4
|
-
describe('createUseLogger', () => {
|
|
5
|
-
it('throws clear adapter-specific error when called without active span', () => {
|
|
6
|
-
const useLogger = createUseLogger<{ requestId: string }>({
|
|
7
|
-
adapterName: 'test-framework',
|
|
8
|
-
enrich: (ctx) => ({ request_id: ctx.requestId }),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
expect(() => useLogger({ requestId: 'r1' })).toThrow(
|
|
12
|
-
'[autotel-adapters/test-framework] No active trace context.',
|
|
13
|
-
);
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('getHeader', () => {
|
|
18
|
-
it('reads from Headers-like object with get()', () => {
|
|
19
|
-
const headers = { get: (name: string) => (name === 'x-request-id' ? 'req-1' : null) };
|
|
20
|
-
expect(getHeader(headers, 'x-request-id')).toBe('req-1');
|
|
21
|
-
expect(getHeader(headers, 'x-missing')).toBeUndefined();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('reads from plain object with exact key match', () => {
|
|
25
|
-
const headers = { 'X-Request-Id': 'req-2' };
|
|
26
|
-
expect(getHeader(headers, 'X-Request-Id')).toBe('req-2');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('falls back to lowercase key lookup', () => {
|
|
30
|
-
const headers = { 'x-request-id': 'req-3' };
|
|
31
|
-
expect(getHeader(headers, 'X-Request-Id')).toBe('req-3');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('returns undefined for missing headers', () => {
|
|
35
|
-
expect(getHeader(undefined, 'x-request-id')).toBeUndefined();
|
|
36
|
-
expect(getHeader({}, 'x-request-id')).toBeUndefined();
|
|
37
|
-
});
|
|
38
|
-
});
|
package/src/core.ts
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import type { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
import {
|
|
3
|
-
createDrainPipeline,
|
|
4
|
-
createStructuredError,
|
|
5
|
-
getRequestLogger,
|
|
6
|
-
parseError,
|
|
7
|
-
trace,
|
|
8
|
-
type ParsedError,
|
|
9
|
-
type RequestLogger,
|
|
10
|
-
type RequestLoggerOptions,
|
|
11
|
-
type RequestLogSnapshot,
|
|
12
|
-
type DrainPipelineOptions,
|
|
13
|
-
type PipelineDrainFn,
|
|
14
|
-
type StructuredError,
|
|
15
|
-
type StructuredErrorInput,
|
|
16
|
-
} from 'autotel';
|
|
17
|
-
|
|
18
|
-
export interface AdapterUseLoggerOptions<TContext> {
|
|
19
|
-
adapterName: string;
|
|
20
|
-
enrich?: (context: TContext) => Record<string, unknown> | undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface AdapterToolkit<TContext> {
|
|
24
|
-
useLogger: (
|
|
25
|
-
context?: TContext,
|
|
26
|
-
options?: RequestLoggerOptions,
|
|
27
|
-
) => RequestLogger;
|
|
28
|
-
parseError: (error: unknown) => ParsedError;
|
|
29
|
-
createStructuredError: (input: StructuredErrorInput) => StructuredError;
|
|
30
|
-
createDrainPipeline: <T = unknown>(
|
|
31
|
-
options?: DrainPipelineOptions<T>,
|
|
32
|
-
) => (drain: (batch: T[]) => void | Promise<void>) => PipelineDrainFn<T>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function createUseLogger<TContext = unknown>(
|
|
36
|
-
options: AdapterUseLoggerOptions<TContext>,
|
|
37
|
-
) {
|
|
38
|
-
return function useLogger(
|
|
39
|
-
context?: TContext,
|
|
40
|
-
requestLoggerOptions?: RequestLoggerOptions,
|
|
41
|
-
): RequestLogger {
|
|
42
|
-
let logger: RequestLogger;
|
|
43
|
-
try {
|
|
44
|
-
logger = getRequestLogger(undefined, requestLoggerOptions);
|
|
45
|
-
} catch {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`[autotel-adapters/${options.adapterName}] No active trace context. ` +
|
|
48
|
-
`Wrap your handler with autotel trace instrumentation before calling useLogger().`,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (context && options.enrich) {
|
|
53
|
-
const extra = options.enrich(context);
|
|
54
|
-
if (extra && Object.keys(extra).length > 0) {
|
|
55
|
-
logger.set(extra);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return logger;
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface RequestRunnerOptions {
|
|
64
|
-
requestLoggerOptions?: RequestLoggerOptions;
|
|
65
|
-
/** Emit one wide event automatically when the handler settles. Default `true`. */
|
|
66
|
-
autoEmit?: boolean;
|
|
67
|
-
/** Fields merged into the wide event at emit time (e.g. response status). */
|
|
68
|
-
finalize?: () => Record<string, unknown> | undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Build a request runner bound to one framework's logger storage. The returned
|
|
73
|
-
* function opens a span, creates a request logger, runs `handler` inside the
|
|
74
|
-
* storage so `useLogger()` resolves it, records thrown errors, and emits one
|
|
75
|
-
* wide event when the handler settles (unless `autoEmit` is `false`).
|
|
76
|
-
*/
|
|
77
|
-
export function createRequestRunner(storage: AsyncLocalStorage<RequestLogger>) {
|
|
78
|
-
return function runRequest<T>(
|
|
79
|
-
spanName: string,
|
|
80
|
-
enrich: (log: RequestLogger) => void,
|
|
81
|
-
handler: () => T | Promise<T>,
|
|
82
|
-
options?: RequestRunnerOptions,
|
|
83
|
-
): Promise<T> {
|
|
84
|
-
const wrapped = trace(
|
|
85
|
-
{ name: spanName },
|
|
86
|
-
(ctx) => async (): Promise<T> => {
|
|
87
|
-
const log = getRequestLogger(ctx, options?.requestLoggerOptions);
|
|
88
|
-
enrich(log);
|
|
89
|
-
try {
|
|
90
|
-
return await storage.run(log, () => handler());
|
|
91
|
-
} catch (error) {
|
|
92
|
-
log.error(error instanceof Error ? error : new Error(String(error)));
|
|
93
|
-
throw error;
|
|
94
|
-
} finally {
|
|
95
|
-
if (options?.autoEmit !== false) {
|
|
96
|
-
log.emitNow(options?.finalize?.());
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
);
|
|
101
|
-
return wrapped();
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function createAdapterToolkit<TContext = unknown>(
|
|
106
|
-
options: AdapterUseLoggerOptions<TContext>,
|
|
107
|
-
): AdapterToolkit<TContext> {
|
|
108
|
-
return {
|
|
109
|
-
useLogger: createUseLogger(options),
|
|
110
|
-
parseError,
|
|
111
|
-
createStructuredError,
|
|
112
|
-
createDrainPipeline,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Description of a single adapter config field. `env` is the ordered list of
|
|
118
|
-
* environment variables to fall back to.
|
|
119
|
-
*/
|
|
120
|
-
export interface ConfigField<T> {
|
|
121
|
-
key: keyof T & string;
|
|
122
|
-
env?: string[];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function resolveEnv(envKeys?: string[]): string | undefined {
|
|
126
|
-
if (!envKeys) return undefined;
|
|
127
|
-
for (const key of envKeys) {
|
|
128
|
-
const value = process.env[key];
|
|
129
|
-
if (value) return value;
|
|
130
|
-
}
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Returns true when at least one env-backed field is not provided via
|
|
136
|
-
* `overrides`, meaning runtime config may still contribute and should be
|
|
137
|
-
* probed to preserve precedence (`overrides > runtime > env`).
|
|
138
|
-
*
|
|
139
|
-
* @example
|
|
140
|
-
* ```ts
|
|
141
|
-
* const FIELDS: ConfigField<MyAdapterConfig>[] = [
|
|
142
|
-
* { key: 'token', env: ['MY_ADAPTER_TOKEN'] },
|
|
143
|
-
* { key: 'endpoint', env: ['MY_ADAPTER_URL'] },
|
|
144
|
-
* ]
|
|
145
|
-
*
|
|
146
|
-
* if (shouldProbeRuntime(FIELDS, overrides)) {
|
|
147
|
-
* runtimeConfig = await loadRuntimeConfig()
|
|
148
|
-
* }
|
|
149
|
-
* ```
|
|
150
|
-
*/
|
|
151
|
-
export function shouldProbeRuntime<T>(
|
|
152
|
-
fields: ConfigField<T>[],
|
|
153
|
-
overrides?: Partial<T>,
|
|
154
|
-
): boolean {
|
|
155
|
-
return fields.some(({ key, env }) => {
|
|
156
|
-
if (!env || env.length === 0) return false;
|
|
157
|
-
if (overrides?.[key] !== undefined) return false;
|
|
158
|
-
return true;
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Resolve adapter configuration with the standard priority chain:
|
|
164
|
-
*
|
|
165
|
-
* 1. `overrides` passed to the adapter factory
|
|
166
|
-
* 2. `runtimeConfig.autotel.{namespace}.{key}` (if a probe was performed)
|
|
167
|
-
* 3. `runtimeConfig.{namespace}.{key}` (if a probe was performed)
|
|
168
|
-
* 4. `process.env[envKey]` for each env in `field.env`
|
|
169
|
-
*
|
|
170
|
-
* Pass an async `probe` to defer the runtime config lookup so it is only
|
|
171
|
-
* invoked when runtime resolution is needed (i.e. at least one env-backed
|
|
172
|
-
* field is not set by overrides). Adapters that have no probe target may pass
|
|
173
|
-
* `() => Promise.resolve(undefined)`.
|
|
174
|
-
*/
|
|
175
|
-
export async function resolveAdapterConfig<T>(
|
|
176
|
-
namespace: string,
|
|
177
|
-
fields: ConfigField<T>[],
|
|
178
|
-
overrides: Partial<T> | undefined,
|
|
179
|
-
probe: () => Promise<Record<string, any> | undefined>,
|
|
180
|
-
): Promise<Partial<T>> {
|
|
181
|
-
const runtimeConfig = shouldProbeRuntime(fields, overrides)
|
|
182
|
-
? await probe()
|
|
183
|
-
: undefined;
|
|
184
|
-
const autotelNs = runtimeConfig?.autotel?.[namespace];
|
|
185
|
-
const rootNs = runtimeConfig?.[namespace];
|
|
186
|
-
|
|
187
|
-
const config: Record<string, unknown> = {};
|
|
188
|
-
for (const { key, env } of fields) {
|
|
189
|
-
config[key] =
|
|
190
|
-
overrides?.[key] ??
|
|
191
|
-
autotelNs?.[key] ??
|
|
192
|
-
rootNs?.[key] ??
|
|
193
|
-
resolveEnv(env);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return config as Partial<T>;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export type HeadersLike =
|
|
200
|
-
| { get(name: string): string | null }
|
|
201
|
-
| Record<string, string | undefined>;
|
|
202
|
-
|
|
203
|
-
export function getHeader(
|
|
204
|
-
headers: HeadersLike | undefined,
|
|
205
|
-
name: string,
|
|
206
|
-
): string | undefined {
|
|
207
|
-
if (!headers) return undefined;
|
|
208
|
-
if ('get' in headers && typeof headers.get === 'function') {
|
|
209
|
-
return headers.get(name) ?? undefined;
|
|
210
|
-
}
|
|
211
|
-
const dictionary = headers as Record<string, string | undefined>;
|
|
212
|
-
const value = dictionary[name] ?? dictionary[name.toLowerCase()];
|
|
213
|
-
return typeof value === 'string' ? value : undefined;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export type {
|
|
217
|
-
RequestLogger,
|
|
218
|
-
RequestLoggerOptions,
|
|
219
|
-
RequestLogSnapshot,
|
|
220
|
-
ParsedError,
|
|
221
|
-
StructuredError,
|
|
222
|
-
StructuredErrorInput,
|
|
223
|
-
DrainPipelineOptions,
|
|
224
|
-
PipelineDrainFn,
|
|
225
|
-
};
|
package/src/express.test.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { useLogger, withAutotel } from './express';
|
|
3
|
-
import type { ExpressRequestLike, ExpressResponseLike } from './express';
|
|
4
|
-
|
|
5
|
-
const req = (over: Partial<ExpressRequestLike> = {}): ExpressRequestLike => ({
|
|
6
|
-
method: 'GET',
|
|
7
|
-
originalUrl: '/api/orders',
|
|
8
|
-
...over,
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
describe('express adapter', () => {
|
|
12
|
-
it('throws a clear error when useLogger is called outside a traced context', () => {
|
|
13
|
-
expect(() => useLogger(req())).toThrow(
|
|
14
|
-
'[autotel-adapters/express] No active trace context.',
|
|
15
|
-
);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('runs the handler with a request-scoped logger', async () => {
|
|
19
|
-
const handler = withAutotel((request, res: ExpressResponseLike) => {
|
|
20
|
-
useLogger(request).set({ feature: 'checkout' });
|
|
21
|
-
res.statusCode = 201;
|
|
22
|
-
return 'ok';
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
await expect(handler(req(), { statusCode: 200 })).resolves.toBe('ok');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('auto-emits one wide event with accumulated context and status', async () => {
|
|
29
|
-
const onEmit = vi.fn();
|
|
30
|
-
const handler = withAutotel(
|
|
31
|
-
(request, res: ExpressResponseLike) => {
|
|
32
|
-
useLogger(request).set({ user: 'u1' });
|
|
33
|
-
res.statusCode = 200;
|
|
34
|
-
return 'done';
|
|
35
|
-
},
|
|
36
|
-
{ requestLoggerOptions: { onEmit } },
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
await handler(req({ method: 'POST' }), { statusCode: 200 });
|
|
40
|
-
|
|
41
|
-
expect(onEmit).toHaveBeenCalledTimes(1);
|
|
42
|
-
const snapshot = onEmit.mock.calls[0]?.[0] as {
|
|
43
|
-
context: Record<string, unknown>;
|
|
44
|
-
};
|
|
45
|
-
expect(snapshot.context.user).toBe('u1');
|
|
46
|
-
expect(snapshot.context['http.request.method']).toBe('POST');
|
|
47
|
-
expect(snapshot.context['http.response.status_code']).toBe(200);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('does not emit when autoEmit is false', async () => {
|
|
51
|
-
const onEmit = vi.fn();
|
|
52
|
-
const handler = withAutotel(() => 'x', {
|
|
53
|
-
autoEmit: false,
|
|
54
|
-
requestLoggerOptions: { onEmit },
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
await handler(req(), { statusCode: 200 });
|
|
58
|
-
expect(onEmit).not.toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('records the error, emits, and forwards to next', async () => {
|
|
62
|
-
const onEmit = vi.fn();
|
|
63
|
-
const next = vi.fn();
|
|
64
|
-
const boom = new Error('boom');
|
|
65
|
-
const handler = withAutotel(
|
|
66
|
-
() => {
|
|
67
|
-
throw boom;
|
|
68
|
-
},
|
|
69
|
-
{ requestLoggerOptions: { onEmit } },
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
await expect(
|
|
73
|
-
handler(req(), { statusCode: 500 }, next),
|
|
74
|
-
).resolves.toBeUndefined();
|
|
75
|
-
expect(next).toHaveBeenCalledWith(boom);
|
|
76
|
-
expect(onEmit).toHaveBeenCalledTimes(1);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('rethrows when no next is provided', async () => {
|
|
80
|
-
const handler = withAutotel(() => {
|
|
81
|
-
throw new Error('no-next');
|
|
82
|
-
});
|
|
83
|
-
await expect(handler(req(), { statusCode: 500 })).rejects.toThrow('no-next');
|
|
84
|
-
});
|
|
85
|
-
});
|