@amkisko/json-logging 0.0.0-stage → 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.
- 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 +26 -0
- package/dist/console-bridge.js +155 -0
- package/dist/console-state.d.ts +11 -0
- package/dist/console-state.js +40 -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 +10 -0
- package/dist/http-ambient.js +18 -0
- package/dist/http-context.d.ts +49 -0
- package/dist/http-context.js +147 -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 +40 -0
- package/dist/next.js +34 -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 +20 -0
- package/dist/sanitize.js +148 -0
- package/dist/serialize.d.ts +27 -0
- package/dist/serialize.js +200 -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
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,10 @@
|
|
|
1
|
+
import type { Logger } from "./logger.js";
|
|
2
|
+
export declare function bindHttpLogger(logger: Logger): void;
|
|
3
|
+
export declare function getAmbientHttpLogger(): Logger | undefined;
|
|
4
|
+
/** Bind options.logger once when building an adapter factory. */
|
|
5
|
+
export declare function bindLoggerFromOptions(options?: {
|
|
6
|
+
logger?: Logger;
|
|
7
|
+
}): void;
|
|
8
|
+
/** Test helper: clear the shared HTTP ambient logger. */
|
|
9
|
+
export declare function resetHttpLoggerForTests(): void;
|
|
10
|
+
//# sourceMappingURL=http-ambient.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
let ambientLogger;
|
|
2
|
+
export function bindHttpLogger(logger) {
|
|
3
|
+
ambientLogger = logger;
|
|
4
|
+
}
|
|
5
|
+
export function getAmbientHttpLogger() {
|
|
6
|
+
return ambientLogger;
|
|
7
|
+
}
|
|
8
|
+
/** Bind options.logger once when building an adapter factory. */
|
|
9
|
+
export function bindLoggerFromOptions(options) {
|
|
10
|
+
if (options?.logger) {
|
|
11
|
+
bindHttpLogger(options.logger);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Test helper: clear the shared HTTP ambient logger. */
|
|
15
|
+
export function resetHttpLoggerForTests() {
|
|
16
|
+
ambientLogger = undefined;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=http-ambient.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Logger } from "./logger.js";
|
|
2
|
+
import type { LoggerOptions } from "./types.js";
|
|
3
|
+
export type HeaderBag = {
|
|
4
|
+
get(name: string): string | null;
|
|
5
|
+
};
|
|
6
|
+
export type HttpRequestInput = {
|
|
7
|
+
method?: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
headers?: HeaderBag | Record<string, string | string[] | undefined>;
|
|
11
|
+
requestId?: string;
|
|
12
|
+
};
|
|
13
|
+
export type WithHttpContextOptions = {
|
|
14
|
+
headerName?: string;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
loggerOptions?: LoggerOptions;
|
|
17
|
+
/** Optional allowlisted fields merged under http fields (http fields win). */
|
|
18
|
+
extra?: Record<string, unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* Copy these request header names into ALS context (sanitized strings).
|
|
21
|
+
* Listing Authorization or Cookie is an explicit risk; default is none.
|
|
22
|
+
*/
|
|
23
|
+
headersAllowlist?: readonly string[];
|
|
24
|
+
/** When true, adapters emit one completion line after the request. */
|
|
25
|
+
logCompletion?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type RequestCompletionFields = {
|
|
28
|
+
method?: string;
|
|
29
|
+
path?: string;
|
|
30
|
+
status: number;
|
|
31
|
+
elapsedMs: number;
|
|
32
|
+
error?: unknown;
|
|
33
|
+
};
|
|
34
|
+
/** Accept inbound request ids that are short and correlation-safe. */
|
|
35
|
+
export declare const REQUEST_ID_PATTERN: RegExp;
|
|
36
|
+
export declare function fallbackRequestId(now?: () => number): string;
|
|
37
|
+
export declare function defaultRandomId(randomUUID?: (() => string) | null): string;
|
|
38
|
+
export declare function isValidRequestId(value: string): boolean;
|
|
39
|
+
export declare function allocateRequestId(existing: string | null | undefined, randomId?: () => string): string;
|
|
40
|
+
export declare function requestPathFromUrl(url: string): string | undefined;
|
|
41
|
+
export declare function pickContextKeys(bag: Record<string, unknown> | undefined, keys: readonly string[] | undefined): Record<string, unknown>;
|
|
42
|
+
export declare function httpFieldsFrom(input: HttpRequestInput, headerName?: string): Record<string, unknown>;
|
|
43
|
+
export declare function pickAllowlistedHeaders(headers: HeaderBag | Record<string, string | string[] | undefined> | undefined, names: readonly string[] | undefined): Record<string, unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* One structured access line. Level is error when status >= 500 or error is set.
|
|
46
|
+
*/
|
|
47
|
+
export declare function logRequestCompletion(logger: Logger, fields: RequestCompletionFields): void;
|
|
48
|
+
export declare function withHttpContext<T>(input: HttpRequestInput, fn: () => T | Promise<T>, options?: WithHttpContextOptions): T | Promise<T>;
|
|
49
|
+
//# sourceMappingURL=http-context.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { withContext } from "./context.js";
|
|
2
|
+
import { sanitizeError, sanitizeString } from "./sanitize.js";
|
|
3
|
+
/** Accept inbound request ids that are short and correlation-safe. */
|
|
4
|
+
export const REQUEST_ID_PATTERN = /^[\w.\-:]{1,128}$/;
|
|
5
|
+
let fallbackCounter = 0;
|
|
6
|
+
export function fallbackRequestId(now = Date.now) {
|
|
7
|
+
fallbackCounter += 1;
|
|
8
|
+
return `req_${now().toString(36)}_${fallbackCounter.toString(36)}`;
|
|
9
|
+
}
|
|
10
|
+
export function defaultRandomId(randomUUID) {
|
|
11
|
+
const resolved = randomUUID === undefined ? globalThis.crypto?.randomUUID?.bind(globalThis.crypto) : randomUUID;
|
|
12
|
+
if (typeof resolved === "function") {
|
|
13
|
+
return resolved();
|
|
14
|
+
}
|
|
15
|
+
return fallbackRequestId();
|
|
16
|
+
}
|
|
17
|
+
export function isValidRequestId(value) {
|
|
18
|
+
return REQUEST_ID_PATTERN.test(value);
|
|
19
|
+
}
|
|
20
|
+
export function allocateRequestId(existing, randomId = defaultRandomId) {
|
|
21
|
+
if (existing && isValidRequestId(existing)) {
|
|
22
|
+
return existing;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return randomId();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return fallbackRequestId();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function requestPathFromUrl(url) {
|
|
32
|
+
if (url.length === 0) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return new URL(url, "http://localhost").pathname;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
const query = url.indexOf("?");
|
|
40
|
+
const hash = url.indexOf("#");
|
|
41
|
+
let end = url.length;
|
|
42
|
+
if (query >= 0) {
|
|
43
|
+
end = query;
|
|
44
|
+
}
|
|
45
|
+
if (hash >= 0 && hash < end) {
|
|
46
|
+
end = hash;
|
|
47
|
+
}
|
|
48
|
+
const path = url.slice(0, end);
|
|
49
|
+
return path.length > 0 ? path : undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function isHeaderBag(value) {
|
|
53
|
+
return typeof Reflect.get(value, "get") === "function";
|
|
54
|
+
}
|
|
55
|
+
function headerGet(headers, name) {
|
|
56
|
+
if (!headers) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (isHeaderBag(headers)) {
|
|
60
|
+
return headers.get(name);
|
|
61
|
+
}
|
|
62
|
+
const direct = Reflect.get(headers, name) ?? Reflect.get(headers, name.toLowerCase());
|
|
63
|
+
if (Array.isArray(direct)) {
|
|
64
|
+
const first = direct[0];
|
|
65
|
+
return typeof first === "string" ? first : null;
|
|
66
|
+
}
|
|
67
|
+
return typeof direct === "string" ? direct : null;
|
|
68
|
+
}
|
|
69
|
+
export function pickContextKeys(bag, keys) {
|
|
70
|
+
if (!bag || !keys || keys.length === 0) {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const key of keys) {
|
|
75
|
+
if (Object.hasOwn(bag, key)) {
|
|
76
|
+
out[key] = bag[key];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
export function httpFieldsFrom(input, headerName = "x-request-id") {
|
|
82
|
+
const fromHeader = headerGet(input.headers, headerName);
|
|
83
|
+
const requestId = allocateRequestId(input.requestId ?? fromHeader);
|
|
84
|
+
const fields = { requestId };
|
|
85
|
+
if (input.method) {
|
|
86
|
+
fields.method = input.method;
|
|
87
|
+
}
|
|
88
|
+
const path = input.path ?? (input.url ? requestPathFromUrl(input.url) : undefined);
|
|
89
|
+
if (path) {
|
|
90
|
+
fields.path = path;
|
|
91
|
+
}
|
|
92
|
+
return fields;
|
|
93
|
+
}
|
|
94
|
+
export function pickAllowlistedHeaders(headers, names) {
|
|
95
|
+
if (!headers || !names || names.length === 0) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const name of names) {
|
|
100
|
+
const value = headerGet(headers, name);
|
|
101
|
+
if (value !== null && value.length > 0) {
|
|
102
|
+
out[name] = sanitizeString(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One structured access line. Level is error when status >= 500 or error is set.
|
|
109
|
+
*/
|
|
110
|
+
export function logRequestCompletion(logger, fields) {
|
|
111
|
+
const payload = {
|
|
112
|
+
event: "http_request",
|
|
113
|
+
status: fields.status,
|
|
114
|
+
elapsedMs: fields.elapsedMs,
|
|
115
|
+
};
|
|
116
|
+
if (fields.method) {
|
|
117
|
+
payload.method = fields.method;
|
|
118
|
+
}
|
|
119
|
+
if (fields.path) {
|
|
120
|
+
payload.path = fields.path;
|
|
121
|
+
}
|
|
122
|
+
if (fields.error instanceof Error) {
|
|
123
|
+
Object.assign(payload, sanitizeError(fields.error));
|
|
124
|
+
}
|
|
125
|
+
else if (fields.error !== undefined) {
|
|
126
|
+
payload.error = {
|
|
127
|
+
message: sanitizeString(String(fields.error)),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const level = fields.status >= 500 || fields.error !== undefined ? "error" : "info";
|
|
131
|
+
if (level === "error") {
|
|
132
|
+
logger.error(payload);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
logger.info(payload);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function withHttpContext(input, fn, options = {}) {
|
|
139
|
+
const headerName = options.headerName ?? "x-request-id";
|
|
140
|
+
const fields = {
|
|
141
|
+
...pickAllowlistedHeaders(input.headers, options.headersAllowlist),
|
|
142
|
+
...(options.extra ?? {}),
|
|
143
|
+
...httpFieldsFrom(input, headerName),
|
|
144
|
+
};
|
|
145
|
+
return withContext(fields, fn);
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=http-context.js.map
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP helpers: ambient logger binding, request context, and completion lines.
|
|
3
|
+
* Framework adapters live on separate entrypoints (`./hono`, `./express`, ...).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { bindHttpLogger, httpLogger, withHttpContext } from "@amkisko/json-logging/http";
|
|
8
|
+
*
|
|
9
|
+
* bindHttpLogger(httpLogger({ loggerOptions: { base: { service: "api" } } }));
|
|
10
|
+
* await withHttpContext({ method: "GET", path: "/" }, () => {
|
|
11
|
+
* httpLogger().info("inside request");
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import type { WithHttpContextOptions } from "./http-context.js";
|
|
18
|
+
import type { Logger } from "./logger.js";
|
|
19
|
+
export { bindHttpLogger, bindLoggerFromOptions, resetHttpLoggerForTests, } from "./http-ambient.js";
|
|
20
|
+
export type { HeaderBag, HttpRequestInput, RequestCompletionFields, WithHttpContextOptions, } from "./http-context.js";
|
|
21
|
+
export { allocateRequestId, defaultRandomId, fallbackRequestId, httpFieldsFrom, isValidRequestId, logRequestCompletion, pickAllowlistedHeaders, pickContextKeys, REQUEST_ID_PATTERN, requestPathFromUrl, withHttpContext, } from "./http-context.js";
|
|
22
|
+
/** Resolve the ambient HTTP logger, or create and bind one from options. */
|
|
23
|
+
export declare function httpLogger(options?: WithHttpContextOptions): Logger;
|
|
24
|
+
//# sourceMappingURL=http.d.ts.map
|