@futuristic.digital/logger 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/README.md CHANGED
@@ -1,3 +1,49 @@
1
- # Temporary Holding Version
1
+ # `@futuristic.digital/logger`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Structured server logging. Every Node package that logs should go through this logger so production emits JSON lines, secrets are redacted, and request logs can carry an OpenTelemetry trace id.
4
+
5
+ This package uses `node:util` and `process.env`. It is server-only.
6
+
7
+ ## What Belongs Here
8
+
9
+ - `createLogger` / `createLoggerForModule` and the `Logger` type.
10
+ - `getTraceContext()` so a request-scoped child logger can attach `traceId` / `spanId`.
11
+ - Default redaction of common secret keys (`password`, `token`, `authorization`, …).
12
+
13
+ ## What Does Not
14
+
15
+ - Domain-specific log events or message catalogs. Callers pass a message string plus a context object.
16
+ - Browser logging. Client code uses `console` or nothing.
17
+
18
+ Keep this package independent of consuming applications.
19
+
20
+ ## Usage
21
+
22
+ Create one module-scoped logger from `import.meta.url`, then a per-request child with the active trace:
23
+
24
+ ```ts
25
+ import {
26
+ createLoggerForModule,
27
+ getTraceContext
28
+ } from "@futuristic.digital/logger";
29
+
30
+ const logger = createLoggerForModule(import.meta.url);
31
+
32
+ export async function handleRequest() {
33
+ const requestLogger = logger.child(getTraceContext());
34
+ requestLogger.info("Handled request", { requestId: "request-123" });
35
+ }
36
+ ```
37
+
38
+ `createLoggerForModule` names the logger from the file path (extensions stripped) so log lines stay stable across bundlers. `child` merges extra context; `withName` replaces the name.
39
+
40
+ ## Output
41
+
42
+ - **Development:** colored, human-readable lines with `util.inspect` context.
43
+ - **Production:** one JSON object per line (`ts`, `level`, `name`, `msg`, plus context). Level defaults to `debug` in development and `info` in production.
44
+
45
+ Context values are normalized to JSON (errors become `{ name, message, stack }`, dates to ISO, cycles to `"[Circular]"`) and then redacted. Passing `redactKeys` replaces the default list entirely — include every key you still need redacted. Do not log raw tokens, cookies, or request bodies that contain them — redaction is a backstop, not a license.
46
+
47
+ ## Testing
48
+
49
+ `npm run test -w @futuristic.digital/logger` runs Vitest against logger behavior via a custom `sink` (no console assertions). Prefer that override in unit tests so output stays deterministic.
@@ -0,0 +1,57 @@
1
+ import { type LogLevel } from "./log-level.js";
2
+ export type { LogLevel } from "./log-level.js";
3
+ /** Arbitrary structured fields attached to a log line (request ids, ids, errors, …). */
4
+ export type LogContext = Record<string, unknown>;
5
+ export interface LogEvent {
6
+ level: Exclude<LogLevel, "silent">;
7
+ name?: string;
8
+ message: string;
9
+ context: LogContext;
10
+ /** ISO-8601 timestamp taken when the line is emitted. */
11
+ timestamp: string;
12
+ }
13
+ export interface Logger {
14
+ debug(message: string, context?: LogContext): void;
15
+ info(message: string, context?: LogContext): void;
16
+ warn(message: string, context?: LogContext): void;
17
+ error(message: string, context?: LogContext): void;
18
+ /** Returns a logger that merges `context` into every subsequent line. */
19
+ child(context: LogContext): Logger;
20
+ /** Returns a logger with the same config but a different `name` label. */
21
+ withName(name: string): Logger;
22
+ isLevelEnabled(level: Exclude<LogLevel, "silent">): boolean;
23
+ }
24
+ export interface LoggerOptions {
25
+ name?: string;
26
+ level: LogLevel;
27
+ context?: LogContext;
28
+ /** Extra key names (case-insensitive) to replace with `[REDACTED]` in context. */
29
+ redactKeys?: string[];
30
+ /** Override the console transport — used by tests and alternate sinks. */
31
+ sink?: (event: LogEvent) => void;
32
+ }
33
+ /** OpenTelemetry ids for correlating a log line with the active span. */
34
+ export type TraceContext = {
35
+ traceId?: string;
36
+ spanId?: string;
37
+ };
38
+ /**
39
+ * Creates a logger with optional name, level, base context, redaction, and sink.
40
+ *
41
+ * @param options Partial overrides; omitted `level` uses LOG_LEVEL, then the Node env default.
42
+ */
43
+ export declare function createLogger(options?: Partial<LoggerOptions>): Logger;
44
+ /**
45
+ * Creates a module-scoped logger named from the caller's file path.
46
+ *
47
+ * @param moduleUrl Pass `import.meta.url` so the name tracks the source file.
48
+ * @param options Same as `createLogger`, except `name` is always derived.
49
+ */
50
+ export declare function createLoggerForModule(moduleUrl: string, options?: Omit<Partial<LoggerOptions>, "name">): Logger;
51
+ /**
52
+ * Reads the active OpenTelemetry span so a child logger can attach `traceId` / `spanId`.
53
+ * Returns an empty object when no span is active.
54
+ */
55
+ export declare function getTraceContext(): TraceContext;
56
+ /** Process-wide default logger (no module name). Prefer `createLoggerForModule` in app code. */
57
+ export declare const logger: Logger;
package/dist/index.js ADDED
@@ -0,0 +1,330 @@
1
+ import { trace } from "@opentelemetry/api";
2
+ import util from "node:util";
3
+ import { LEVEL_ORDER, resolveDefaultLevel } from "./log-level.js";
4
+ const DEFAULT_REDACT_KEYS = [
5
+ "password",
6
+ "pass",
7
+ "secret",
8
+ "token",
9
+ "access_token",
10
+ "refresh_token",
11
+ "authorization",
12
+ "cookie",
13
+ "set-cookie",
14
+ "api_key",
15
+ "apikey",
16
+ "client_secret"
17
+ ];
18
+ const REDACTED_VALUE = "[REDACTED]";
19
+ // Captured at module load so the sink format stays stable for the process lifetime.
20
+ const isProduction = process.env.NODE_ENV === "production";
21
+ const ANSI_RESET = "\x1b[0m";
22
+ const ANSI_DIM = "\x1b[2m";
23
+ const LEVEL_COLORS = {
24
+ debug: "\x1b[36m",
25
+ info: "\x1b[32m",
26
+ warn: "\x1b[33m",
27
+ error: "\x1b[31m"
28
+ };
29
+ /**
30
+ * Flattens an `Error` into JSON-safe fields sinks can serialize.
31
+ *
32
+ * @param error Any thrown or logged Error instance.
33
+ */
34
+ function normalizeError(error) {
35
+ return {
36
+ name: error.name,
37
+ message: error.message,
38
+ stack: error.stack ?? null
39
+ };
40
+ }
41
+ /**
42
+ * Converts any runtime value into JSON-safe data for structured logs.
43
+ * Dates become ISO strings, Errors become `{ name, message, stack }`, cycles
44
+ * become `"[Circular]"`, and unsupported types fall back to `String(value)`.
45
+ *
46
+ * @param value Context field or nested value from the caller.
47
+ * @param seen Objects already visiting on this path — detects cycles.
48
+ */
49
+ function normalizeValue(value, seen = new WeakSet()) {
50
+ if (value === null || value === undefined) {
51
+ return null;
52
+ }
53
+ if (typeof value === "string" ||
54
+ typeof value === "number" ||
55
+ typeof value === "boolean") {
56
+ return value;
57
+ }
58
+ if (value instanceof Date) {
59
+ return value.toISOString();
60
+ }
61
+ if (value instanceof Error) {
62
+ return normalizeError(value);
63
+ }
64
+ if (typeof value === "bigint") {
65
+ return value.toString();
66
+ }
67
+ if (Array.isArray(value)) {
68
+ return value.map((item) => normalizeValue(item, seen));
69
+ }
70
+ if (typeof value === "object") {
71
+ const objectValue = value;
72
+ if (seen.has(objectValue)) {
73
+ return "[Circular]";
74
+ }
75
+ seen.add(objectValue);
76
+ const normalizedEntries = Object.entries(objectValue).map(([key, entryValue]) => [key, normalizeValue(entryValue, seen)]);
77
+ // Drop after this branch so the same object can appear under sibling keys
78
+ // without being treated as a cycle.
79
+ seen.delete(objectValue);
80
+ return Object.fromEntries(normalizedEntries);
81
+ }
82
+ return String(value);
83
+ }
84
+ /**
85
+ * Walks a normalized payload and replaces sensitive key values.
86
+ * Matching is case-insensitive on the key name only — values are not scanned.
87
+ *
88
+ * @param value Already-normalized JSON tree from `normalizeValue`.
89
+ * @param redactKeysSet Lowercased key names that must never leave the process.
90
+ */
91
+ function redactContext(value, redactKeysSet) {
92
+ if (Array.isArray(value)) {
93
+ return value.map((entry) => redactContext(entry, redactKeysSet));
94
+ }
95
+ if (value && typeof value === "object") {
96
+ const output = {};
97
+ for (const [key, nestedValue] of Object.entries(value)) {
98
+ if (redactKeysSet.has(key.toLowerCase())) {
99
+ output[key] = REDACTED_VALUE;
100
+ }
101
+ else {
102
+ output[key] = redactContext(nestedValue, redactKeysSet);
103
+ }
104
+ }
105
+ return output;
106
+ }
107
+ return value;
108
+ }
109
+ /**
110
+ * Merges caller context over base context so request-scoped values can override defaults.
111
+ *
112
+ * @param base Context baked into the logger (module or child).
113
+ * @param next Per-call context; omitted keys keep the base value.
114
+ */
115
+ function mergeContexts(base, next) {
116
+ if (!next) {
117
+ return base;
118
+ }
119
+ return { ...base, ...next };
120
+ }
121
+ /**
122
+ * Formats an ISO timestamp for the human-readable dev sink.
123
+ *
124
+ * @param timestamp ISO-8601 string from the log event.
125
+ * @returns `YYYY-MM-DD HH:mm:ss.SSS` in the process local zone, or the raw string if invalid.
126
+ */
127
+ function formatTimestamp(timestamp) {
128
+ const date = new Date(timestamp);
129
+ if (Number.isNaN(date.getTime())) {
130
+ return timestamp;
131
+ }
132
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}.${String(date.getMilliseconds()).padStart(3, "0")}`;
133
+ }
134
+ /**
135
+ * Default transport: colored human lines in development, JSON lines in production.
136
+ *
137
+ * @param event Fully normalized and redacted log event.
138
+ */
139
+ function defaultSink(event) {
140
+ const { level, message, name, context, timestamp } = event;
141
+ const prefix = name ? `[${name}]` : "";
142
+ if (!isProduction) {
143
+ const formattedTimestamp = formatTimestamp(timestamp);
144
+ const levelLabel = level.toUpperCase().padEnd(5, " ");
145
+ const coloredLevelLabel = `${LEVEL_COLORS[level]}${levelLabel}${ANSI_RESET}`;
146
+ const coloredPrefix = prefix ? `${ANSI_DIM}${prefix}${ANSI_RESET}` : "";
147
+ const line = `${formattedTimestamp} ${coloredLevelLabel} ${coloredPrefix} ${message}`.trim();
148
+ const hasContext = Object.keys(context).length > 0;
149
+ const formattedContext = hasContext
150
+ ? util.inspect(context, { depth: 10, colors: true })
151
+ : undefined;
152
+ if (level === "error") {
153
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
154
+ hasContext ? console.error(line, formattedContext) : console.error(line);
155
+ }
156
+ else if (level === "warn") {
157
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
158
+ hasContext ? console.warn(line, formattedContext) : console.warn(line);
159
+ }
160
+ else {
161
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
162
+ hasContext ? console.log(line, formattedContext) : console.log(line);
163
+ }
164
+ return;
165
+ }
166
+ const payload = {
167
+ ts: timestamp,
168
+ level,
169
+ name,
170
+ msg: message,
171
+ ...context
172
+ };
173
+ const line = JSON.stringify(payload);
174
+ if (level === "error") {
175
+ console.error(line);
176
+ }
177
+ else if (level === "warn") {
178
+ console.warn(line);
179
+ }
180
+ else {
181
+ console.log(line);
182
+ }
183
+ }
184
+ class BaseLogger {
185
+ name;
186
+ level;
187
+ baseContext;
188
+ redactKeysSet;
189
+ sink;
190
+ constructor(options) {
191
+ this.name = options.name;
192
+ this.level = options.level;
193
+ this.baseContext = options.context ?? {};
194
+ this.redactKeysSet = new Set((options.redactKeys ?? DEFAULT_REDACT_KEYS).map((key) => key.toLowerCase()));
195
+ this.sink = options.sink ?? defaultSink;
196
+ }
197
+ debug(message, context) {
198
+ this.log("debug", message, context);
199
+ }
200
+ info(message, context) {
201
+ this.log("info", message, context);
202
+ }
203
+ warn(message, context) {
204
+ this.log("warn", message, context);
205
+ }
206
+ error(message, context) {
207
+ this.log("error", message, context);
208
+ }
209
+ child(context) {
210
+ return new BaseLogger({
211
+ name: this.name,
212
+ level: this.level,
213
+ context: mergeContexts(this.baseContext, context),
214
+ redactKeys: [...this.redactKeysSet],
215
+ sink: this.sink
216
+ });
217
+ }
218
+ withName(name) {
219
+ return new BaseLogger({
220
+ name,
221
+ level: this.level,
222
+ context: this.baseContext,
223
+ redactKeys: [...this.redactKeysSet],
224
+ sink: this.sink
225
+ });
226
+ }
227
+ isLevelEnabled(level) {
228
+ return LEVEL_ORDER[level] >= LEVEL_ORDER[this.level];
229
+ }
230
+ log(level, message, context) {
231
+ if (!this.isLevelEnabled(level)) {
232
+ return;
233
+ }
234
+ // Normalize first, then redact, so every sink receives safe, predictable payloads.
235
+ const mergedContext = mergeContexts(this.baseContext, context);
236
+ const normalizedContext = normalizeValue(mergedContext);
237
+ const redactedContext = redactContext(normalizedContext, this.redactKeysSet);
238
+ this.sink({
239
+ level,
240
+ name: this.name,
241
+ message,
242
+ context: redactedContext,
243
+ timestamp: new Date().toISOString()
244
+ });
245
+ }
246
+ }
247
+ /**
248
+ * Creates a logger with optional name, level, base context, redaction, and sink.
249
+ *
250
+ * @param options Partial overrides; omitted `level` uses LOG_LEVEL, then the Node env default.
251
+ */
252
+ export function createLogger(options = {}) {
253
+ return new BaseLogger({
254
+ name: options.name,
255
+ level: options.level ?? resolveDefaultLevel(),
256
+ context: options.context,
257
+ redactKeys: options.redactKeys,
258
+ sink: options.sink
259
+ });
260
+ }
261
+ /**
262
+ * Normalizes Windows path separators so module names match across platforms.
263
+ *
264
+ * @param value Absolute or relative filesystem path.
265
+ */
266
+ function toPosixPath(value) {
267
+ return value.replaceAll("\\", "/");
268
+ }
269
+ /**
270
+ * Strips TS/JS extensions so logger names remain stable across build tools.
271
+ *
272
+ * @param value Path or bare module specifier that may end in `.ts` / `.js` / etc.
273
+ */
274
+ function stripKnownExtensions(value) {
275
+ return value.replace(/\.(?:m|c)?(?:t|j)sx?$/, "");
276
+ }
277
+ /**
278
+ * Builds a readable logger name from `import.meta.url` for module-scoped loggers.
279
+ * Prefers a path relative to `process.cwd()` when the URL is a `file://` under the repo.
280
+ *
281
+ * @param moduleUrl Typically `import.meta.url` from the calling module.
282
+ */
283
+ function deriveLoggerNameFromModuleUrl(moduleUrl) {
284
+ if (!moduleUrl) {
285
+ return "unknown-module";
286
+ }
287
+ try {
288
+ if (moduleUrl.startsWith("file://")) {
289
+ const pathname = toPosixPath(decodeURIComponent(new URL(moduleUrl).pathname));
290
+ const cwd = typeof process !== "undefined" && typeof process.cwd === "function"
291
+ ? toPosixPath(process.cwd())
292
+ : "";
293
+ const pathWithoutCwd = cwd && pathname.startsWith(`${cwd}/`)
294
+ ? pathname.slice(cwd.length + 1)
295
+ : pathname.replace(/^\/+/, "");
296
+ return stripKnownExtensions(pathWithoutCwd) || "unknown-module";
297
+ }
298
+ return stripKnownExtensions(moduleUrl);
299
+ }
300
+ catch {
301
+ return stripKnownExtensions(moduleUrl) || "unknown-module";
302
+ }
303
+ }
304
+ /**
305
+ * Creates a module-scoped logger named from the caller's file path.
306
+ *
307
+ * @param moduleUrl Pass `import.meta.url` so the name tracks the source file.
308
+ * @param options Same as `createLogger`, except `name` is always derived.
309
+ */
310
+ export function createLoggerForModule(moduleUrl, options = {}) {
311
+ return createLogger({
312
+ ...options,
313
+ name: deriveLoggerNameFromModuleUrl(moduleUrl)
314
+ });
315
+ }
316
+ /**
317
+ * Reads the active OpenTelemetry span so a child logger can attach `traceId` / `spanId`.
318
+ * Returns an empty object when no span is active.
319
+ */
320
+ export function getTraceContext() {
321
+ const span = trace.getActiveSpan();
322
+ if (!span) {
323
+ return {};
324
+ }
325
+ const { traceId, spanId } = span.spanContext();
326
+ return { traceId, spanId };
327
+ }
328
+ /** Process-wide default logger (no module name). Prefer `createLoggerForModule` in app code. */
329
+ export const logger = createLogger();
330
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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","sourcesContent":["import { trace } from \"@opentelemetry/api\";\nimport util from \"node:util\";\n\nimport {\n LEVEL_ORDER,\n resolveDefaultLevel,\n type LogLevel\n} from \"./log-level.js\";\n\nexport type { LogLevel } from \"./log-level.js\";\n\ntype JsonPrimitive = string | number | boolean | null;\ntype JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };\n\n/** Arbitrary structured fields attached to a log line (request ids, ids, errors, …). */\nexport type LogContext = Record<string, unknown>;\n\nexport interface LogEvent {\n level: Exclude<LogLevel, \"silent\">;\n name?: string;\n message: string;\n context: LogContext;\n /** ISO-8601 timestamp taken when the line is emitted. */\n timestamp: string;\n}\n\nexport interface Logger {\n debug(message: string, context?: LogContext): void;\n info(message: string, context?: LogContext): void;\n warn(message: string, context?: LogContext): void;\n error(message: string, context?: LogContext): void;\n /** Returns a logger that merges `context` into every subsequent line. */\n child(context: LogContext): Logger;\n /** Returns a logger with the same config but a different `name` label. */\n withName(name: string): Logger;\n isLevelEnabled(level: Exclude<LogLevel, \"silent\">): boolean;\n}\n\nexport interface LoggerOptions {\n name?: string;\n level: LogLevel;\n context?: LogContext;\n /** Extra key names (case-insensitive) to replace with `[REDACTED]` in context. */\n redactKeys?: string[];\n /** Override the console transport — used by tests and alternate sinks. */\n sink?: (event: LogEvent) => void;\n}\n\n/** OpenTelemetry ids for correlating a log line with the active span. */\nexport type TraceContext = {\n traceId?: string;\n spanId?: string;\n};\n\nconst DEFAULT_REDACT_KEYS = [\n \"password\",\n \"pass\",\n \"secret\",\n \"token\",\n \"access_token\",\n \"refresh_token\",\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"api_key\",\n \"apikey\",\n \"client_secret\"\n];\n\nconst REDACTED_VALUE = \"[REDACTED]\";\n\n// Captured at module load so the sink format stays stable for the process lifetime.\nconst isProduction = process.env.NODE_ENV === \"production\";\nconst ANSI_RESET = \"\\x1b[0m\";\nconst ANSI_DIM = \"\\x1b[2m\";\nconst LEVEL_COLORS: Record<Exclude<LogLevel, \"silent\">, string> = {\n debug: \"\\x1b[36m\",\n info: \"\\x1b[32m\",\n warn: \"\\x1b[33m\",\n error: \"\\x1b[31m\"\n};\n\n/**\n * Flattens an `Error` into JSON-safe fields sinks can serialize.\n *\n * @param error Any thrown or logged Error instance.\n */\nfunction normalizeError(error: Error): { [key: string]: JsonValue } {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack ?? null\n };\n}\n\n/**\n * Converts any runtime value into JSON-safe data for structured logs.\n * Dates become ISO strings, Errors become `{ name, message, stack }`, cycles\n * become `\"[Circular]\"`, and unsupported types fall back to `String(value)`.\n *\n * @param value Context field or nested value from the caller.\n * @param seen Objects already visiting on this path — detects cycles.\n */\nfunction normalizeValue(\n value: unknown,\n seen = new WeakSet<object>()\n): JsonValue {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (value instanceof Error) {\n return normalizeError(value);\n }\n\n if (typeof value === \"bigint\") {\n return value.toString();\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => normalizeValue(item, seen));\n }\n\n if (typeof value === \"object\") {\n const objectValue = value as Record<string, unknown>;\n\n if (seen.has(objectValue)) {\n return \"[Circular]\";\n }\n\n seen.add(objectValue);\n\n const normalizedEntries = Object.entries(objectValue).map(\n ([key, entryValue]) => [key, normalizeValue(entryValue, seen)]\n );\n\n // Drop after this branch so the same object can appear under sibling keys\n // without being treated as a cycle.\n seen.delete(objectValue);\n\n return Object.fromEntries(normalizedEntries);\n }\n\n return String(value);\n}\n\n/**\n * Walks a normalized payload and replaces sensitive key values.\n * Matching is case-insensitive on the key name only — values are not scanned.\n *\n * @param value Already-normalized JSON tree from `normalizeValue`.\n * @param redactKeysSet Lowercased key names that must never leave the process.\n */\nfunction redactContext(\n value: JsonValue,\n redactKeysSet: ReadonlySet<string>\n): JsonValue {\n if (Array.isArray(value)) {\n return value.map((entry) => redactContext(entry, redactKeysSet));\n }\n\n if (value && typeof value === \"object\") {\n const output: Record<string, JsonValue> = {};\n\n for (const [key, nestedValue] of Object.entries(value)) {\n if (redactKeysSet.has(key.toLowerCase())) {\n output[key] = REDACTED_VALUE;\n } else {\n output[key] = redactContext(nestedValue, redactKeysSet);\n }\n }\n\n return output;\n }\n\n return value;\n}\n\n/**\n * Merges caller context over base context so request-scoped values can override defaults.\n *\n * @param base Context baked into the logger (module or child).\n * @param next Per-call context; omitted keys keep the base value.\n */\nfunction mergeContexts(base: LogContext, next?: LogContext): LogContext {\n if (!next) {\n return base;\n }\n\n return { ...base, ...next };\n}\n\n/**\n * Formats an ISO timestamp for the human-readable dev sink.\n *\n * @param timestamp ISO-8601 string from the log event.\n * @returns `YYYY-MM-DD HH:mm:ss.SSS` in the process local zone, or the raw string if invalid.\n */\nfunction formatTimestamp(timestamp: string): string {\n const date = new Date(timestamp);\n if (Number.isNaN(date.getTime())) {\n return timestamp;\n }\n\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(\n 2,\n \"0\"\n )}-${String(date.getDate()).padStart(2, \"0\")} ${String(\n date.getHours()\n ).padStart(2, \"0\")}:${String(date.getMinutes()).padStart(2, \"0\")}:${String(\n date.getSeconds()\n ).padStart(2, \"0\")}.${String(date.getMilliseconds()).padStart(3, \"0\")}`;\n}\n\n/**\n * Default transport: colored human lines in development, JSON lines in production.\n *\n * @param event Fully normalized and redacted log event.\n */\nfunction defaultSink(event: LogEvent): void {\n const { level, message, name, context, timestamp } = event;\n const prefix = name ? `[${name}]` : \"\";\n\n if (!isProduction) {\n const formattedTimestamp = formatTimestamp(timestamp);\n const levelLabel = level.toUpperCase().padEnd(5, \" \");\n const coloredLevelLabel = `${LEVEL_COLORS[level]}${levelLabel}${ANSI_RESET}`;\n const coloredPrefix = prefix ? `${ANSI_DIM}${prefix}${ANSI_RESET}` : \"\";\n const line =\n `${formattedTimestamp} ${coloredLevelLabel} ${coloredPrefix} ${message}`.trim();\n const hasContext = Object.keys(context).length > 0;\n const formattedContext = hasContext\n ? util.inspect(context, { depth: 10, colors: true })\n : undefined;\n\n if (level === \"error\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-expressions\n hasContext ? console.error(line, formattedContext) : console.error(line);\n } else if (level === \"warn\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-expressions\n hasContext ? console.warn(line, formattedContext) : console.warn(line);\n } else {\n // eslint-disable-next-line @typescript-eslint/no-unused-expressions\n hasContext ? console.log(line, formattedContext) : console.log(line);\n }\n\n return;\n }\n\n const payload = {\n ts: timestamp,\n level,\n name,\n msg: message,\n ...context\n };\n\n const line = JSON.stringify(payload);\n\n if (level === \"error\") {\n console.error(line);\n } else if (level === \"warn\") {\n console.warn(line);\n } else {\n console.log(line);\n }\n}\n\nclass BaseLogger implements Logger {\n private readonly name?: string;\n private readonly level: LogLevel;\n private readonly baseContext: LogContext;\n private readonly redactKeysSet: ReadonlySet<string>;\n private readonly sink: (event: LogEvent) => void;\n\n constructor(options: LoggerOptions) {\n this.name = options.name;\n this.level = options.level;\n this.baseContext = options.context ?? {};\n this.redactKeysSet = new Set(\n (options.redactKeys ?? DEFAULT_REDACT_KEYS).map((key) =>\n key.toLowerCase()\n )\n );\n this.sink = options.sink ?? defaultSink;\n }\n\n debug(message: string, context?: LogContext): void {\n this.log(\"debug\", message, context);\n }\n\n info(message: string, context?: LogContext): void {\n this.log(\"info\", message, context);\n }\n\n warn(message: string, context?: LogContext): void {\n this.log(\"warn\", message, context);\n }\n\n error(message: string, context?: LogContext): void {\n this.log(\"error\", message, context);\n }\n\n child(context: LogContext): Logger {\n return new BaseLogger({\n name: this.name,\n level: this.level,\n context: mergeContexts(this.baseContext, context),\n redactKeys: [...this.redactKeysSet],\n sink: this.sink\n });\n }\n\n withName(name: string): Logger {\n return new BaseLogger({\n name,\n level: this.level,\n context: this.baseContext,\n redactKeys: [...this.redactKeysSet],\n sink: this.sink\n });\n }\n\n isLevelEnabled(level: Exclude<LogLevel, \"silent\">): boolean {\n return LEVEL_ORDER[level] >= LEVEL_ORDER[this.level];\n }\n\n private log(\n level: Exclude<LogLevel, \"silent\">,\n message: string,\n context?: LogContext\n ): void {\n if (!this.isLevelEnabled(level)) {\n return;\n }\n\n // Normalize first, then redact, so every sink receives safe, predictable payloads.\n const mergedContext = mergeContexts(this.baseContext, context);\n const normalizedContext = normalizeValue(mergedContext) as JsonValue;\n const redactedContext = redactContext(\n normalizedContext,\n this.redactKeysSet\n ) as LogContext;\n\n this.sink({\n level,\n name: this.name,\n message,\n context: redactedContext,\n timestamp: new Date().toISOString()\n });\n }\n}\n\n/**\n * Creates a logger with optional name, level, base context, redaction, and sink.\n *\n * @param options Partial overrides; omitted `level` uses LOG_LEVEL, then the Node env default.\n */\nexport function createLogger(options: Partial<LoggerOptions> = {}): Logger {\n return new BaseLogger({\n name: options.name,\n level: options.level ?? resolveDefaultLevel(),\n context: options.context,\n redactKeys: options.redactKeys,\n sink: options.sink\n });\n}\n\n/**\n * Normalizes Windows path separators so module names match across platforms.\n *\n * @param value Absolute or relative filesystem path.\n */\nfunction toPosixPath(value: string): string {\n return value.replaceAll(\"\\\\\", \"/\");\n}\n\n/**\n * Strips TS/JS extensions so logger names remain stable across build tools.\n *\n * @param value Path or bare module specifier that may end in `.ts` / `.js` / etc.\n */\nfunction stripKnownExtensions(value: string): string {\n return value.replace(/\\.(?:m|c)?(?:t|j)sx?$/, \"\");\n}\n\n/**\n * Builds a readable logger name from `import.meta.url` for module-scoped loggers.\n * Prefers a path relative to `process.cwd()` when the URL is a `file://` under the repo.\n *\n * @param moduleUrl Typically `import.meta.url` from the calling module.\n */\nfunction deriveLoggerNameFromModuleUrl(moduleUrl: string): string {\n if (!moduleUrl) {\n return \"unknown-module\";\n }\n\n try {\n if (moduleUrl.startsWith(\"file://\")) {\n const pathname = toPosixPath(\n decodeURIComponent(new URL(moduleUrl).pathname)\n );\n const cwd =\n typeof process !== \"undefined\" && typeof process.cwd === \"function\"\n ? toPosixPath(process.cwd())\n : \"\";\n\n const pathWithoutCwd =\n cwd && pathname.startsWith(`${cwd}/`)\n ? pathname.slice(cwd.length + 1)\n : pathname.replace(/^\\/+/, \"\");\n\n return stripKnownExtensions(pathWithoutCwd) || \"unknown-module\";\n }\n\n return stripKnownExtensions(moduleUrl);\n } catch {\n return stripKnownExtensions(moduleUrl) || \"unknown-module\";\n }\n}\n\n/**\n * Creates a module-scoped logger named from the caller's file path.\n *\n * @param moduleUrl Pass `import.meta.url` so the name tracks the source file.\n * @param options Same as `createLogger`, except `name` is always derived.\n */\nexport function createLoggerForModule(\n moduleUrl: string,\n options: Omit<Partial<LoggerOptions>, \"name\"> = {}\n): Logger {\n return createLogger({\n ...options,\n name: deriveLoggerNameFromModuleUrl(moduleUrl)\n });\n}\n\n/**\n * Reads the active OpenTelemetry span so a child logger can attach `traceId` / `spanId`.\n * Returns an empty object when no span is active.\n */\nexport function getTraceContext(): TraceContext {\n const span = trace.getActiveSpan();\n if (!span) {\n return {};\n }\n\n const { traceId, spanId } = span.spanContext();\n return { traceId, spanId };\n}\n\n/** Process-wide default logger (no module name). Prefer `createLoggerForModule` in app code. */\nexport const logger = createLogger();\n"]}
@@ -0,0 +1,5 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
2
+ /** Monotonic severity map used to filter log events. */
3
+ export declare const LEVEL_ORDER: Record<LogLevel, number>;
4
+ /** Uses LOG_LEVEL when valid, otherwise the production/development default. */
5
+ export declare function resolveDefaultLevel(): LogLevel;
@@ -0,0 +1,23 @@
1
+ /** Monotonic severity map used to filter log events. */
2
+ export const LEVEL_ORDER = {
3
+ debug: 10,
4
+ info: 20,
5
+ warn: 30,
6
+ error: 40,
7
+ silent: Number.POSITIVE_INFINITY
8
+ };
9
+ /** Uses LOG_LEVEL when valid, otherwise the production/development default. */
10
+ export function resolveDefaultLevel() {
11
+ const level = process.env.LOG_LEVEL;
12
+ switch (level) {
13
+ case "debug":
14
+ case "info":
15
+ case "warn":
16
+ case "error":
17
+ case "silent":
18
+ return level;
19
+ default:
20
+ return process.env.NODE_ENV === "production" ? "info" : "debug";
21
+ }
22
+ }
23
+ //# sourceMappingURL=log-level.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-level.js","sourceRoot":"","sources":["../log-level.ts"],"names":[],"mappings":"AAEA,wDAAwD;AACxD,MAAM,CAAC,MAAM,WAAW,GAA6B;IACnD,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,EAAE;IACR,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,MAAM,CAAC,iBAAiB;CACjC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,UAAU,mBAAmB;IACjC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;IAEpC,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,OAAO,CAAC;QACb,KAAK,MAAM,CAAC;QACZ,KAAK,MAAM,CAAC;QACZ,KAAK,OAAO,CAAC;QACb,KAAK,QAAQ;YACX,OAAO,KAAK,CAAC;QACf;YACE,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IACpE,CAAC;AACH,CAAC","sourcesContent":["export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"silent\";\n\n/** Monotonic severity map used to filter log events. */\nexport const LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n silent: Number.POSITIVE_INFINITY\n};\n\n/** Uses LOG_LEVEL when valid, otherwise the production/development default. */\nexport function resolveDefaultLevel(): LogLevel {\n const level = process.env.LOG_LEVEL;\n\n switch (level) {\n case \"debug\":\n case \"info\":\n case \"warn\":\n case \"error\":\n case \"silent\":\n return level;\n default:\n return process.env.NODE_ENV === \"production\" ? \"info\" : \"debug\";\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@futuristic.digital/logger",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "clean": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true });\"",
15
+ "build": "npm run clean && tsc -p tsconfig.json",
16
+ "check-types": "tsc -p tsconfig.json --noEmit",
17
+ "lint": "eslint .",
18
+ "test": "vitest run",
19
+ "prepack": "npm run build",
20
+ "prepublishOnly": "npm run check-types"
21
+ },
22
+ "dependencies": {
23
+ "@opentelemetry/api": "^1.9.0"
24
+ },
25
+ "description": "Structured Node.js logging with redaction and OpenTelemetry trace context.",
26
+ "license": "UNLICENSED",
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "sideEffects": false
38
+ }