@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.
Files changed (53) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +231 -2
  3. package/dist/bun-serve.d.ts +30 -0
  4. package/dist/bun-serve.js +60 -0
  5. package/dist/console-bridge.d.ts +32 -0
  6. package/dist/console-bridge.js +158 -0
  7. package/dist/console-state.d.ts +19 -0
  8. package/dist/console-state.js +46 -0
  9. package/dist/console.d.ts +18 -0
  10. package/dist/console.js +17 -0
  11. package/dist/context.d.ts +40 -0
  12. package/dist/context.js +162 -0
  13. package/dist/express.d.ts +41 -0
  14. package/dist/express.js +48 -0
  15. package/dist/fastify.d.ts +37 -0
  16. package/dist/fastify.js +53 -0
  17. package/dist/hono.d.ts +37 -0
  18. package/dist/hono.js +50 -0
  19. package/dist/http-ambient.d.ts +12 -0
  20. package/dist/http-ambient.js +20 -0
  21. package/dist/http-context.d.ts +65 -0
  22. package/dist/http-context.js +156 -0
  23. package/dist/http.d.ts +24 -0
  24. package/dist/http.js +34 -0
  25. package/dist/index.d.ts +31 -0
  26. package/dist/index.js +26 -0
  27. package/dist/json-logging.d.ts +9 -0
  28. package/dist/json-logging.js +12 -0
  29. package/dist/koa.d.ts +34 -0
  30. package/dist/koa.js +54 -0
  31. package/dist/logger.d.ts +46 -0
  32. package/dist/logger.js +130 -0
  33. package/dist/next.d.ts +41 -0
  34. package/dist/next.js +35 -0
  35. package/dist/objects.d.ts +12 -0
  36. package/dist/objects.js +18 -0
  37. package/dist/redact.d.ts +9 -0
  38. package/dist/redact.js +77 -0
  39. package/dist/remix.d.ts +43 -0
  40. package/dist/remix.js +38 -0
  41. package/dist/sanitize.d.ts +25 -0
  42. package/dist/sanitize.js +153 -0
  43. package/dist/serialize.d.ts +31 -0
  44. package/dist/serialize.js +203 -0
  45. package/dist/severity.d.ts +10 -0
  46. package/dist/severity.js +25 -0
  47. package/dist/sinks.d.ts +14 -0
  48. package/dist/sinks.js +66 -0
  49. package/dist/timestamp.d.ts +15 -0
  50. package/dist/timestamp.js +47 -0
  51. package/dist/types.d.ts +42 -0
  52. package/dist/types.js +2 -0
  53. package/package.json +98 -4
