@amkisko/json-logging 0.0.0-stage → 0.1.1
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/LICENSE.md +21 -0
- package/README.md +231 -2
- package/dist/bun-serve.d.ts +30 -0
- package/dist/bun-serve.js +60 -0
- package/dist/console-bridge.d.ts +32 -0
- package/dist/console-bridge.js +158 -0
- package/dist/console-state.d.ts +19 -0
- package/dist/console-state.js +46 -0
- package/dist/console.d.ts +18 -0
- package/dist/console.js +17 -0
- package/dist/context.d.ts +40 -0
- package/dist/context.js +162 -0
- package/dist/express.d.ts +41 -0
- package/dist/express.js +48 -0
- package/dist/fastify.d.ts +37 -0
- package/dist/fastify.js +53 -0
- package/dist/hono.d.ts +37 -0
- package/dist/hono.js +50 -0
- package/dist/http-ambient.d.ts +12 -0
- package/dist/http-ambient.js +20 -0
- package/dist/http-context.d.ts +65 -0
- package/dist/http-context.js +156 -0
- package/dist/http.d.ts +24 -0
- package/dist/http.js +34 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +26 -0
- package/dist/json-logging.d.ts +9 -0
- package/dist/json-logging.js +12 -0
- package/dist/koa.d.ts +34 -0
- package/dist/koa.js +54 -0
- package/dist/logger.d.ts +46 -0
- package/dist/logger.js +130 -0
- package/dist/next.d.ts +41 -0
- package/dist/next.js +35 -0
- package/dist/objects.d.ts +12 -0
- package/dist/objects.js +18 -0
- package/dist/redact.d.ts +9 -0
- package/dist/redact.js +77 -0
- package/dist/remix.d.ts +43 -0
- package/dist/remix.js +38 -0
- package/dist/sanitize.d.ts +25 -0
- package/dist/sanitize.js +153 -0
- package/dist/serialize.d.ts +31 -0
- package/dist/serialize.js +203 -0
- package/dist/severity.d.ts +10 -0
- package/dist/severity.js +25 -0
- package/dist/sinks.d.ts +14 -0
- package/dist/sinks.js +66 -0
- package/dist/timestamp.d.ts +15 -0
- package/dist/timestamp.js +47 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +2 -0
- package/package.json +98 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Key/value fields stored in the ambient request context. */
|
|
2
|
+
export type ContextFields = Record<string, unknown>;
|
|
3
|
+
type Store = {
|
|
4
|
+
context: ContextFields;
|
|
5
|
+
tags: string[];
|
|
6
|
+
};
|
|
7
|
+
/** Minimal ALS surface used for request/block context (run + getStore only). */
|
|
8
|
+
export type AlsInstance<T> = {
|
|
9
|
+
run<R>(store: T, callback: () => R): R;
|
|
10
|
+
getStore(): T | undefined;
|
|
11
|
+
};
|
|
12
|
+
/** Constructor for {@link AlsInstance}, matching `node:async_hooks`.AsyncLocalStorage. */
|
|
13
|
+
export type AlsConstructor = new <T>() => AlsInstance<T>;
|
|
14
|
+
/** True when this runtime can construct AsyncLocalStorage for ambient context. */
|
|
15
|
+
export declare function isAsyncLocalStorageAvailable(): boolean;
|
|
16
|
+
/** Test helper: force ALS constructor resolution (`null` = unavailable). */
|
|
17
|
+
export declare function setAlsConstructorForTests(value: AlsConstructor | null | undefined): void;
|
|
18
|
+
/** Test helper: pass null to simulate a runtime without AsyncLocalStorage. */
|
|
19
|
+
export declare function setAlsOverrideForTests(value: AlsInstance<Store> | null | undefined): void;
|
|
20
|
+
export declare function readStore(als: AlsInstance<Store> | undefined): Store | undefined;
|
|
21
|
+
/** Copy of the current ambient context fields, or undefined outside a scope. */
|
|
22
|
+
export declare function getContext(): ContextFields | undefined;
|
|
23
|
+
/** Copy of nested `tagged` block tags for the current ALS scope. */
|
|
24
|
+
export declare function getBlockTags(): string[];
|
|
25
|
+
/** One store read for the log hot path. Returns live references; do not mutate. */
|
|
26
|
+
export declare function peekLogStore(): {
|
|
27
|
+
context: ContextFields | undefined;
|
|
28
|
+
tags: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Run `fn` with ambient context fields merged for the duration of the call.
|
|
32
|
+
* Without AsyncLocalStorage the callback still runs; context is not retained.
|
|
33
|
+
*/
|
|
34
|
+
export declare function withContext<T>(fields: ContextFields, fn: () => T | Promise<T>): T | Promise<T>;
|
|
35
|
+
/** Nested tag scope used by {@link Logger.tagged} block form. */
|
|
36
|
+
export declare function withBlockTags<T>(tags: readonly string[], fn: () => T | Promise<T>): T | Promise<T>;
|
|
37
|
+
/** Test helper: clear the global ALS singleton between suites. */
|
|
38
|
+
export declare function resetContextStorageForTests(): void;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=context.d.ts.map
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const GLOBAL_KEY = "__amkisko_json_logging_als__";
|
|
2
|
+
let alsOverride;
|
|
3
|
+
let alsConstructorOverride;
|
|
4
|
+
/** `false` means not resolved yet; `undefined` means unavailable after resolve. */
|
|
5
|
+
let cachedConstructor = false;
|
|
6
|
+
function isAlsConstructor(value) {
|
|
7
|
+
return typeof value === "function";
|
|
8
|
+
}
|
|
9
|
+
function readProcessBuiltin(id) {
|
|
10
|
+
const proc = Reflect.get(globalThis, "process");
|
|
11
|
+
if (typeof proc !== "object" || proc === null) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const getBuiltinModule = Reflect.get(proc, "getBuiltinModule");
|
|
15
|
+
if (typeof getBuiltinModule !== "function") {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return Reflect.apply(getBuiltinModule, proc, [id]);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Soft-load AsyncLocalStorage without a static `node:async_hooks` import so browsers
|
|
22
|
+
* and Workers without Node builtins can still import this module for basic logging.
|
|
23
|
+
*/
|
|
24
|
+
function loadAlsConstructor() {
|
|
25
|
+
if (alsConstructorOverride === null) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (alsConstructorOverride !== undefined) {
|
|
29
|
+
return alsConstructorOverride;
|
|
30
|
+
}
|
|
31
|
+
if (cachedConstructor !== false) {
|
|
32
|
+
return cachedConstructor;
|
|
33
|
+
}
|
|
34
|
+
for (const id of ["async_hooks", "node:async_hooks"]) {
|
|
35
|
+
try {
|
|
36
|
+
const mod = readProcessBuiltin(id);
|
|
37
|
+
if (typeof mod !== "object" || mod === null) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const ctor = Reflect.get(mod, "AsyncLocalStorage");
|
|
41
|
+
if (isAlsConstructor(ctor)) {
|
|
42
|
+
cachedConstructor = ctor;
|
|
43
|
+
return cachedConstructor;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// try next id
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
cachedConstructor = undefined;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/** True when this runtime can construct AsyncLocalStorage for ambient context. */
|
|
54
|
+
export function isAsyncLocalStorageAvailable() {
|
|
55
|
+
return loadAlsConstructor() !== undefined;
|
|
56
|
+
}
|
|
57
|
+
/** Test helper: force ALS constructor resolution (`null` = unavailable). */
|
|
58
|
+
export function setAlsConstructorForTests(value) {
|
|
59
|
+
alsConstructorOverride = value;
|
|
60
|
+
cachedConstructor = false;
|
|
61
|
+
}
|
|
62
|
+
/** Test helper: pass null to simulate a runtime without AsyncLocalStorage. */
|
|
63
|
+
export function setAlsOverrideForTests(value) {
|
|
64
|
+
alsOverride = value;
|
|
65
|
+
}
|
|
66
|
+
function getAls() {
|
|
67
|
+
if (alsOverride === null) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
if (alsOverride !== undefined) {
|
|
71
|
+
return alsOverride;
|
|
72
|
+
}
|
|
73
|
+
const Ctor = loadAlsConstructor();
|
|
74
|
+
if (!Ctor) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const globalRef = globalThis;
|
|
78
|
+
if (!globalRef[GLOBAL_KEY]) {
|
|
79
|
+
globalRef[GLOBAL_KEY] = new Ctor();
|
|
80
|
+
}
|
|
81
|
+
return globalRef[GLOBAL_KEY];
|
|
82
|
+
}
|
|
83
|
+
export function readStore(als) {
|
|
84
|
+
if (!als) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return als.getStore();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function currentStore() {
|
|
95
|
+
return readStore(getAls());
|
|
96
|
+
}
|
|
97
|
+
/** Copy of the current ambient context fields, or undefined outside a scope. */
|
|
98
|
+
export function getContext() {
|
|
99
|
+
const store = currentStore();
|
|
100
|
+
if (!store) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return { ...store.context };
|
|
104
|
+
}
|
|
105
|
+
/** Copy of nested `tagged` block tags for the current ALS scope. */
|
|
106
|
+
export function getBlockTags() {
|
|
107
|
+
const store = currentStore();
|
|
108
|
+
if (!store || store.tags.length === 0) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return [...store.tags];
|
|
112
|
+
}
|
|
113
|
+
/** One store read for the log hot path. Returns live references; do not mutate. */
|
|
114
|
+
export function peekLogStore() {
|
|
115
|
+
const store = currentStore();
|
|
116
|
+
if (!store) {
|
|
117
|
+
return { context: undefined, tags: [] };
|
|
118
|
+
}
|
|
119
|
+
return { context: store.context, tags: store.tags };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Run `fn` with ambient context fields merged for the duration of the call.
|
|
123
|
+
* Without AsyncLocalStorage the callback still runs; context is not retained.
|
|
124
|
+
*/
|
|
125
|
+
export function withContext(fields, fn) {
|
|
126
|
+
const als = getAls();
|
|
127
|
+
if (!als) {
|
|
128
|
+
return fn();
|
|
129
|
+
}
|
|
130
|
+
const parent = currentStore();
|
|
131
|
+
const next = {
|
|
132
|
+
context: { ...(parent?.context ?? {}), ...fields },
|
|
133
|
+
tags: parent?.tags ? [...parent.tags] : [],
|
|
134
|
+
};
|
|
135
|
+
return als.run(next, fn);
|
|
136
|
+
}
|
|
137
|
+
/** Nested tag scope used by {@link Logger.tagged} block form. */
|
|
138
|
+
export function withBlockTags(tags, fn) {
|
|
139
|
+
const als = getAls();
|
|
140
|
+
if (!als) {
|
|
141
|
+
return fn();
|
|
142
|
+
}
|
|
143
|
+
const parent = currentStore();
|
|
144
|
+
const next = {
|
|
145
|
+
context: parent?.context ? { ...parent.context } : {},
|
|
146
|
+
tags: [...(parent?.tags ?? []), ...tags],
|
|
147
|
+
};
|
|
148
|
+
return als.run(next, fn);
|
|
149
|
+
}
|
|
150
|
+
/** Test helper: clear the global ALS singleton between suites. */
|
|
151
|
+
export function resetContextStorageForTests() {
|
|
152
|
+
const globalRef = globalThis;
|
|
153
|
+
const Ctor = loadAlsConstructor();
|
|
154
|
+
if (Ctor) {
|
|
155
|
+
globalRef[GLOBAL_KEY] = new Ctor();
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
delete globalRef[GLOBAL_KEY];
|
|
159
|
+
}
|
|
160
|
+
alsOverride = undefined;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware that attaches request context (and optional completion on `finish`).
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { withExpressLogging } from "@amkisko/json-logging/express";
|
|
7
|
+
* app.use(withExpressLogging({ logCompletion: true }));
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import type { Logger } from "./logger.js";
|
|
13
|
+
import type { LoggerOptions } from "./types.js";
|
|
14
|
+
/** Minimal Express request shape (no Express runtime dependency). */
|
|
15
|
+
export type ExpressRequestLike = {
|
|
16
|
+
method?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
originalUrl?: string;
|
|
19
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
20
|
+
};
|
|
21
|
+
/** Minimal Express response shape. */
|
|
22
|
+
export type ExpressResponseLike = {
|
|
23
|
+
statusCode?: number;
|
|
24
|
+
on?: (event: string, listener: () => void) => unknown;
|
|
25
|
+
};
|
|
26
|
+
/** Options for {@link withExpressLogging}. */
|
|
27
|
+
export type WithExpressLoggingOptions = {
|
|
28
|
+
headerName?: string;
|
|
29
|
+
logger?: Logger;
|
|
30
|
+
loggerOptions?: LoggerOptions;
|
|
31
|
+
headersAllowlist?: readonly string[];
|
|
32
|
+
logCompletion?: boolean;
|
|
33
|
+
};
|
|
34
|
+
/** Express-style middleware function. */
|
|
35
|
+
export type ExpressMiddleware = (req: ExpressRequestLike, res: ExpressResponseLike, next: (error?: unknown) => void) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Build Express middleware: `(req, res, next) => ...`.
|
|
38
|
+
* Bind loggers via `@amkisko/json-logging/http`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function withExpressLogging(options?: WithExpressLoggingOptions): ExpressMiddleware;
|
|
41
|
+
//# sourceMappingURL=express.d.ts.map
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware that attaches request context (and optional completion on `finish`).
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { withExpressLogging } from "@amkisko/json-logging/express";
|
|
7
|
+
* app.use(withExpressLogging({ logCompletion: true }));
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { bindLoggerFromOptions, getAmbientHttpLogger } from "./http-ambient.js";
|
|
13
|
+
import { httpFieldsFrom, logRequestCompletion, withHttpContext, } from "./http-context.js";
|
|
14
|
+
/**
|
|
15
|
+
* Build Express middleware: `(req, res, next) => ...`.
|
|
16
|
+
* Bind loggers via `@amkisko/json-logging/http`.
|
|
17
|
+
*/
|
|
18
|
+
export function withExpressLogging(options = {}) {
|
|
19
|
+
bindLoggerFromOptions(options);
|
|
20
|
+
const headerName = options.headerName ?? "x-request-id";
|
|
21
|
+
return (req, res, next) => {
|
|
22
|
+
const input = {
|
|
23
|
+
method: req.method,
|
|
24
|
+
url: req.originalUrl ?? req.url,
|
|
25
|
+
headers: req.headers,
|
|
26
|
+
};
|
|
27
|
+
const started = Date.now();
|
|
28
|
+
const fields = httpFieldsFrom(input, headerName);
|
|
29
|
+
void withHttpContext(input, () => {
|
|
30
|
+
if (options.logCompletion && typeof res.on === "function") {
|
|
31
|
+
res.on("finish", () => {
|
|
32
|
+
const logger = options.logger ?? getAmbientHttpLogger();
|
|
33
|
+
if (!logger) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
logRequestCompletion(logger, {
|
|
37
|
+
method: typeof fields.method === "string" ? fields.method : undefined,
|
|
38
|
+
path: typeof fields.path === "string" ? fields.path : undefined,
|
|
39
|
+
status: typeof res.statusCode === "number" ? res.statusCode : 0,
|
|
40
|
+
elapsedMs: Date.now() - started,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
next();
|
|
45
|
+
}, options);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fastify route-handler wrap for request-scoped log context (and optional completion).
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { withFastifyLogging } from "@amkisko/json-logging/fastify";
|
|
7
|
+
* app.get("/", withFastifyLogging(async (req, reply) => reply.send("ok")));
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import type { Logger } from "./logger.js";
|
|
13
|
+
import type { LoggerOptions } from "./types.js";
|
|
14
|
+
/** Minimal Fastify request shape (no Fastify runtime dependency). */
|
|
15
|
+
export type FastifyRequestLike = {
|
|
16
|
+
method?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
19
|
+
};
|
|
20
|
+
/** Minimal Fastify reply shape. */
|
|
21
|
+
export type FastifyReplyLike = {
|
|
22
|
+
statusCode?: number;
|
|
23
|
+
};
|
|
24
|
+
/** Options for {@link withFastifyLogging}. */
|
|
25
|
+
export type WithFastifyLoggingOptions = {
|
|
26
|
+
headerName?: string;
|
|
27
|
+
logger?: Logger;
|
|
28
|
+
loggerOptions?: LoggerOptions;
|
|
29
|
+
headersAllowlist?: readonly string[];
|
|
30
|
+
logCompletion?: boolean;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Wrap a Fastify route handler so ALS context covers the handler body.
|
|
34
|
+
* Bind loggers via `./http`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withFastifyLogging<TRequest extends FastifyRequestLike, TReply extends FastifyReplyLike, TResult>(handler: (this: unknown, request: TRequest, reply: TReply) => TResult | Promise<TResult>, options?: WithFastifyLoggingOptions): (this: unknown, request: TRequest, reply: TReply) => Promise<TResult>;
|
|
37
|
+
//# sourceMappingURL=fastify.d.ts.map
|
package/dist/fastify.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fastify route-handler wrap for request-scoped log context (and optional completion).
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { withFastifyLogging } from "@amkisko/json-logging/fastify";
|
|
7
|
+
* app.get("/", withFastifyLogging(async (req, reply) => reply.send("ok")));
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { bindLoggerFromOptions, getAmbientHttpLogger } from "./http-ambient.js";
|
|
13
|
+
import { httpFieldsFrom, logRequestCompletion, withHttpContext, } from "./http-context.js";
|
|
14
|
+
/**
|
|
15
|
+
* Wrap a Fastify route handler so ALS context covers the handler body.
|
|
16
|
+
* Bind loggers via `./http`.
|
|
17
|
+
*/
|
|
18
|
+
export function withFastifyLogging(handler, options = {}) {
|
|
19
|
+
bindLoggerFromOptions(options);
|
|
20
|
+
const headerName = options.headerName ?? "x-request-id";
|
|
21
|
+
return async function (request, reply) {
|
|
22
|
+
const input = {
|
|
23
|
+
method: request.method,
|
|
24
|
+
url: request.url,
|
|
25
|
+
headers: request.headers,
|
|
26
|
+
};
|
|
27
|
+
const started = Date.now();
|
|
28
|
+
const fields = httpFieldsFrom(input, headerName);
|
|
29
|
+
let error;
|
|
30
|
+
try {
|
|
31
|
+
return (await withHttpContext(input, () => handler.call(this, request, reply), options));
|
|
32
|
+
}
|
|
33
|
+
catch (caught) {
|
|
34
|
+
error = caught;
|
|
35
|
+
throw caught;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
if (options.logCompletion) {
|
|
39
|
+
const logger = options.logger ?? getAmbientHttpLogger();
|
|
40
|
+
if (logger) {
|
|
41
|
+
logRequestCompletion(logger, {
|
|
42
|
+
method: typeof fields.method === "string" ? fields.method : undefined,
|
|
43
|
+
path: typeof fields.path === "string" ? fields.path : undefined,
|
|
44
|
+
status: typeof reply.statusCode === "number" ? reply.statusCode : 0,
|
|
45
|
+
elapsedMs: Date.now() - started,
|
|
46
|
+
error,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=fastify.js.map
|
package/dist/hono.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono middleware that attaches request context (and optional completion lines).
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { withHonoLogging } from "@amkisko/json-logging/hono";
|
|
7
|
+
* app.use(withHonoLogging({ logCompletion: true }));
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import type { Logger } from "./logger.js";
|
|
13
|
+
import type { LoggerOptions } from "./types.js";
|
|
14
|
+
/** Minimal Hono context shape (no Hono runtime dependency). */
|
|
15
|
+
export type HonoContextLike = {
|
|
16
|
+
req: {
|
|
17
|
+
method: string;
|
|
18
|
+
url: string;
|
|
19
|
+
header: (name: string) => string | undefined;
|
|
20
|
+
};
|
|
21
|
+
res?: {
|
|
22
|
+
status: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** Options for {@link withHonoLogging}. */
|
|
26
|
+
export type WithHonoLoggingOptions = {
|
|
27
|
+
headerName?: string;
|
|
28
|
+
logger?: Logger;
|
|
29
|
+
loggerOptions?: LoggerOptions;
|
|
30
|
+
headersAllowlist?: readonly string[];
|
|
31
|
+
logCompletion?: boolean;
|
|
32
|
+
};
|
|
33
|
+
/** Hono-style middleware function. */
|
|
34
|
+
export type HonoMiddleware = (c: HonoContextLike, next: () => Promise<void>) => Promise<void>;
|
|
35
|
+
/** Build Hono middleware: `(c, next) => ...`. Bind loggers via `./http`. */
|
|
36
|
+
export declare function withHonoLogging(options?: WithHonoLoggingOptions): HonoMiddleware;
|
|
37
|
+
//# sourceMappingURL=hono.d.ts.map
|
package/dist/hono.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono middleware that attaches request context (and optional completion lines).
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { withHonoLogging } from "@amkisko/json-logging/hono";
|
|
7
|
+
* app.use(withHonoLogging({ logCompletion: true }));
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { bindLoggerFromOptions, getAmbientHttpLogger } from "./http-ambient.js";
|
|
13
|
+
import { httpFieldsFrom, logRequestCompletion, withHttpContext, } from "./http-context.js";
|
|
14
|
+
/** Build Hono middleware: `(c, next) => ...`. Bind loggers via `./http`. */
|
|
15
|
+
export function withHonoLogging(options = {}) {
|
|
16
|
+
bindLoggerFromOptions(options);
|
|
17
|
+
const headerName = options.headerName ?? "x-request-id";
|
|
18
|
+
return async (c, next) => {
|
|
19
|
+
const existing = c.req.header(headerName) ?? null;
|
|
20
|
+
const input = {
|
|
21
|
+
method: c.req.method,
|
|
22
|
+
url: c.req.url,
|
|
23
|
+
requestId: existing ?? undefined,
|
|
24
|
+
headers: {
|
|
25
|
+
get: (name) => c.req.header(name) ?? null,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const started = Date.now();
|
|
29
|
+
const fields = httpFieldsFrom(input, headerName);
|
|
30
|
+
await withHttpContext(input, async () => {
|
|
31
|
+
try {
|
|
32
|
+
await next();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (options.logCompletion) {
|
|
36
|
+
const logger = options.logger ?? getAmbientHttpLogger();
|
|
37
|
+
if (logger) {
|
|
38
|
+
logRequestCompletion(logger, {
|
|
39
|
+
method: typeof fields.method === "string" ? fields.method : undefined,
|
|
40
|
+
path: typeof fields.path === "string" ? fields.path : undefined,
|
|
41
|
+
status: c.res?.status ?? 0,
|
|
42
|
+
elapsedMs: Date.now() - started,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, options);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=hono.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Logger } from "./logger.js";
|
|
2
|
+
/** Set the process-wide ambient logger used by HTTP adapters. */
|
|
3
|
+
export declare function bindHttpLogger(logger: Logger): void;
|
|
4
|
+
/** Current ambient HTTP logger, if {@link bindHttpLogger} was called. */
|
|
5
|
+
export declare function getAmbientHttpLogger(): Logger | undefined;
|
|
6
|
+
/** Bind options.logger once when building an adapter factory. */
|
|
7
|
+
export declare function bindLoggerFromOptions(options?: {
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
}): void;
|
|
10
|
+
/** Test helper: clear the shared HTTP ambient logger. */
|
|
11
|
+
export declare function resetHttpLoggerForTests(): void;
|
|
12
|
+
//# sourceMappingURL=http-ambient.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
let ambientLogger;
|
|
2
|
+
/** Set the process-wide ambient logger used by HTTP adapters. */
|
|
3
|
+
export function bindHttpLogger(logger) {
|
|
4
|
+
ambientLogger = logger;
|
|
5
|
+
}
|
|
6
|
+
/** Current ambient HTTP logger, if {@link bindHttpLogger} was called. */
|
|
7
|
+
export function getAmbientHttpLogger() {
|
|
8
|
+
return ambientLogger;
|
|
9
|
+
}
|
|
10
|
+
/** Bind options.logger once when building an adapter factory. */
|
|
11
|
+
export function bindLoggerFromOptions(options) {
|
|
12
|
+
if (options?.logger) {
|
|
13
|
+
bindHttpLogger(options.logger);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Test helper: clear the shared HTTP ambient logger. */
|
|
17
|
+
export function resetHttpLoggerForTests() {
|
|
18
|
+
ambientLogger = undefined;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=http-ambient.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Logger } from "./logger.js";
|
|
2
|
+
import type { LoggerOptions } from "./types.js";
|
|
3
|
+
/** Headers-like object with a `get` method (Fetch `Headers`, etc.). */
|
|
4
|
+
export type HeaderBag = {
|
|
5
|
+
get(name: string): string | null;
|
|
6
|
+
};
|
|
7
|
+
/** Request fields used to build ambient HTTP context. */
|
|
8
|
+
export type HttpRequestInput = {
|
|
9
|
+
method?: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
headers?: HeaderBag | Record<string, string | string[] | undefined>;
|
|
13
|
+
requestId?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Options for {@link withHttpContext} and framework adapters. */
|
|
16
|
+
export type WithHttpContextOptions = {
|
|
17
|
+
/** Header name for inbound request id. Default `x-request-id`. */
|
|
18
|
+
headerName?: string;
|
|
19
|
+
/** Explicit logger; otherwise ambient or a new logger from loggerOptions. */
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
/** Used when creating a logger if none is bound. */
|
|
22
|
+
loggerOptions?: LoggerOptions;
|
|
23
|
+
/** Optional allowlisted fields merged under http fields (http fields win). */
|
|
24
|
+
extra?: Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* Copy these request header names into ALS context (sanitized strings).
|
|
27
|
+
* Listing Authorization or Cookie is an explicit risk; default is none.
|
|
28
|
+
*/
|
|
29
|
+
headersAllowlist?: readonly string[];
|
|
30
|
+
/** When true, adapters emit one completion line after the request. */
|
|
31
|
+
logCompletion?: boolean;
|
|
32
|
+
};
|
|
33
|
+
/** Fields for {@link logRequestCompletion}. */
|
|
34
|
+
export type RequestCompletionFields = {
|
|
35
|
+
method?: string;
|
|
36
|
+
path?: string;
|
|
37
|
+
status: number;
|
|
38
|
+
elapsedMs: number;
|
|
39
|
+
error?: unknown;
|
|
40
|
+
};
|
|
41
|
+
/** Accept inbound request ids that are short and correlation-safe. */
|
|
42
|
+
export declare const REQUEST_ID_PATTERN: RegExp;
|
|
43
|
+
/** Monotonic fallback request id when crypto UUID is unavailable. */
|
|
44
|
+
export declare function fallbackRequestId(now?: () => number): string;
|
|
45
|
+
/** Prefer `crypto.randomUUID`, else {@link fallbackRequestId}. */
|
|
46
|
+
export declare function defaultRandomId(randomUUID?: (() => string) | null): string;
|
|
47
|
+
/** True when `value` matches {@link REQUEST_ID_PATTERN}. */
|
|
48
|
+
export declare function isValidRequestId(value: string): boolean;
|
|
49
|
+
/** Keep a valid inbound id, otherwise allocate a new one. */
|
|
50
|
+
export declare function allocateRequestId(existing: string | null | undefined, randomId?: () => string): string;
|
|
51
|
+
/** Pathname from an absolute or relative URL string. */
|
|
52
|
+
export declare function requestPathFromUrl(url: string): string | undefined;
|
|
53
|
+
/** Pick named keys from a bag; missing keys are omitted. */
|
|
54
|
+
export declare function pickContextKeys(bag: Record<string, unknown> | undefined, keys: readonly string[] | undefined): Record<string, unknown>;
|
|
55
|
+
/** Build requestId / method / path fields from {@link HttpRequestInput}. */
|
|
56
|
+
export declare function httpFieldsFrom(input: HttpRequestInput, headerName?: string): Record<string, unknown>;
|
|
57
|
+
/** Copy allowlisted header names into a sanitized string map. */
|
|
58
|
+
export declare function pickAllowlistedHeaders(headers: HeaderBag | Record<string, string | string[] | undefined> | undefined, names: readonly string[] | undefined): Record<string, unknown>;
|
|
59
|
+
/**
|
|
60
|
+
* One structured access line. Level is error when status >= 500 or error is set.
|
|
61
|
+
*/
|
|
62
|
+
export declare function logRequestCompletion(logger: Logger, fields: RequestCompletionFields): void;
|
|
63
|
+
/** Run `fn` under ALS context built from the HTTP request input. */
|
|
64
|
+
export declare function withHttpContext<T>(input: HttpRequestInput, fn: () => T | Promise<T>, options?: WithHttpContextOptions): T | Promise<T>;
|
|
65
|
+
//# sourceMappingURL=http-context.d.ts.map
|