autotel-adapters 0.1.0

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.
Files changed (69) hide show
  1. package/README.md +119 -0
  2. package/dist/chunk-2MUUBQJZ.js +43 -0
  3. package/dist/chunk-2MUUBQJZ.js.map +1 -0
  4. package/dist/chunk-F2K6UTRF.js +73 -0
  5. package/dist/chunk-F2K6UTRF.js.map +1 -0
  6. package/dist/chunk-FPHF553F.js +23 -0
  7. package/dist/chunk-FPHF553F.js.map +1 -0
  8. package/dist/chunk-HC3ZQAZV.js +84 -0
  9. package/dist/chunk-HC3ZQAZV.js.map +1 -0
  10. package/dist/chunk-LFDWJWP2.js +21 -0
  11. package/dist/chunk-LFDWJWP2.js.map +1 -0
  12. package/dist/chunk-VFTRQVDR.js +67 -0
  13. package/dist/chunk-VFTRQVDR.js.map +1 -0
  14. package/dist/cloudflare.cjs +138 -0
  15. package/dist/cloudflare.cjs.map +1 -0
  16. package/dist/cloudflare.d.cts +33 -0
  17. package/dist/cloudflare.d.ts +33 -0
  18. package/dist/cloudflare.js +4 -0
  19. package/dist/cloudflare.js.map +1 -0
  20. package/dist/core.cjs +47 -0
  21. package/dist/core.cjs.map +1 -0
  22. package/dist/core.d.cts +21 -0
  23. package/dist/core.d.ts +21 -0
  24. package/dist/core.js +3 -0
  25. package/dist/core.js.map +1 -0
  26. package/dist/hono.cjs +55 -0
  27. package/dist/hono.cjs.map +1 -0
  28. package/dist/hono.d.cts +9 -0
  29. package/dist/hono.d.ts +9 -0
  30. package/dist/hono.js +4 -0
  31. package/dist/hono.js.map +1 -0
  32. package/dist/index.cjs +258 -0
  33. package/dist/index.cjs.map +1 -0
  34. package/dist/index.d-DgrxjJLc.d.cts +21 -0
  35. package/dist/index.d-DgrxjJLc.d.ts +21 -0
  36. package/dist/index.d.cts +9 -0
  37. package/dist/index.d.ts +9 -0
  38. package/dist/index.js +8 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/next.cjs +127 -0
  41. package/dist/next.cjs.map +1 -0
  42. package/dist/next.d.cts +28 -0
  43. package/dist/next.d.ts +28 -0
  44. package/dist/next.js +4 -0
  45. package/dist/next.js.map +1 -0
  46. package/dist/nitro.cjs +101 -0
  47. package/dist/nitro.cjs.map +1 -0
  48. package/dist/nitro.d.cts +26 -0
  49. package/dist/nitro.d.ts +26 -0
  50. package/dist/nitro.js +4 -0
  51. package/dist/nitro.js.map +1 -0
  52. package/dist/tanstack.cjs +53 -0
  53. package/dist/tanstack.cjs.map +1 -0
  54. package/dist/tanstack.d.cts +14 -0
  55. package/dist/tanstack.d.ts +14 -0
  56. package/dist/tanstack.js +4 -0
  57. package/dist/tanstack.js.map +1 -0
  58. package/package.json +103 -0
  59. package/src/cloudflare.test.ts +31 -0
  60. package/src/cloudflare.ts +181 -0
  61. package/src/core.test.ts +38 -0
  62. package/src/core.ts +98 -0
  63. package/src/hono.ts +20 -0
  64. package/src/index.ts +6 -0
  65. package/src/next.test.ts +22 -0
  66. package/src/next.ts +127 -0
  67. package/src/nitro.test.ts +24 -0
  68. package/src/nitro.ts +111 -0
  69. package/src/tanstack.ts +23 -0
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "autotel-adapters",
3
+ "version": "0.1.0",
4
+ "description": "Framework adapters and composable DX helpers for autotel",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./core": {
16
+ "types": "./dist/core.d.ts",
17
+ "import": "./dist/core.js",
18
+ "require": "./dist/core.cjs"
19
+ },
20
+ "./hono": {
21
+ "types": "./dist/hono.d.ts",
22
+ "import": "./dist/hono.js",
23
+ "require": "./dist/hono.cjs"
24
+ },
25
+ "./tanstack": {
26
+ "types": "./dist/tanstack.d.ts",
27
+ "import": "./dist/tanstack.js",
28
+ "require": "./dist/tanstack.cjs"
29
+ },
30
+ "./next": {
31
+ "types": "./dist/next.d.ts",
32
+ "import": "./dist/next.js",
33
+ "require": "./dist/next.cjs"
34
+ },
35
+ "./nitro": {
36
+ "types": "./dist/nitro.d.ts",
37
+ "import": "./dist/nitro.js",
38
+ "require": "./dist/nitro.cjs"
39
+ },
40
+ "./cloudflare": {
41
+ "types": "./dist/cloudflare.d.ts",
42
+ "import": "./dist/cloudflare.js",
43
+ "require": "./dist/cloudflare.cjs"
44
+ }
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "src",
49
+ "README.md"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "type-check": "tsc --noEmit",
55
+ "lint": "eslint src",
56
+ "test": "vitest run",
57
+ "clean": "rimraf dist"
58
+ },
59
+ "dependencies": {
60
+ "autotel": "workspace:*"
61
+ },
62
+ "peerDependencies": {
63
+ "hono": ">=4.12.3",
64
+ "@tanstack/react-start": "^1.166.1",
65
+ "@tanstack/solid-start": "^1.166.1",
66
+ "next": ">=16.1.6",
67
+ "h3": "^1.15.5",
68
+ "nitropack": "^2.13.1"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "hono": {
72
+ "optional": true
73
+ },
74
+ "@tanstack/react-start": {
75
+ "optional": true
76
+ },
77
+ "@tanstack/solid-start": {
78
+ "optional": true
79
+ },
80
+ "next": {
81
+ "optional": true
82
+ },
83
+ "h3": {
84
+ "optional": true
85
+ },
86
+ "nitropack": {
87
+ "optional": true
88
+ }
89
+ },
90
+ "devDependencies": {
91
+ "@types/node": "^25.3.3",
92
+ "hono": "^4.12.3",
93
+ "rimraf": "^6.1.3",
94
+ "tsup": "^8.5.1",
95
+ "typescript": "^5.9.3",
96
+ "vitest": "^4.0.18"
97
+ },
98
+ "repository": {
99
+ "type": "git",
100
+ "url": "https://github.com/jagreehal/autotel",
101
+ "directory": "packages/autotel-adapters"
102
+ }
103
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } 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
+
@@ -0,0 +1,181 @@
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
+ }
40
+
41
+ const requestLoggers = new WeakMap<object, RequestLogger>();
42
+
43
+ function enrichFromRequest(
44
+ request?: CloudflareRequestLike,
45
+ ): Record<string, unknown> | undefined {
46
+ if (!request) return undefined;
47
+
48
+ let route = '/';
49
+ if (request.url) {
50
+ try {
51
+ route = new URL(request.url).pathname;
52
+ } catch {
53
+ route = request.url;
54
+ }
55
+ }
56
+
57
+ const requestId =
58
+ getHeader(request.headers, 'x-request-id') ??
59
+ getHeader(request.headers, 'cf-ray');
60
+
61
+ return {
62
+ ...(request.method ? { 'http.request.method': request.method } : {}),
63
+ ...(request.url ? { 'url.full': request.url } : {}),
64
+ ...(route ? { 'http.route': route } : {}),
65
+ ...(requestId ? { 'http.request.id': requestId } : {}),
66
+ ...(request.cf?.country ? { 'cloudflare.country': request.cf.country } : {}),
67
+ ...(request.cf?.colo ? { 'cloudflare.colo': request.cf.colo } : {}),
68
+ ...(request.cf?.city ? { 'cloudflare.city': request.cf.city } : {}),
69
+ };
70
+ }
71
+
72
+ const baseUseLogger = createUseLogger<CloudflareRequestLike>({
73
+ adapterName: 'cloudflare',
74
+ enrich: enrichFromRequest,
75
+ });
76
+
77
+ export function useLogger(
78
+ request?: CloudflareRequestLike,
79
+ requestLoggerOptions?: RequestLoggerOptions,
80
+ ): RequestLogger {
81
+ if (request) {
82
+ const existing = requestLoggers.get(request as object);
83
+ if (existing) return existing;
84
+ }
85
+ return baseUseLogger(request, requestLoggerOptions);
86
+ }
87
+
88
+ export function withAutotelFetch<
89
+ TEnv,
90
+ TRequest extends CloudflareRequestLike,
91
+ TContext extends CloudflareExecutionContextLike,
92
+ TReturn,
93
+ >(
94
+ handler: (
95
+ request: TRequest,
96
+ env: TEnv,
97
+ ctx: TContext,
98
+ ) => TReturn | Promise<TReturn>,
99
+ options?: CloudflareWithAutotelOptions<TEnv>,
100
+ ): (
101
+ request: TRequest,
102
+ env: TEnv,
103
+ ctx: TContext,
104
+ ) => Promise<TReturn> {
105
+ return async (
106
+ request: TRequest,
107
+ env: TEnv,
108
+ executionContext: TContext,
109
+ ): Promise<TReturn> => {
110
+ const spanName =
111
+ typeof options?.spanName === 'function'
112
+ ? options.spanName(request, env)
113
+ : (options?.spanName ?? `cloudflare.${request.method ?? 'request'}`);
114
+
115
+ const wrapped = trace(
116
+ { name: spanName },
117
+ (ctx) => async (
118
+ innerRequest: TRequest,
119
+ innerEnv: TEnv,
120
+ innerExecutionContext: TContext,
121
+ ) => {
122
+ const log = getRequestLogger(ctx, options?.requestLoggerOptions);
123
+ const auto = enrichFromRequest(innerRequest);
124
+ if (auto && Object.keys(auto).length > 0) {
125
+ log.set(auto);
126
+ }
127
+ const custom = options?.enrich?.(
128
+ innerRequest,
129
+ innerEnv,
130
+ innerExecutionContext,
131
+ );
132
+ if (custom && Object.keys(custom).length > 0) {
133
+ log.set(custom);
134
+ }
135
+
136
+ requestLoggers.set(innerRequest as object, log);
137
+ try {
138
+ return await handler(innerRequest, innerEnv, innerExecutionContext);
139
+ } finally {
140
+ requestLoggers.delete(innerRequest as object);
141
+ }
142
+ },
143
+ );
144
+
145
+ return await wrapped(request, env, executionContext);
146
+ };
147
+ }
148
+
149
+ export function createCloudflareAdapter<TEnv = unknown>(
150
+ options?: CloudflareWithAutotelOptions<TEnv>,
151
+ ) {
152
+ return {
153
+ withAutotelFetch: <
154
+ TRequest extends CloudflareRequestLike,
155
+ TContext extends CloudflareExecutionContextLike,
156
+ TReturn,
157
+ >(
158
+ handler: (
159
+ request: TRequest,
160
+ env: TEnv,
161
+ ctx: TContext,
162
+ ) => TReturn | Promise<TReturn>,
163
+ ) => withAutotelFetch(handler, options),
164
+ useLogger,
165
+ parseError: (error: unknown): ParsedError => parseError(error),
166
+ createStructuredError: (
167
+ input: StructuredErrorInput,
168
+ ): StructuredError => createStructuredError(input),
169
+ createDrainPipeline: <T = unknown>(
170
+ drainOptions?: DrainPipelineOptions<T>,
171
+ ): ((batchDrain: (batch: T[]) => void | Promise<void>) => PipelineDrainFn<T>) =>
172
+ createDrainPipeline(drainOptions),
173
+ };
174
+ }
175
+
176
+ export const cloudflareToolkit = createAdapterToolkit<CloudflareRequestLike>({
177
+ adapterName: 'cloudflare',
178
+ enrich: enrichFromRequest,
179
+ });
180
+
181
+ export { parseError, createDrainPipeline, createStructuredError };
@@ -0,0 +1,38 @@
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 ADDED
@@ -0,0 +1,98 @@
1
+ import {
2
+ createDrainPipeline,
3
+ createStructuredError,
4
+ getRequestLogger,
5
+ parseError,
6
+ type ParsedError,
7
+ type RequestLogger,
8
+ type RequestLoggerOptions,
9
+ type RequestLogSnapshot,
10
+ type DrainPipelineOptions,
11
+ type PipelineDrainFn,
12
+ type StructuredError,
13
+ type StructuredErrorInput,
14
+ } from 'autotel';
15
+
16
+ export interface AdapterUseLoggerOptions<TContext> {
17
+ adapterName: string;
18
+ enrich?: (context: TContext) => Record<string, unknown> | undefined;
19
+ }
20
+
21
+ export interface AdapterToolkit<TContext> {
22
+ useLogger: (
23
+ context?: TContext,
24
+ options?: RequestLoggerOptions,
25
+ ) => RequestLogger;
26
+ parseError: (error: unknown) => ParsedError;
27
+ createStructuredError: (input: StructuredErrorInput) => StructuredError;
28
+ createDrainPipeline: <T = unknown>(
29
+ options?: DrainPipelineOptions<T>,
30
+ ) => (drain: (batch: T[]) => void | Promise<void>) => PipelineDrainFn<T>;
31
+ }
32
+
33
+ export function createUseLogger<TContext = unknown>(
34
+ options: AdapterUseLoggerOptions<TContext>,
35
+ ) {
36
+ return function useLogger(
37
+ context?: TContext,
38
+ requestLoggerOptions?: RequestLoggerOptions,
39
+ ): RequestLogger {
40
+ let logger: RequestLogger;
41
+ try {
42
+ logger = getRequestLogger(undefined, requestLoggerOptions);
43
+ } catch {
44
+ throw new Error(
45
+ `[autotel-adapters/${options.adapterName}] No active trace context. ` +
46
+ `Wrap your handler with autotel trace instrumentation before calling useLogger().`,
47
+ );
48
+ }
49
+
50
+ if (context && options.enrich) {
51
+ const extra = options.enrich(context);
52
+ if (extra && Object.keys(extra).length > 0) {
53
+ logger.set(extra);
54
+ }
55
+ }
56
+
57
+ return logger;
58
+ };
59
+ }
60
+
61
+ export function createAdapterToolkit<TContext = unknown>(
62
+ options: AdapterUseLoggerOptions<TContext>,
63
+ ): AdapterToolkit<TContext> {
64
+ return {
65
+ useLogger: createUseLogger(options),
66
+ parseError,
67
+ createStructuredError,
68
+ createDrainPipeline,
69
+ };
70
+ }
71
+
72
+ export type HeadersLike =
73
+ | { get(name: string): string | null }
74
+ | Record<string, string | undefined>;
75
+
76
+ export function getHeader(
77
+ headers: HeadersLike | undefined,
78
+ name: string,
79
+ ): string | undefined {
80
+ if (!headers) return undefined;
81
+ if ('get' in headers && typeof headers.get === 'function') {
82
+ return headers.get(name) ?? undefined;
83
+ }
84
+ const dictionary = headers as Record<string, string | undefined>;
85
+ const value = dictionary[name] ?? dictionary[name.toLowerCase()];
86
+ return typeof value === 'string' ? value : undefined;
87
+ }
88
+
89
+ export type {
90
+ RequestLogger,
91
+ RequestLoggerOptions,
92
+ RequestLogSnapshot,
93
+ ParsedError,
94
+ StructuredError,
95
+ StructuredErrorInput,
96
+ DrainPipelineOptions,
97
+ PipelineDrainFn,
98
+ };
package/src/hono.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { Context } from 'hono';
2
+ import { createUseLogger, createAdapterToolkit } from './core';
3
+
4
+ export const useLogger = createUseLogger<Context>({
5
+ adapterName: 'hono',
6
+ enrich: (c) => ({
7
+ 'http.request.method': c.req.method,
8
+ 'url.full': c.req.url,
9
+ 'http.route': c.req.path,
10
+ }),
11
+ });
12
+
13
+ export const honoToolkit = createAdapterToolkit<Context>({
14
+ adapterName: 'hono',
15
+ enrich: (c) => ({
16
+ 'http.request.method': c.req.method,
17
+ 'url.full': c.req.url,
18
+ 'http.route': c.req.path,
19
+ }),
20
+ });
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './core';
2
+ export { createNextAdapter, withAutotel } from './next';
3
+ export { createNitroAdapter, withAutotelEventHandler } from './nitro';
4
+ export { createCloudflareAdapter, withAutotelFetch } from './cloudflare';
5
+ export { honoToolkit } from './hono';
6
+ export { tanstackToolkit } from './tanstack';
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { useLogger, withAutotel } from './next';
3
+
4
+ describe('next adapter', () => {
5
+ it('throws clear error when useLogger is called outside traced context', () => {
6
+ expect(() => useLogger({ method: 'GET', url: '/api/orders' })).toThrow(
7
+ '[autotel-adapters/next] No active trace context.',
8
+ );
9
+ });
10
+
11
+ it('provides request-scoped logger inside withAutotel()', async () => {
12
+ const handler = withAutotel(async (request: { url: string }) => {
13
+ const log = useLogger(request);
14
+ log.set({ feature: 'checkout' });
15
+ return 'ok';
16
+ });
17
+
18
+ await expect(handler({ url: 'https://example.com/orders' })).resolves.toBe(
19
+ 'ok',
20
+ );
21
+ });
22
+ });
package/src/next.ts ADDED
@@ -0,0 +1,127 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import {
3
+ createDrainPipeline,
4
+ getRequestLogger,
5
+ parseError,
6
+ trace,
7
+ createStructuredError,
8
+ type RequestLogger,
9
+ type RequestLoggerOptions,
10
+ type ParsedError,
11
+ type DrainPipelineOptions,
12
+ type PipelineDrainFn,
13
+ type StructuredError,
14
+ type StructuredErrorInput,
15
+ } from 'autotel';
16
+ import { createAdapterToolkit, createUseLogger, getHeader } from './core';
17
+
18
+ export interface NextRequestLike {
19
+ method?: string;
20
+ url?: string;
21
+ headers?:
22
+ | { get(name: string): string | null }
23
+ | Record<string, string | undefined>;
24
+ }
25
+
26
+ export interface NextWithAutotelOptions {
27
+ spanName?: string | ((request?: NextRequestLike) => string);
28
+ requestLoggerOptions?: RequestLoggerOptions;
29
+ enrich?: (request?: NextRequestLike) => Record<string, unknown> | undefined;
30
+ }
31
+
32
+ const nextLoggerStorage = new AsyncLocalStorage<RequestLogger>();
33
+
34
+ function enrichFromRequest(
35
+ request?: NextRequestLike,
36
+ ): Record<string, unknown> | undefined {
37
+ if (!request) return undefined;
38
+
39
+ let route = '/';
40
+ if (request.url) {
41
+ try {
42
+ route = new URL(request.url).pathname;
43
+ } catch {
44
+ route = request.url;
45
+ }
46
+ }
47
+ const requestId = getHeader(request.headers, 'x-request-id');
48
+
49
+ return {
50
+ ...(request.method ? { 'http.request.method': request.method } : {}),
51
+ ...(request.url ? { 'url.full': request.url } : {}),
52
+ ...(route ? { 'http.route': route } : {}),
53
+ ...(requestId ? { 'http.request.header.x-request-id': requestId } : {}),
54
+ };
55
+ }
56
+
57
+ const baseUseLogger = createUseLogger<NextRequestLike>({
58
+ adapterName: 'next',
59
+ enrich: enrichFromRequest,
60
+ });
61
+
62
+ export function useLogger(
63
+ request?: NextRequestLike,
64
+ requestLoggerOptions?: RequestLoggerOptions,
65
+ ): RequestLogger {
66
+ const logger = nextLoggerStorage.getStore();
67
+ if (logger) return logger;
68
+ return baseUseLogger(request, requestLoggerOptions);
69
+ }
70
+
71
+ export function withAutotel<TArgs extends unknown[], TReturn>(
72
+ handler: (...args: TArgs) => TReturn | Promise<TReturn>,
73
+ options?: NextWithAutotelOptions,
74
+ ): (...args: TArgs) => Promise<TReturn> {
75
+ return async (...args: TArgs): Promise<TReturn> => {
76
+ const request = args[0] as NextRequestLike | undefined;
77
+ const spanName =
78
+ typeof options?.spanName === 'function'
79
+ ? options.spanName(request)
80
+ : (options?.spanName ?? 'next.request');
81
+
82
+ const wrapped = trace(
83
+ { name: spanName },
84
+ (ctx) => async (...innerArgs: TArgs) => {
85
+ const innerRequest = innerArgs[0] as NextRequestLike | undefined;
86
+ const log = getRequestLogger(ctx, options?.requestLoggerOptions);
87
+ const auto = enrichFromRequest(innerRequest);
88
+ if (auto && Object.keys(auto).length > 0) {
89
+ log.set(auto);
90
+ }
91
+ const custom = options?.enrich?.(innerRequest);
92
+ if (custom && Object.keys(custom).length > 0) {
93
+ log.set(custom);
94
+ }
95
+ return await nextLoggerStorage.run(log, async () => handler(...innerArgs));
96
+ },
97
+ );
98
+ return await wrapped(...args);
99
+ };
100
+ }
101
+
102
+ export function createNextAdapter(options?: NextWithAutotelOptions) {
103
+ return {
104
+ withAutotel: <TArgs extends unknown[], TReturn>(
105
+ handler: (...args: TArgs) => TReturn | Promise<TReturn>,
106
+ ) => withAutotel(handler, options),
107
+ useLogger: (
108
+ request?: NextRequestLike,
109
+ requestLoggerOptions?: RequestLoggerOptions,
110
+ ): RequestLogger => useLogger(request, requestLoggerOptions),
111
+ parseError: (error: unknown): ParsedError => parseError(error),
112
+ createStructuredError: (
113
+ input: StructuredErrorInput,
114
+ ): StructuredError => createStructuredError(input),
115
+ createDrainPipeline: <T = unknown>(
116
+ drainOptions?: DrainPipelineOptions<T>,
117
+ ): ((batchDrain: (batch: T[]) => void | Promise<void>) => PipelineDrainFn<T>) =>
118
+ createDrainPipeline(drainOptions),
119
+ };
120
+ }
121
+
122
+ export const nextToolkit = createAdapterToolkit<NextRequestLike>({
123
+ adapterName: 'next',
124
+ enrich: enrichFromRequest,
125
+ });
126
+
127
+ export { parseError, createDrainPipeline, createStructuredError };
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { useLogger, withAutotelEventHandler } from './nitro';
3
+
4
+ describe('nitro adapter', () => {
5
+ it('throws clear error when useLogger is called outside traced context', () => {
6
+ expect(() =>
7
+ useLogger({ method: 'GET', path: '/api/orders', context: {} }),
8
+ ).toThrow('[autotel-adapters/nitro] No active trace context.');
9
+ });
10
+
11
+ it('provides request-scoped logger inside withAutotelEventHandler()', async () => {
12
+ const handler = withAutotelEventHandler(
13
+ async (event: { path: string; context: Record<string, unknown> }) => {
14
+ const log = useLogger(event, 'api-service');
15
+ log.set({ route: event.path });
16
+ return { ok: true };
17
+ },
18
+ );
19
+
20
+ await expect(
21
+ handler({ path: '/orders', context: {} }),
22
+ ).resolves.toMatchObject({ ok: true });
23
+ });
24
+ });