@@ -0,0 +1,156 @@
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
+ /** Monotonic fallback request id when crypto UUID is unavailable. */
7
+ export function fallbackRequestId(now = Date.now) {
8
+ fallbackCounter += 1;
9
+ return `req_${now().toString(36)}_${fallbackCounter.toString(36)}`;
10
+ }
11
+ /** Prefer `crypto.randomUUID`, else {@link fallbackRequestId}. */
12
+ export function defaultRandomId(randomUUID) {
13
+ const resolved = randomUUID === undefined ? globalThis.crypto?.randomUUID?.bind(globalThis.crypto) : randomUUID;
14
+ if (typeof resolved === "function") {
15
+ return resolved();
16
+ }
17
+ return fallbackRequestId();
18
+ }
19
+ /** True when `value` matches {@link REQUEST_ID_PATTERN}. */
20
+ export function isValidRequestId(value) {
21
+ return REQUEST_ID_PATTERN.test(value);
22
+ }
23
+ /** Keep a valid inbound id, otherwise allocate a new one. */
24
+ export function allocateRequestId(existing, randomId = defaultRandomId) {
25
+ if (existing && isValidRequestId(existing)) {
26
+ return existing;
27
+ }
28
+ try {
29
+ return randomId();
30
+ }
31
+ catch {
32
+ return fallbackRequestId();
33
+ }
34
+ }
35
+ /** Pathname from an absolute or relative URL string. */
36
+ export function requestPathFromUrl(url) {
37
+ if (url.length === 0) {
38
+ return undefined;
39
+ }
40
+ try {
41
+ return new URL(url, "http://localhost").pathname;
42
+ }
43
+ catch {
44
+ const query = url.indexOf("?");
45
+ const hash = url.indexOf("#");
46
+ let end = url.length;
47
+ if (query >= 0) {
48
+ end = query;
49
+ }
50
+ if (hash >= 0 && hash < end) {
51
+ end = hash;
52
+ }
53
+ const path = url.slice(0, end);
54
+ return path.length > 0 ? path : undefined;
55
+ }
56
+ }
57
+ function isHeaderBag(value) {
58
+ return typeof Reflect.get(value, "get") === "function";
59
+ }
60
+ function headerGet(headers, name) {
61
+ if (!headers) {
62
+ return null;
63
+ }
64
+ if (isHeaderBag(headers)) {
65
+ return headers.get(name);
66
+ }
67
+ const direct = Reflect.get(headers, name) ?? Reflect.get(headers, name.toLowerCase());
68
+ if (Array.isArray(direct)) {
69
+ const first = direct[0];
70
+ return typeof first === "string" ? first : null;
71
+ }
72
+ return typeof direct === "string" ? direct : null;
73
+ }
74
+ /** Pick named keys from a bag; missing keys are omitted. */
75
+ export function pickContextKeys(bag, keys) {
76
+ if (!bag || !keys || keys.length === 0) {
77
+ return {};
78
+ }
79
+ const out = {};
80
+ for (const key of keys) {
81
+ if (Object.hasOwn(bag, key)) {
82
+ out[key] = bag[key];
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+ /** Build requestId / method / path fields from {@link HttpRequestInput}. */
88
+ export function httpFieldsFrom(input, headerName = "x-request-id") {
89
+ const fromHeader = headerGet(input.headers, headerName);
90
+ const requestId = allocateRequestId(input.requestId ?? fromHeader);
91
+ const fields = { requestId };
92
+ if (input.method) {
93
+ fields.method = input.method;
94
+ }
95
+ const path = input.path ?? (input.url ? requestPathFromUrl(input.url) : undefined);
96
+ if (path) {
97
+ fields.path = path;
98
+ }
99
+ return fields;
100
+ }
101
+ /** Copy allowlisted header names into a sanitized string map. */
102
+ export function pickAllowlistedHeaders(headers, names) {
103
+ if (!headers || !names || names.length === 0) {
104
+ return {};
105
+ }
106
+ const out = {};
107
+ for (const name of names) {
108
+ const value = headerGet(headers, name);
109
+ if (value !== null && value.length > 0) {
110
+ out[name] = sanitizeString(value);
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+ /**
116
+ * One structured access line. Level is error when status >= 500 or error is set.
117
+ */
118
+ export function logRequestCompletion(logger, fields) {
119
+ const payload = {
120
+ event: "http_request",
121
+ status: fields.status,
122
+ elapsedMs: fields.elapsedMs,
123
+ };
124
+ if (fields.method) {
125
+ payload.method = fields.method;
126
+ }
127
+ if (fields.path) {
128
+ payload.path = fields.path;
129
+ }
130
+ if (fields.error instanceof Error) {
131
+ Object.assign(payload, sanitizeError(fields.error));
132
+ }
133
+ else if (fields.error !== undefined) {
134
+ payload.error = {
135
+ message: sanitizeString(String(fields.error)),
136
+ };
137
+ }
138
+ const level = fields.status >= 500 || fields.error !== undefined ? "error" : "info";
139
+ if (level === "error") {
140
+ logger.error(payload);
141
+ }
142
+ else {
143
+ logger.info(payload);
144
+ }
145
+ }
146
+ /** Run `fn` under ALS context built from the HTTP request input. */
147
+ export function withHttpContext(input, fn, options = {}) {
148
+ const headerName = options.headerName ?? "x-request-id";
149
+ const fields = {
150
+ ...pickAllowlistedHeaders(input.headers, options.headersAllowlist),
151
+ ...(options.extra ?? {}),
152
+ ...httpFieldsFrom(input, headerName),
153
+ };
154
+ return withContext(fields, fn);
155
+ }
156
+ //# 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
package/dist/http.js ADDED
@@ -0,0 +1,34 @@
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 { bindHttpLogger, getAmbientHttpLogger } from "./http-ambient.js";
18
+ import { createLogger } from "./json-logging.js";
19
+ export { bindHttpLogger, bindLoggerFromOptions, resetHttpLoggerForTests, } from "./http-ambient.js";
20
+ export { allocateRequestId, defaultRandomId, fallbackRequestId, httpFieldsFrom, isValidRequestId, logRequestCompletion, pickAllowlistedHeaders, pickContextKeys, REQUEST_ID_PATTERN, requestPathFromUrl, withHttpContext, } from "./http-context.js";
21
+ /** Resolve the ambient HTTP logger, or create and bind one from options. */
22
+ export function httpLogger(options) {
23
+ if (options?.logger) {
24
+ return options.logger;
25
+ }
26
+ const ambient = getAmbientHttpLogger();
27
+ if (ambient) {
28
+ return ambient;
29
+ }
30
+ const created = createLogger(options?.loggerOptions);
31
+ bindHttpLogger(created);
32
+ return created;
33
+ }
34
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Structured JSON logging with ActiveSupport-style `tagged` and `withContext`.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { createLogger, withContext } from "@amkisko/json-logging";
7
+ *
8
+ * const logger = createLogger({ base: { service: "api" } });
9
+ * await withContext({ requestId: "abc" }, async () => {
10
+ * logger.info("handled request");
11
+ * });
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ export type { ConsoleMethodName, ConsoleMethods } from "./console-state.js";
17
+ export { captureConsoleMethods, getBridgeOriginals, isInBridge, } from "./console-state.js";
18
+ export type { AlsConstructor, AlsInstance, ContextFields } from "./context.js";
19
+ export { getBlockTags, getContext, isAsyncLocalStorageAvailable, peekLogStore, withContext, } from "./context.js";
20
+ export { createLogger, JsonLogging } from "./json-logging.js";
21
+ export { Logger } from "./logger.js";
22
+ export type { CompiledRedactPath } from "./redact.js";
23
+ export { applyCompiledRedactPaths, applyRedactPaths, compileRedactPaths, } from "./redact.js";
24
+ export { isSensitiveKey, MAX_CONTEXT_SIZE, MAX_DEPTH, MAX_STRING_LENGTH, prepareTags, sanitizeError, sanitizeHash, sanitizeString, } from "./sanitize.js";
25
+ export { buildFallbackRecord, buildRecord, currentTimestamp, formatTimestamp, parseMessage, SYSTEM_KEYS, toJsonLine, } from "./serialize.js";
26
+ export { isEnabled, LEVEL_ORDER, SEVERITY_NAMES, severityName } from "./severity.js";
27
+ export { consoleSink, consoleSinkFrom, streamSink } from "./sinks.js";
28
+ export type { PerformanceLike } from "./timestamp.js";
29
+ export { defaultPerformance } from "./timestamp.js";
30
+ export type { ContextProvider, FieldSerializer, JsonPrimitive, JsonValue, Level, LoggerOptions, LogMessage, LogRecord, SeverityName, Sink, } from "./types.js";
31
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Structured JSON logging with ActiveSupport-style `tagged` and `withContext`.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { createLogger, withContext } from "@amkisko/json-logging";
7
+ *
8
+ * const logger = createLogger({ base: { service: "api" } });
9
+ * await withContext({ requestId: "abc" }, async () => {
10
+ * logger.info("handled request");
11
+ * });
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ export { captureConsoleMethods, getBridgeOriginals, isInBridge, } from "./console-state.js";
17
+ export { getBlockTags, getContext, isAsyncLocalStorageAvailable, peekLogStore, withContext, } from "./context.js";
18
+ export { createLogger, JsonLogging } from "./json-logging.js";
19
+ export { Logger } from "./logger.js";
20
+ export { applyCompiledRedactPaths, applyRedactPaths, compileRedactPaths, } from "./redact.js";
21
+ export { isSensitiveKey, MAX_CONTEXT_SIZE, MAX_DEPTH, MAX_STRING_LENGTH, prepareTags, sanitizeError, sanitizeHash, sanitizeString, } from "./sanitize.js";
22
+ export { buildFallbackRecord, buildRecord, currentTimestamp, formatTimestamp, parseMessage, SYSTEM_KEYS, toJsonLine, } from "./serialize.js";
23
+ export { isEnabled, LEVEL_ORDER, SEVERITY_NAMES, severityName } from "./severity.js";
24
+ export { consoleSink, consoleSinkFrom, streamSink } from "./sinks.js";
25
+ export { defaultPerformance } from "./timestamp.js";
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ import { Logger } from "./logger.js";
2
+ import type { LoggerOptions } from "./types.js";
3
+ /** Create a {@link Logger} with optional level, sink, base fields, redact, and serializers. */
4
+ export declare function createLogger(options?: LoggerOptions): Logger;
5
+ /** ActiveSupport-style factory: `JsonLogging.new(options)` mirrors {@link createLogger}. */
6
+ export declare const JsonLogging: {
7
+ new: (options?: LoggerOptions) => Logger;
8
+ };
9
+ //# sourceMappingURL=json-logging.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { Logger } from "./logger.js";
2
+ /** Create a {@link Logger} with optional level, sink, base fields, redact, and serializers. */
3
+ export function createLogger(options = {}) {
4
+ return new Logger(options);
5
+ }
6
+ /** ActiveSupport-style factory: `JsonLogging.new(options)` mirrors {@link createLogger}. */
7
+ export const JsonLogging = {
8
+ new(options = {}) {
9
+ return createLogger(options);
10
+ },
11
+ };
12
+ //# sourceMappingURL=json-logging.js.map
package/dist/koa.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Koa middleware that attaches request context (and optional completion lines).
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { withKoaLogging } from "@amkisko/json-logging/koa";
7
+ * app.use(withKoaLogging({ logCompletion: true }));
8
+ * ```
9
+ *
10
+ * @module
11
+ */
12
+ import type { Logger } from "./logger.js";
13
+ import type { LoggerOptions } from "./types.js";
14
+ /** Minimal Koa context shape (no Koa runtime dependency). */
15
+ export type KoaContextLike = {
16
+ method: string;
17
+ path?: string;
18
+ url?: string;
19
+ status?: number;
20
+ get: (name: string) => string;
21
+ };
22
+ /** Options for {@link withKoaLogging}. */
23
+ export type WithKoaLoggingOptions = {
24
+ headerName?: string;
25
+ logger?: Logger;
26
+ loggerOptions?: LoggerOptions;
27
+ headersAllowlist?: readonly string[];
28
+ logCompletion?: boolean;
29
+ };
30
+ /** Koa-style middleware function. */
31
+ export type KoaMiddleware = (ctx: KoaContextLike, next: () => Promise<void>) => Promise<void>;
32
+ /** Build Koa middleware: `(ctx, next) => ...`. Bind loggers via `./http`. */
33
+ export declare function withKoaLogging(options?: WithKoaLoggingOptions): KoaMiddleware;
34
+ //# sourceMappingURL=koa.d.ts.map
package/dist/koa.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Koa middleware that attaches request context (and optional completion lines).
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { withKoaLogging } from "@amkisko/json-logging/koa";
7
+ * app.use(withKoaLogging({ 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 Koa middleware: `(ctx, next) => ...`. Bind loggers via `./http`. */
15
+ export function withKoaLogging(options = {}) {
16
+ bindLoggerFromOptions(options);
17
+ const headerName = options.headerName ?? "x-request-id";
18
+ return async (ctx, next) => {
19
+ const existing = ctx.get(headerName);
20
+ const input = {
21
+ method: ctx.method,
22
+ path: ctx.path,
23
+ url: ctx.url,
24
+ requestId: existing.length > 0 ? existing : undefined,
25
+ headers: {
26
+ get: (name) => {
27
+ const value = ctx.get(name);
28
+ return value.length > 0 ? value : null;
29
+ },
30
+ },
31
+ };
32
+ const started = Date.now();
33
+ const fields = httpFieldsFrom(input, headerName);
34
+ await withHttpContext(input, async () => {
35
+ try {
36
+ await next();
37
+ }
38
+ finally {
39
+ if (options.logCompletion) {
40
+ const logger = options.logger ?? getAmbientHttpLogger();
41
+ if (logger) {
42
+ logRequestCompletion(logger, {
43
+ method: typeof fields.method === "string" ? fields.method : undefined,
44
+ path: typeof fields.path === "string" ? fields.path : undefined,
45
+ status: typeof ctx.status === "number" ? ctx.status : 0,
46
+ elapsedMs: Date.now() - started,
47
+ });
48
+ }
49
+ }
50
+ }
51
+ }, options);
52
+ };
53
+ }
54
+ //# sourceMappingURL=koa.js.map
@@ -0,0 +1,46 @@
1
+ import type { ContextProvider, FieldSerializer, Level, LoggerOptions, LogMessage, Sink } from "./types.js";
2
+ /** Structured logger: severity methods, sticky tags, child loggers, never-raise emit path. */
3
+ export declare class Logger {
4
+ /** Minimum level that will be written. */
5
+ readonly level: Level;
6
+ /** Destination for JSON lines. */
7
+ readonly sink: Sink;
8
+ /** Sanitized root fields merged into every line. */
9
+ readonly base: Record<string, unknown>;
10
+ /** Optional clock override (tests). */
11
+ readonly clock?: () => Date;
12
+ /** Sticky tags from construction or {@link Logger.tagged}. */
13
+ readonly tags: readonly string[];
14
+ /** Configured redact paths (dot notation). */
15
+ readonly redact: readonly string[];
16
+ /** Root-field serializers. */
17
+ readonly serializers: Readonly<Record<string, FieldSerializer>>;
18
+ /** Optional ambient context provider. */
19
+ readonly contextProvider?: ContextProvider;
20
+ private readonly compiledRedact;
21
+ constructor(options?: LoggerOptions);
22
+ /** True when a message at `level` would be emitted. */
23
+ isLevelEnabled(level: Level): boolean;
24
+ /** Emit a debug line when enabled. */
25
+ debug(message: LogMessage): void;
26
+ /** Emit an info line when enabled. */
27
+ info(message: LogMessage): void;
28
+ /** Emit a warn line when enabled. */
29
+ warn(message: LogMessage): void;
30
+ /** Emit an error line when enabled. */
31
+ error(message: LogMessage): void;
32
+ /** Emit a fatal line when enabled. */
33
+ fatal(message: LogMessage): void;
34
+ /** Child logger with extra root bindings merged into `base`. */
35
+ child(bindings: Record<string, unknown>): Logger;
36
+ /**
37
+ * Block form runs `fn` with nested tags; sticky form returns a child logger.
38
+ * Pass tags then a callback, or only tags for a sticky child.
39
+ */
40
+ tagged(callback: () => unknown): unknown;
41
+ tagged(tag: string | number | boolean, callback: () => unknown): unknown;
42
+ tagged(tag: string | number | boolean, tag2: string | number | boolean, callback: () => unknown): unknown;
43
+ tagged(...tags: (string | number | boolean | null | undefined)[]): Logger;
44
+ private log;
45
+ }
46
+ //# sourceMappingURL=logger.d.ts.map
package/dist/logger.js ADDED
@@ -0,0 +1,130 @@
1
+ import { withBlockTags } from "./context.js";
2
+ import { compileRedactPaths } from "./redact.js";
3
+ import { prepareTags, sanitizeHash } from "./sanitize.js";
4
+ import { buildFallbackRecord, buildRecord, toJsonLine } from "./serialize.js";
5
+ import { isEnabled } from "./severity.js";
6
+ import { consoleSink } from "./sinks.js";
7
+ function isCallback(value) {
8
+ return typeof value === "function";
9
+ }
10
+ /** Structured logger: severity methods, sticky tags, child loggers, never-raise emit path. */
11
+ export class Logger {
12
+ /** Minimum level that will be written. */
13
+ level;
14
+ /** Destination for JSON lines. */
15
+ sink;
16
+ /** Sanitized root fields merged into every line. */
17
+ base;
18
+ /** Optional clock override (tests). */
19
+ clock;
20
+ /** Sticky tags from construction or {@link Logger.tagged}. */
21
+ tags;
22
+ /** Configured redact paths (dot notation). */
23
+ redact;
24
+ /** Root-field serializers. */
25
+ serializers;
26
+ /** Optional ambient context provider. */
27
+ contextProvider;
28
+ compiledRedact;
29
+ constructor(options = {}) {
30
+ this.level = options.level ?? "info";
31
+ this.sink = options.sink ?? consoleSink();
32
+ this.base = options.base ? sanitizeHash(options.base) : {};
33
+ this.clock = options.clock;
34
+ this.tags = options.tags ? prepareTags(options.tags) : [];
35
+ this.redact = options.redact ? [...options.redact] : [];
36
+ this.serializers = options.serializers ? { ...options.serializers } : {};
37
+ this.contextProvider = options.contextProvider;
38
+ this.compiledRedact = compileRedactPaths(this.redact);
39
+ }
40
+ /** True when a message at `level` would be emitted. */
41
+ isLevelEnabled(level) {
42
+ return isEnabled(this.level, level);
43
+ }
44
+ /** Emit a debug line when enabled. */
45
+ debug(message) {
46
+ this.log("debug", message);
47
+ }
48
+ /** Emit an info line when enabled. */
49
+ info(message) {
50
+ this.log("info", message);
51
+ }
52
+ /** Emit a warn line when enabled. */
53
+ warn(message) {
54
+ this.log("warn", message);
55
+ }
56
+ /** Emit an error line when enabled. */
57
+ error(message) {
58
+ this.log("error", message);
59
+ }
60
+ /** Emit a fatal line when enabled. */
61
+ fatal(message) {
62
+ this.log("fatal", message);
63
+ }
64
+ /** Child logger with extra root bindings merged into `base`. */
65
+ child(bindings) {
66
+ return new Logger({
67
+ level: this.level,
68
+ sink: this.sink,
69
+ clock: this.clock,
70
+ tags: this.tags,
71
+ redact: this.redact,
72
+ serializers: this.serializers,
73
+ contextProvider: this.contextProvider,
74
+ base: { ...this.base, ...sanitizeHash(bindings) },
75
+ });
76
+ }
77
+ tagged(...args) {
78
+ if (args.length === 0) {
79
+ return this;
80
+ }
81
+ const last = args[args.length - 1];
82
+ if (isCallback(last)) {
83
+ const tags = prepareTags(args.slice(0, -1));
84
+ return withBlockTags(tags, last);
85
+ }
86
+ const tags = prepareTags(args);
87
+ return new Logger({
88
+ level: this.level,
89
+ sink: this.sink,
90
+ clock: this.clock,
91
+ base: this.base,
92
+ redact: this.redact,
93
+ serializers: this.serializers,
94
+ contextProvider: this.contextProvider,
95
+ tags: [...this.tags, ...tags],
96
+ });
97
+ }
98
+ log(level, message) {
99
+ if (!this.isLevelEnabled(level)) {
100
+ return;
101
+ }
102
+ try {
103
+ const record = buildRecord({
104
+ message,
105
+ level,
106
+ stickyTags: this.tags,
107
+ base: this.base,
108
+ preparedBase: true,
109
+ preparedTags: true,
110
+ clock: this.clock,
111
+ compiledRedact: this.compiledRedact,
112
+ serializers: this.serializers,
113
+ contextProvider: this.contextProvider,
114
+ });
115
+ const line = toJsonLine(record);
116
+ void this.sink.write(line, record);
117
+ }
118
+ catch (error) {
119
+ try {
120
+ const fallback = buildFallbackRecord(level, message, error, this.clock);
121
+ const line = toJsonLine(fallback);
122
+ void this.sink.write(line, fallback);
123
+ }
124
+ catch {
125
+ // Last resort: never throw from the log path.
126
+ }
127
+ }
128
+ }
129
+ }
130
+ //# sourceMappingURL=logger.js.map
package/dist/next.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Next.js helpers: wrap middleware or route handlers with request-scoped log context.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { withRequestLogging } from "@amkisko/json-logging/next";
7
+ * import { httpLogger } from "@amkisko/json-logging/http";
8
+ *
9
+ * export default withRequestLogging(async (req) => {
10
+ * httpLogger().info({ path: req.nextUrl?.pathname });
11
+ * });
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ import { allocateRequestId, defaultRandomId, fallbackRequestId } from "./http-context.js";
17
+ import type { Logger } from "./logger.js";
18
+ import type { LoggerOptions } from "./types.js";
19
+ /** Minimal Next request shape used by the wrapper (no Next runtime dependency). */
20
+ export type RequestLike = {
21
+ headers: Headers;
22
+ method?: string;
23
+ url?: string;
24
+ nextUrl?: {
25
+ pathname?: string;
26
+ };
27
+ };
28
+ /** Options for {@link withRequestLogging}. */
29
+ export type WithRequestLoggingOptions = {
30
+ headerName?: string;
31
+ logger?: Logger;
32
+ loggerOptions?: LoggerOptions;
33
+ };
34
+ /** Re-export request-id helpers for Next entrypoint consumers. */
35
+ export { allocateRequestId, defaultRandomId, fallbackRequestId };
36
+ /**
37
+ * Wrap a Next.js middleware or route handler with request-scoped context.
38
+ * Bind loggers via `@amkisko/json-logging/http` (`bindHttpLogger` / `httpLogger`).
39
+ */
40
+ export declare function withRequestLogging<TArgs extends unknown[], TResult>(handler: (request: RequestLike, ...rest: TArgs) => TResult | Promise<TResult>, options?: WithRequestLoggingOptions): (request: RequestLike, ...rest: TArgs) => Promise<TResult>;
41
+ //# sourceMappingURL=next.d.ts.map
package/dist/next.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Next.js helpers: wrap middleware or route handlers with request-scoped log context.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { withRequestLogging } from "@amkisko/json-logging/next";
7
+ * import { httpLogger } from "@amkisko/json-logging/http";
8
+ *
9
+ * export default withRequestLogging(async (req) => {
10
+ * httpLogger().info({ path: req.nextUrl?.pathname });
11
+ * });
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ import { bindLoggerFromOptions } from "./http-ambient.js";
17
+ import { allocateRequestId, defaultRandomId, fallbackRequestId, withHttpContext, } from "./http-context.js";
18
+ /** Re-export request-id helpers for Next entrypoint consumers. */
19
+ export { allocateRequestId, defaultRandomId, fallbackRequestId };
20
+ /**
21
+ * Wrap a Next.js middleware or route handler with request-scoped context.
22
+ * Bind loggers via `@amkisko/json-logging/http` (`bindHttpLogger` / `httpLogger`).
23
+ */
24
+ export function withRequestLogging(handler, options = {}) {
25
+ bindLoggerFromOptions(options);
26
+ return async (request, ...rest) => {
27
+ return (await withHttpContext({
28
+ headers: request.headers,
29
+ method: request.method,
30
+ url: request.url,
31
+ path: request.nextUrl?.pathname,
32
+ }, () => handler(request, ...rest), options));
33
+ };
34
+ }
35
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1,12 @@
1
+ export declare function isPlainObject(value: unknown): value is {
2
+ [key: string]: unknown;
3
+ };
4
+ export declare function plainObjectFrom(value: object): {
5
+ [key: string]: unknown;
6
+ };
7
+ export declare function readObjectField(record: {
8
+ [key: string]: unknown;
9
+ }, key: string): {
10
+ [key: string]: unknown;
11
+ };
12
+ //# sourceMappingURL=objects.d.ts.map
@@ -0,0 +1,18 @@
1
+ export function isPlainObject(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ export function plainObjectFrom(value) {
5
+ const result = {};
6
+ for (const key of Object.keys(value)) {
7
+ result[key] = Reflect.get(value, key);
8
+ }
9
+ return result;
10
+ }
11
+ export function readObjectField(record, key) {
12
+ const value = record[key];
13
+ if (isPlainObject(value)) {
14
+ return value;
15
+ }
16
+ return {};
17
+ }
18
+ //# sourceMappingURL=objects.js.map