@kamranbiglari/pino-logger 1.0.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/dist/index.mjs ADDED
@@ -0,0 +1,301 @@
1
+ // src/logger.ts
2
+ import pino from "pino";
3
+
4
+ // src/types.ts
5
+ var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "alert"];
6
+
7
+ // src/resolve-level.ts
8
+ function resolveLogLevel() {
9
+ if (process.env.LOG_VERBOSE === "true") {
10
+ return "trace";
11
+ }
12
+ const raw = process.env.LOG_LEVEL?.toLowerCase().trim();
13
+ if (!raw) return "info";
14
+ if (LOG_LEVELS.includes(raw)) {
15
+ return raw;
16
+ }
17
+ process.stderr.write(
18
+ `[pino-logger] Invalid LOG_LEVEL="${raw}". Valid values: ${LOG_LEVELS.join(", ")}. Falling back to "info".
19
+ `
20
+ );
21
+ return "info";
22
+ }
23
+
24
+ // src/context.ts
25
+ import { AsyncLocalStorage } from "async_hooks";
26
+ var storage = new AsyncLocalStorage();
27
+ function withContext(context, fn) {
28
+ const existing = storage.getStore();
29
+ const merged = existing ? { ...existing, ...context } : context;
30
+ return storage.run(merged, fn);
31
+ }
32
+ function getContext() {
33
+ return storage.getStore();
34
+ }
35
+
36
+ // src/logger.ts
37
+ var LEVEL_OBJECTS = Object.fromEntries(
38
+ LOG_LEVELS.map((l) => [l, { level: l }])
39
+ );
40
+ var LEVEL_PREFIXES = Object.fromEntries(
41
+ LOG_LEVELS.map((l) => [l, `[${l.toUpperCase()}] `])
42
+ );
43
+ var ALERT_LEVEL_NUM = 70;
44
+ var ALERT_LEVEL_NAME = "alert";
45
+ function buildTransport() {
46
+ if (process.env.NODE_ENV !== "development") return void 0;
47
+ return {
48
+ target: "pino-pretty",
49
+ options: {
50
+ colorize: true,
51
+ translateTime: "HH:MM:ss.l",
52
+ ignore: "pid,hostname",
53
+ messageFormat: "{msg}"
54
+ }
55
+ };
56
+ }
57
+ var PinoLogger = class _PinoLogger {
58
+ constructor(pinoInstance, sample, sampleState) {
59
+ this._pino = pinoInstance;
60
+ this._sample = sample;
61
+ this._sampleState = sampleState ?? /* @__PURE__ */ new Map();
62
+ }
63
+ // ── core log dispatch ────────────────────────────────────────────────────
64
+ /**
65
+ * Internal log dispatch. Handles sampling, async context merge,
66
+ * and level prefix injection. Optimised for the common fast path
67
+ * (no context, no sampling) to avoid object allocation.
68
+ */
69
+ _log(level, objOrMsg, msg) {
70
+ const rate = this._sample?.[level];
71
+ let sampleFields = null;
72
+ if (rate && rate > 1) {
73
+ let state = this._sampleState.get(level);
74
+ if (!state) {
75
+ state = { since_last_emit: 0, total: 0 };
76
+ this._sampleState.set(level, state);
77
+ }
78
+ state.total++;
79
+ state.since_last_emit++;
80
+ if (state.since_last_emit < rate) return;
81
+ sampleFields = { sampled_count: state.since_last_emit, sampled_total: state.total };
82
+ state.since_last_emit = 0;
83
+ }
84
+ const ctx = getContext();
85
+ const hasExtra = ctx !== void 0 || sampleFields !== null;
86
+ const logFn = this._pino[level].bind(this._pino);
87
+ const prefix = LEVEL_PREFIXES[level] ?? `[${level.toUpperCase()}] `;
88
+ if (typeof objOrMsg === "string") {
89
+ const message = prefix + objOrMsg;
90
+ if (hasExtra) {
91
+ const merged = sampleFields ? ctx ? { ...ctx, ...sampleFields } : sampleFields : ctx;
92
+ logFn(merged, message);
93
+ } else {
94
+ logFn(message);
95
+ }
96
+ } else {
97
+ const message = prefix + msg;
98
+ if (hasExtra) {
99
+ const merged = { ...ctx, ...sampleFields, ...objOrMsg };
100
+ logFn(merged, message);
101
+ } else {
102
+ logFn(objOrMsg, message);
103
+ }
104
+ }
105
+ }
106
+ trace(objOrMsg, msg) {
107
+ this._log("trace", objOrMsg, msg);
108
+ }
109
+ debug(objOrMsg, msg) {
110
+ this._log("debug", objOrMsg, msg);
111
+ }
112
+ info(objOrMsg, msg) {
113
+ this._log("info", objOrMsg, msg);
114
+ }
115
+ warn(objOrMsg, msg) {
116
+ this._log("warn", objOrMsg, msg);
117
+ }
118
+ error(objOrMsg, msg) {
119
+ this._log("error", objOrMsg, msg);
120
+ }
121
+ fatal(objOrMsg, msg) {
122
+ this._log("fatal", objOrMsg, msg);
123
+ this.flushSampleCounts();
124
+ try {
125
+ this._pino.flush?.();
126
+ } catch {
127
+ }
128
+ process.exit(1);
129
+ }
130
+ alert(objOrMsg, msg) {
131
+ this._log("alert", objOrMsg, msg);
132
+ }
133
+ // ── child ────────────────────────────────────────────────────────────────
134
+ /**
135
+ * Creates a child logger with additional bound fields.
136
+ * Shares sample counters with the parent for consistent global sampling.
137
+ */
138
+ child(bindings) {
139
+ return new _PinoLogger(this._pino.child(bindings), this._sample, this._sampleState);
140
+ }
141
+ // ── startTimer ───────────────────────────────────────────────────────────
142
+ /**
143
+ * Starts a high-resolution timer. Returns a Timer object with:
144
+ * - elapsed(): returns ms elapsed so far
145
+ * - done(msg) / done(obj, msg): logs at info level with duration_ms
146
+ *
147
+ * @example
148
+ * const timer = logger.startTimer();
149
+ * await processRequest();
150
+ * timer.done({ req_id }, 'request processed'); // includes duration_ms
151
+ */
152
+ startTimer() {
153
+ const start = process.hrtime.bigint();
154
+ const self = this;
155
+ return {
156
+ elapsed() {
157
+ return Number(process.hrtime.bigint() - start) / 1e6;
158
+ },
159
+ done(objOrMsg, msg) {
160
+ const duration_ms = Math.round(Number(process.hrtime.bigint() - start) * 100 / 1e6) / 100;
161
+ if (typeof objOrMsg === "string") {
162
+ self._log("info", { duration_ms }, objOrMsg);
163
+ } else {
164
+ self._log("info", { ...objOrMsg, duration_ms }, msg);
165
+ }
166
+ }
167
+ };
168
+ }
169
+ // ── metric ───────────────────────────────────────────────────────────────
170
+ /**
171
+ * Emits a structured metric log at info level.
172
+ * All metric logs include `metric_type: "metric"` for easy filtering
173
+ * in your log parser / aggregator.
174
+ *
175
+ * @example
176
+ * logger.metric({ metric_name: 'http_request_duration', metric_value: 42, metric_unit: 'ms' });
177
+ * logger.metric({ metric_name: 'queue_depth', metric_value: 150, metric_unit: 'count', queue: 'orders' });
178
+ */
179
+ metric(fields) {
180
+ const { metric_name, ...rest } = fields;
181
+ this._log("info", { metric_type: "metric", metric_name, ...rest }, `metric: ${metric_name}`);
182
+ }
183
+ // ── sampling ─────────────────────────────────────────────────────────────
184
+ /**
185
+ * Flushes any remaining sample counters as summary log lines.
186
+ * Called automatically by fatal() and shutdown().
187
+ * Call manually if you need to ensure all counts are emitted.
188
+ */
189
+ flushSampleCounts() {
190
+ if (!this._sample) return;
191
+ for (const [level, state] of this._sampleState.entries()) {
192
+ if (state.since_last_emit > 0) {
193
+ const prefix = LEVEL_PREFIXES[level] ?? `[${level.toUpperCase()}] `;
194
+ const logFn = this._pino[level].bind(this._pino);
195
+ logFn(
196
+ { sampled_count: state.since_last_emit, sampled_total: state.total },
197
+ prefix + "sampled log flush"
198
+ );
199
+ state.since_last_emit = 0;
200
+ }
201
+ }
202
+ }
203
+ // ── shutdown ─────────────────────────────────────────────────────────────
204
+ /**
205
+ * Gracefully shuts down the logger:
206
+ * 1. Flushes any remaining sample counters
207
+ * 2. Flushes the Pino write buffer (important for async transports)
208
+ *
209
+ * Call this on SIGTERM / SIGINT before process exit.
210
+ *
211
+ * @example
212
+ * process.on('SIGTERM', async () => {
213
+ * logger.info('shutting down');
214
+ * await logger.shutdown();
215
+ * process.exit(0);
216
+ * });
217
+ */
218
+ async shutdown() {
219
+ this.flushSampleCounts();
220
+ return new Promise((resolve, reject) => {
221
+ if (typeof this._pino.flush === "function") {
222
+ this._pino.flush((err) => {
223
+ if (err) reject(err);
224
+ else resolve();
225
+ });
226
+ } else {
227
+ resolve();
228
+ }
229
+ });
230
+ }
231
+ // ── isLevelEnabled ───────────────────────────────────────────────────────
232
+ isLevelEnabled(level) {
233
+ return this._pino.isLevelEnabled(level);
234
+ }
235
+ // ── instance ─────────────────────────────────────────────────────────────
236
+ /**
237
+ * Exposes the raw Pino Logger instance.
238
+ * Use for integrations that require it directly (e.g. pino-http).
239
+ */
240
+ get instance() {
241
+ return this._pino;
242
+ }
243
+ };
244
+ function createLogger(fields) {
245
+ const pinoInstance = pino({
246
+ level: resolveLogLevel(),
247
+ // Register custom "alert" level above fatal (70 > 60)
248
+ customLevels: { [ALERT_LEVEL_NAME]: ALERT_LEVEL_NUM },
249
+ // Merged into every log line
250
+ base: {
251
+ service: fields.service,
252
+ env: fields.env ?? process.env.NODE_ENV ?? "production",
253
+ version: fields.version ?? process.env.npm_package_version ?? "unknown"
254
+ },
255
+ // ISO timestamp on every line
256
+ timestamp: pino.stdTimeFunctions.isoTime,
257
+ // Use string level labels (info/warn/error) not numeric codes (30/40/50)
258
+ // Uses pre-allocated objects to avoid per-call allocation + GC pressure.
259
+ formatters: {
260
+ level(label) {
261
+ return LEVEL_OBJECTS[label] ?? { level: label };
262
+ }
263
+ },
264
+ // Serialise Error objects: message, stack, code, type
265
+ serializers: {
266
+ err: pino.stdSerializers.err,
267
+ error: pino.stdSerializers.err
268
+ },
269
+ // Redact sensitive fields before they reach stdout
270
+ // NOTE: Avoid wildcard paths (*.field) — they force Pino to walk
271
+ // the entire object tree on every log call, which is expensive.
272
+ // Use explicit paths for predictable O(1) redaction.
273
+ redact: {
274
+ paths: [
275
+ "req.headers.authorization",
276
+ "req.headers.cookie",
277
+ "body.password",
278
+ "body.api_key",
279
+ "body.token",
280
+ "body.secret",
281
+ "headers.authorization",
282
+ "headers.cookie",
283
+ "password",
284
+ "apiKey",
285
+ "api_key",
286
+ "secret"
287
+ ],
288
+ censor: "[REDACTED]"
289
+ },
290
+ transport: buildTransport()
291
+ });
292
+ return new PinoLogger(pinoInstance, fields.sample);
293
+ }
294
+ export {
295
+ LOG_LEVELS,
296
+ PinoLogger,
297
+ createLogger,
298
+ getContext,
299
+ withContext
300
+ };
301
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logger.ts","../src/types.ts","../src/resolve-level.ts","../src/context.ts"],"sourcesContent":["import pino, { LoggerOptions, LogFn } from 'pino';\nimport { CreateLoggerOptions, LOG_LEVELS, LogLevel, MetricFields, Timer } from './types.js';\nimport { resolveLogLevel } from './resolve-level.js';\nimport { getContext } from './context.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n// Pino logger with custom 'alert' level added\ntype PinoInstance = pino.Logger<'alert'>;\n\n// A function that can log — covers all Pino level methods including custom ones\ntype PinoLogFn = LogFn;\n\n// ─── Pre-allocated constants ──────────────────────────────────────────────────\n\n// Pre-allocate level label objects to avoid creating a new object on every log call.\n// This eliminates per-call GC pressure from the formatters.level function.\nconst LEVEL_OBJECTS: Record<string, { level: string }> = Object.fromEntries(\n LOG_LEVELS.map((l) => [l, { level: l }]),\n);\n\n// Pre-allocate level prefix strings: \"[INFO] \", \"[ERROR] \", etc.\nconst LEVEL_PREFIXES: Record<string, string> = Object.fromEntries(\n LOG_LEVELS.map((l) => [l, `[${l.toUpperCase()}] `]),\n);\n\n// Pino's built-in numeric levels max at fatal=60. Alert is above fatal.\nconst ALERT_LEVEL_NUM = 70;\nconst ALERT_LEVEL_NAME = 'alert';\n\n// ─── Sample state ─────────────────────────────────────────────────────────────\n\ninterface SampleState {\n since_last_emit: number;\n total: number;\n}\n\n// ─── Transport ────────────────────────────────────────────────────────────────\n\nfunction buildTransport(): LoggerOptions['transport'] {\n if (process.env.NODE_ENV !== 'development') return undefined;\n\n return {\n target: 'pino-pretty',\n options: {\n colorize: true,\n translateTime: 'HH:MM:ss.l',\n ignore: 'pid,hostname',\n messageFormat: '{msg}',\n },\n };\n}\n\n// ─── PinoLogger ───────────────────────────────────────────────────────────────\n\n/**\n * Opinionated Pino wrapper with:\n * - LOG_LEVEL / LOG_VERBOSE env var control\n * - Level prefix in messages: \"[INFO] msg\" for log parser alerting\n * - Full Error serialisation (message, stack, code) via `err` field\n * - alert() — highest severity level (above fatal), does NOT kill process\n * - fatal() kills the process with exit code 1\n * - child() returns PinoLogger (not raw Pino instance)\n * - AsyncLocalStorage context propagation (withContext)\n * - Request duration tracking (startTimer)\n * - Structured metric logging (metric)\n * - Log sampling with counting (no data loss)\n * - Graceful async shutdown\n * - pino-pretty in development (NODE_ENV=development)\n * - Sensitive field redaction\n */\nexport class PinoLogger {\n private readonly _pino: PinoInstance;\n private readonly _sample: Partial<Record<LogLevel, number>> | undefined;\n // Shared across parent + child loggers so sampling counters are global\n private readonly _sampleState: Map<string, SampleState>;\n\n constructor(\n pinoInstance: PinoInstance,\n sample?: Partial<Record<LogLevel, number>>,\n sampleState?: Map<string, SampleState>,\n ) {\n this._pino = pinoInstance;\n this._sample = sample;\n this._sampleState = sampleState ?? new Map();\n }\n\n // ── core log dispatch ────────────────────────────────────────────────────\n\n /**\n * Internal log dispatch. Handles sampling, async context merge,\n * and level prefix injection. Optimised for the common fast path\n * (no context, no sampling) to avoid object allocation.\n */\n private _log(level: LogLevel, objOrMsg: Record<string, unknown> | string, msg?: string): void {\n // ── Sampling gate ──\n const rate = this._sample?.[level];\n let sampleFields: { sampled_count: number; sampled_total: number } | null = null;\n if (rate && rate > 1) {\n let state = this._sampleState.get(level);\n if (!state) {\n state = { since_last_emit: 0, total: 0 };\n this._sampleState.set(level, state);\n }\n state.total++;\n state.since_last_emit++;\n if (state.since_last_emit < rate) return; // skip but counted\n sampleFields = { sampled_count: state.since_last_emit, sampled_total: state.total };\n state.since_last_emit = 0;\n }\n\n // ── Async context ──\n const ctx = getContext();\n const hasExtra = ctx !== undefined || sampleFields !== null;\n\n // ── Resolve the Pino log function for this level ──\n const logFn: PinoLogFn = (this._pino[level] as PinoLogFn).bind(this._pino);\n\n // ── Emit with level prefix in message ──\n const prefix = LEVEL_PREFIXES[level] ?? `[${level.toUpperCase()}] `;\n\n if (typeof objOrMsg === 'string') {\n const message = prefix + objOrMsg;\n if (hasExtra) {\n const merged = sampleFields\n ? ctx ? { ...ctx, ...sampleFields } : sampleFields\n : ctx!;\n logFn(merged, message);\n } else {\n logFn(message);\n }\n } else {\n const message = prefix + msg!;\n if (hasExtra) {\n const merged = { ...ctx, ...sampleFields, ...objOrMsg };\n logFn(merged, message);\n } else {\n logFn(objOrMsg, message);\n }\n }\n }\n\n // ── trace ────────────────────────────────────────────────────────────────\n\n trace(obj: Record<string, unknown>, msg: string): void;\n trace(msg: string): void;\n trace(objOrMsg: Record<string, unknown> | string, msg?: string): void {\n this._log('trace', objOrMsg, msg);\n }\n\n // ── debug ────────────────────────────────────────────────────────────────\n\n debug(obj: Record<string, unknown>, msg: string): void;\n debug(msg: string): void;\n debug(objOrMsg: Record<string, unknown> | string, msg?: string): void {\n this._log('debug', objOrMsg, msg);\n }\n\n // ── info ─────────────────────────────────────────────────────────────────\n\n info(obj: Record<string, unknown>, msg: string): void;\n info(msg: string): void;\n info(objOrMsg: Record<string, unknown> | string, msg?: string): void {\n this._log('info', objOrMsg, msg);\n }\n\n // ── warn ─────────────────────────────────────────────────────────────────\n\n warn(obj: Record<string, unknown>, msg: string): void;\n warn(msg: string): void;\n warn(objOrMsg: Record<string, unknown> | string, msg?: string): void {\n this._log('warn', objOrMsg, msg);\n }\n\n // ── error ────────────────────────────────────────────────────────────────\n\n error(obj: Record<string, unknown>, msg: string): void;\n error(msg: string): void;\n error(objOrMsg: Record<string, unknown> | string, msg?: string): void {\n this._log('error', objOrMsg, msg);\n }\n\n // ── fatal ────────────────────────────────────────────────────────────────\n\n /**\n * Logs at FATAL level then kills the process with exit code 1.\n * Flushes sample counters and log buffer before exiting.\n */\n fatal(obj: Record<string, unknown>, msg: string): never;\n fatal(msg: string): never;\n fatal(objOrMsg: Record<string, unknown> | string, msg?: string): never {\n this._log('fatal', objOrMsg, msg);\n this.flushSampleCounts();\n try { this._pino.flush?.(); } catch { /* exiting anyway */ }\n process.exit(1);\n }\n\n // ── alert ────────────────────────────────────────────────────────────────\n\n /**\n * Logs at ALERT level — highest severity, above fatal.\n * Use for conditions requiring immediate operator attention:\n * - Security breaches detected\n * - Data corruption detected\n * - Critical SLA violations\n *\n * Unlike fatal(), alert() does NOT kill the process.\n * The service continues running so it can handle other requests.\n *\n * @example\n * logger.alert({ breach_type: 'unauthorized_access', ip }, 'security breach detected');\n */\n alert(obj: Record<string, unknown>, msg: string): void;\n alert(msg: string): void;\n alert(objOrMsg: Record<string, unknown> | string, msg?: string): void {\n this._log('alert', objOrMsg, msg);\n }\n\n // ── child ────────────────────────────────────────────────────────────────\n\n /**\n * Creates a child logger with additional bound fields.\n * Shares sample counters with the parent for consistent global sampling.\n */\n child(bindings: Record<string, unknown>): PinoLogger {\n return new PinoLogger(this._pino.child(bindings), this._sample, this._sampleState);\n }\n\n // ── startTimer ───────────────────────────────────────────────────────────\n\n /**\n * Starts a high-resolution timer. Returns a Timer object with:\n * - elapsed(): returns ms elapsed so far\n * - done(msg) / done(obj, msg): logs at info level with duration_ms\n *\n * @example\n * const timer = logger.startTimer();\n * await processRequest();\n * timer.done({ req_id }, 'request processed'); // includes duration_ms\n */\n startTimer(): Timer {\n const start = process.hrtime.bigint();\n const self = this;\n return {\n elapsed(): number {\n return Number(process.hrtime.bigint() - start) / 1_000_000;\n },\n done(objOrMsg: Record<string, unknown> | string, msg?: string): void {\n const duration_ms = Math.round(Number(process.hrtime.bigint() - start) * 100 / 1_000_000) / 100;\n if (typeof objOrMsg === 'string') {\n self._log('info', { duration_ms }, objOrMsg);\n } else {\n self._log('info', { ...objOrMsg, duration_ms }, msg!);\n }\n },\n } as Timer;\n }\n\n // ── metric ───────────────────────────────────────────────────────────────\n\n /**\n * Emits a structured metric log at info level.\n * All metric logs include `metric_type: \"metric\"` for easy filtering\n * in your log parser / aggregator.\n *\n * @example\n * logger.metric({ metric_name: 'http_request_duration', metric_value: 42, metric_unit: 'ms' });\n * logger.metric({ metric_name: 'queue_depth', metric_value: 150, metric_unit: 'count', queue: 'orders' });\n */\n metric(fields: MetricFields): void {\n const { metric_name, ...rest } = fields;\n this._log('info', { metric_type: 'metric', metric_name, ...rest }, `metric: ${metric_name}`);\n }\n\n // ── sampling ─────────────────────────────────────────────────────────────\n\n /**\n * Flushes any remaining sample counters as summary log lines.\n * Called automatically by fatal() and shutdown().\n * Call manually if you need to ensure all counts are emitted.\n */\n flushSampleCounts(): void {\n if (!this._sample) return;\n for (const [level, state] of this._sampleState.entries()) {\n if (state.since_last_emit > 0) {\n const prefix = LEVEL_PREFIXES[level] ?? `[${level.toUpperCase()}] `;\n const logFn: PinoLogFn = (this._pino[level as keyof PinoInstance] as PinoLogFn).bind(this._pino);\n logFn(\n { sampled_count: state.since_last_emit, sampled_total: state.total },\n prefix + 'sampled log flush',\n );\n state.since_last_emit = 0;\n }\n }\n }\n\n // ── shutdown ─────────────────────────────────────────────────────────────\n\n /**\n * Gracefully shuts down the logger:\n * 1. Flushes any remaining sample counters\n * 2. Flushes the Pino write buffer (important for async transports)\n *\n * Call this on SIGTERM / SIGINT before process exit.\n *\n * @example\n * process.on('SIGTERM', async () => {\n * logger.info('shutting down');\n * await logger.shutdown();\n * process.exit(0);\n * });\n */\n async shutdown(): Promise<void> {\n this.flushSampleCounts();\n return new Promise<void>((resolve, reject) => {\n if (typeof this._pino.flush === 'function') {\n this._pino.flush((err?: Error) => {\n if (err) reject(err);\n else resolve();\n });\n } else {\n resolve();\n }\n });\n }\n\n // ── isLevelEnabled ───────────────────────────────────────────────────────\n\n isLevelEnabled(level: LogLevel): boolean {\n return this._pino.isLevelEnabled(level);\n }\n\n // ── instance ─────────────────────────────────────────────────────────────\n\n /**\n * Exposes the raw Pino Logger instance.\n * Use for integrations that require it directly (e.g. pino-http).\n */\n get instance(): PinoInstance {\n return this._pino;\n }\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────────\n\n/**\n * Creates a configured PinoLogger instance.\n *\n * Reads from environment:\n * LOG_LEVEL — trace|debug|info|warn|error|fatal|alert (default: info)\n * LOG_VERBOSE — true|false forces trace level (default: false)\n * NODE_ENV — development enables pino-pretty (default: production)\n *\n * @example\n * const logger = createLogger({\n * service: 'windy-gateway',\n * version: process.env.npm_package_version,\n * env: process.env.NODE_ENV,\n * sample: { trace: 100, debug: 10 }, // emit every 100th trace, every 10th debug\n * });\n */\nexport function createLogger(fields: CreateLoggerOptions): PinoLogger {\n const pinoInstance = pino({\n level: resolveLogLevel(),\n\n // Register custom \"alert\" level above fatal (70 > 60)\n customLevels: { [ALERT_LEVEL_NAME]: ALERT_LEVEL_NUM },\n\n // Merged into every log line\n base: {\n service: fields.service,\n env: fields.env ?? process.env.NODE_ENV ?? 'production',\n version: fields.version ?? process.env.npm_package_version ?? 'unknown',\n },\n\n // ISO timestamp on every line\n timestamp: pino.stdTimeFunctions.isoTime,\n\n // Use string level labels (info/warn/error) not numeric codes (30/40/50)\n // Uses pre-allocated objects to avoid per-call allocation + GC pressure.\n formatters: {\n level(label) {\n return LEVEL_OBJECTS[label] ?? { level: label };\n },\n },\n\n // Serialise Error objects: message, stack, code, type\n serializers: {\n err: pino.stdSerializers.err,\n error: pino.stdSerializers.err,\n },\n\n // Redact sensitive fields before they reach stdout\n // NOTE: Avoid wildcard paths (*.field) — they force Pino to walk\n // the entire object tree on every log call, which is expensive.\n // Use explicit paths for predictable O(1) redaction.\n redact: {\n paths: [\n 'req.headers.authorization',\n 'req.headers.cookie',\n 'body.password',\n 'body.api_key',\n 'body.token',\n 'body.secret',\n 'headers.authorization',\n 'headers.cookie',\n 'password',\n 'apiKey',\n 'api_key',\n 'secret',\n ],\n censor: '[REDACTED]',\n },\n\n transport: buildTransport(),\n });\n\n return new PinoLogger(pinoInstance, fields.sample);\n}\n","export const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'alert'] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\n/**\n * Fields merged into every log line emitted by this service.\n * All fields follow snake_case naming convention.\n */\nexport interface BaseLogFields {\n /** Service identifier in kebab-case. e.g. 'windy-gateway', 'fx-api' */\n service: string;\n /** App version — recommended: process.env.npm_package_version */\n version?: string;\n /** Runtime environment — recommended: process.env.NODE_ENV */\n env?: string;\n}\n\n/**\n * Options for createLogger — extends BaseLogFields with sampling config.\n */\nexport interface CreateLoggerOptions extends BaseLogFields {\n /**\n * Sample rates per level. e.g. { trace: 100 } emits every 100th trace log.\n * Skipped logs are counted — the emitted log includes `sampled_count` and\n * `sampled_total` fields so no data is lost.\n * Levels not listed here (and warn/error/fatal/alert) always emit every log.\n */\n sample?: Partial<Record<LogLevel, number>>;\n}\n\n/**\n * Fields to include when logging an HTTP request context.\n * Bind these via logger.child() at request start.\n */\nexport interface RequestContext {\n req_id: string;\n method?: string;\n path?: string;\n user_id?: string;\n}\n\n/**\n * Fields to include when logging an error.\n * Always pass the raw Error object as `err` — Pino serialises it automatically.\n */\nexport interface ErrorContext {\n err: Error | unknown;\n req_id?: string;\n [key: string]: unknown;\n}\n\n/**\n * Fields for structured metric logging.\n */\nexport interface MetricFields {\n /** Metric identifier in snake_case. e.g. 'http_request_duration_ms' */\n metric_name: string;\n /** Numeric value of the metric */\n metric_value: number;\n /** Unit of measurement. e.g. 'ms', 'bytes', 'count' */\n metric_unit?: string;\n [key: string]: unknown;\n}\n\n/**\n * Timer returned by startTimer().\n */\nexport interface Timer {\n /** Returns elapsed time in milliseconds */\n elapsed(): number;\n /** Logs at info level with duration_ms field */\n done(msg: string): void;\n done(obj: Record<string, unknown>, msg: string): void;\n}\n","import { LOG_LEVELS, LogLevel } from './types.js';\n\n/**\n * Resolves the active log level from environment variables.\n *\n * Priority:\n * 1. LOG_VERBOSE=true → forces 'trace' (verbose mode)\n * 2. LOG_LEVEL → uses the specified level\n * 3. default → 'info'\n *\n * Invalid LOG_LEVEL values warn to stderr and fall back to 'info'.\n * This prevents silent log blackouts from typos in config.\n */\nexport function resolveLogLevel(): LogLevel {\n if (process.env.LOG_VERBOSE === 'true') {\n return 'trace';\n }\n\n const raw = process.env.LOG_LEVEL?.toLowerCase().trim();\n\n if (!raw) return 'info';\n\n if ((LOG_LEVELS as readonly string[]).includes(raw)) {\n return raw as LogLevel;\n }\n\n process.stderr.write(\n `[pino-logger] Invalid LOG_LEVEL=\"${raw}\". ` +\n `Valid values: ${LOG_LEVELS.join(', ')}. Falling back to \"info\".\\n`\n );\n\n return 'info';\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\n\nconst storage = new AsyncLocalStorage<Record<string, unknown>>();\n\n/**\n * Runs a function with context fields that are automatically merged\n * into every log call within that async scope.\n *\n * Contexts nest — inner calls inherit and can override outer fields.\n *\n * @example\n * // In middleware:\n * app.use((req, res, next) => {\n * withContext({ req_id: req.id, user_id: req.userId }, next);\n * });\n *\n * // Anywhere in the call stack — no need to pass logger around:\n * logger.info('processing'); // automatically includes req_id, user_id\n */\nexport function withContext<T>(context: Record<string, unknown>, fn: () => T): T {\n const existing = storage.getStore();\n const merged = existing ? { ...existing, ...context } : context;\n return storage.run(merged, fn);\n}\n\n/**\n * Returns the current async context, or undefined if none is active.\n * Used internally by PinoLogger to auto-merge context into log calls.\n */\nexport function getContext(): Record<string, unknown> | undefined {\n return storage.getStore();\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@kamranbiglari/pino-logger",
3
+ "version": "1.0.0",
4
+ "description": "Opinionated Pino wrapper for structured logging across Node.js services",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "rimraf dist && tsup",
20
+ "dev": "tsup --watch",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "typecheck": "tsc --noEmit",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/KamranBiglari/pino-logger.git"
29
+ },
30
+ "keywords": [
31
+ "logger",
32
+ "pino",
33
+ "structured-logging",
34
+ "typescript"
35
+ ],
36
+ "license": "UNLICENSED",
37
+ "dependencies": {
38
+ "pino": "^10.3.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.0.0",
42
+ "pino-pretty": "^13.1.3",
43
+ "rimraf": "^5.0.0",
44
+ "tsup": "^8.0.0",
45
+ "typescript": "^5.0.0",
46
+ "vitest": "^4.1.2"
47
+ },
48
+ "peerDependencies": {
49
+ "pino-pretty": ">=11.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "pino-pretty": {
53
+ "optional": true
54
+ }
55
+ }
56
+ }