@sprqvntrs/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/LICENSE +21 -0
- package/README.md +311 -0
- package/index.ts +53 -0
- package/package.json +54 -0
- package/src/context/async-context.ts +126 -0
- package/src/core/buffered-logger.ts +210 -0
- package/src/core/destinations.ts +63 -0
- package/src/core/logger.ts +208 -0
- package/src/core/pino-config.ts +92 -0
- package/src/core/serializers.ts +81 -0
- package/src/middleware/http.ts +225 -0
- package/src/presets/server.ts +206 -0
- package/src/testing/mock-logger.ts +234 -0
- package/src/types.ts +198 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Logger,
|
|
3
|
+
LogContext,
|
|
4
|
+
LogEntry,
|
|
5
|
+
LogLevel,
|
|
6
|
+
BufferedLogger,
|
|
7
|
+
BufferedLoggerOptions,
|
|
8
|
+
FlushDestination,
|
|
9
|
+
} from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a buffered logger that wraps an existing logger
|
|
13
|
+
* Logs are sent to the underlying logger AND buffered for later retrieval
|
|
14
|
+
*
|
|
15
|
+
* Key behavior: Child loggers share the parent's buffer
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { createLogger, createBufferedLogger, jsonDestination } from '@sprqvntrs/logger';
|
|
20
|
+
*
|
|
21
|
+
* const baseLogger = createLogger({ serviceName: 'my-service' });
|
|
22
|
+
* const buffered = createBufferedLogger({ logger: baseLogger });
|
|
23
|
+
*
|
|
24
|
+
* buffered.info('Request started', { path: '/api/users' });
|
|
25
|
+
*
|
|
26
|
+
* // Child loggers contribute to parent's buffer
|
|
27
|
+
* const childLogger = buffered.child({ userId: '123' });
|
|
28
|
+
* childLogger.info('User action');
|
|
29
|
+
*
|
|
30
|
+
* // Get all logs (including child logs)
|
|
31
|
+
* const logs = buffered.getBuffer();
|
|
32
|
+
* // logs.length === 2
|
|
33
|
+
*
|
|
34
|
+
* // Flush to JSON and clear
|
|
35
|
+
* const json = await buffered.flush(jsonDestination());
|
|
36
|
+
* // buffered.bufferSize === 0
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // With auto-flush on buffer full
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const buffered = createBufferedLogger({
|
|
43
|
+
* logger: baseLogger,
|
|
44
|
+
* maxBufferSize: 100,
|
|
45
|
+
* onBufferFull: async (entries) => {
|
|
46
|
+
* await sendToLogAggregator(entries);
|
|
47
|
+
* },
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function createBufferedLogger(options: BufferedLoggerOptions): BufferedLogger {
|
|
52
|
+
const { logger, maxBufferSize, onBufferFull } = options;
|
|
53
|
+
|
|
54
|
+
// Shared buffer that child loggers will also write to
|
|
55
|
+
const buffer: LogEntry[] = [];
|
|
56
|
+
|
|
57
|
+
// Track if we're currently flushing to prevent re-entrancy
|
|
58
|
+
let isFlushing = false;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Adds an entry to the buffer and handles auto-flush if needed
|
|
62
|
+
*/
|
|
63
|
+
async function addToBuffer(entry: LogEntry): Promise<void> {
|
|
64
|
+
buffer.push(entry);
|
|
65
|
+
|
|
66
|
+
// Check if we need to auto-flush
|
|
67
|
+
if (maxBufferSize && buffer.length >= maxBufferSize && onBufferFull && !isFlushing) {
|
|
68
|
+
isFlushing = true;
|
|
69
|
+
try {
|
|
70
|
+
const entriesToFlush = [...buffer];
|
|
71
|
+
buffer.length = 0;
|
|
72
|
+
await onBufferFull(entriesToFlush);
|
|
73
|
+
} finally {
|
|
74
|
+
isFlushing = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a log method that logs to the underlying logger and buffers
|
|
81
|
+
*/
|
|
82
|
+
function createLogMethod(level: LogLevel, underlyingLogger: Logger) {
|
|
83
|
+
return (message: string, context?: LogContext): void => {
|
|
84
|
+
// Log to underlying logger
|
|
85
|
+
underlyingLogger[level](message, context);
|
|
86
|
+
|
|
87
|
+
// Add to shared buffer (fire and forget for sync interface)
|
|
88
|
+
const entry: LogEntry = {
|
|
89
|
+
level,
|
|
90
|
+
message,
|
|
91
|
+
context,
|
|
92
|
+
timestamp: new Date(),
|
|
93
|
+
};
|
|
94
|
+
void addToBuffer(entry);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Creates a child buffered logger that shares the parent's buffer
|
|
100
|
+
*/
|
|
101
|
+
function createChildBufferedLogger(
|
|
102
|
+
childLogger: Logger,
|
|
103
|
+
bindings: Record<string, unknown>
|
|
104
|
+
): Logger {
|
|
105
|
+
// Note: Returns Logger, not BufferedLogger, because child should not
|
|
106
|
+
// expose buffer manipulation methods (only parent controls the buffer)
|
|
107
|
+
const child: Logger = {
|
|
108
|
+
trace: (message: string, context?: LogContext): void => {
|
|
109
|
+
childLogger.trace(message, context);
|
|
110
|
+
void addToBuffer({
|
|
111
|
+
level: 'trace',
|
|
112
|
+
message,
|
|
113
|
+
context: { ...bindings, ...context },
|
|
114
|
+
timestamp: new Date(),
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
debug: (message: string, context?: LogContext): void => {
|
|
118
|
+
childLogger.debug(message, context);
|
|
119
|
+
void addToBuffer({
|
|
120
|
+
level: 'debug',
|
|
121
|
+
message,
|
|
122
|
+
context: { ...bindings, ...context },
|
|
123
|
+
timestamp: new Date(),
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
info: (message: string, context?: LogContext): void => {
|
|
127
|
+
childLogger.info(message, context);
|
|
128
|
+
void addToBuffer({
|
|
129
|
+
level: 'info',
|
|
130
|
+
message,
|
|
131
|
+
context: { ...bindings, ...context },
|
|
132
|
+
timestamp: new Date(),
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
warn: (message: string, context?: LogContext): void => {
|
|
136
|
+
childLogger.warn(message, context);
|
|
137
|
+
void addToBuffer({
|
|
138
|
+
level: 'warn',
|
|
139
|
+
message,
|
|
140
|
+
context: { ...bindings, ...context },
|
|
141
|
+
timestamp: new Date(),
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
error: (message: string, context?: LogContext): void => {
|
|
145
|
+
childLogger.error(message, context);
|
|
146
|
+
void addToBuffer({
|
|
147
|
+
level: 'error',
|
|
148
|
+
message,
|
|
149
|
+
context: { ...bindings, ...context },
|
|
150
|
+
timestamp: new Date(),
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
fatal: (message: string, context?: LogContext): void => {
|
|
154
|
+
childLogger.fatal(message, context);
|
|
155
|
+
void addToBuffer({
|
|
156
|
+
level: 'fatal',
|
|
157
|
+
message,
|
|
158
|
+
context: { ...bindings, ...context },
|
|
159
|
+
timestamp: new Date(),
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
child(moreBindings: Record<string, unknown>): Logger {
|
|
163
|
+
const mergedBindings = { ...bindings, ...moreBindings };
|
|
164
|
+
return createChildBufferedLogger(childLogger.child(moreBindings), mergedBindings);
|
|
165
|
+
},
|
|
166
|
+
get pino() {
|
|
167
|
+
return childLogger.pino;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
return child;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const bufferedLogger: BufferedLogger = {
|
|
174
|
+
trace: createLogMethod('trace', logger),
|
|
175
|
+
debug: createLogMethod('debug', logger),
|
|
176
|
+
info: createLogMethod('info', logger),
|
|
177
|
+
warn: createLogMethod('warn', logger),
|
|
178
|
+
error: createLogMethod('error', logger),
|
|
179
|
+
fatal: createLogMethod('fatal', logger),
|
|
180
|
+
|
|
181
|
+
child(bindings: Record<string, unknown>): Logger {
|
|
182
|
+
const underlyingChild = logger.child(bindings);
|
|
183
|
+
return createChildBufferedLogger(underlyingChild, bindings);
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
get pino() {
|
|
187
|
+
return logger.pino;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
getBuffer(): LogEntry[] {
|
|
191
|
+
return [...buffer];
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
clearBuffer(): void {
|
|
195
|
+
buffer.length = 0;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async flush<T>(destination: FlushDestination<T>): Promise<T> {
|
|
199
|
+
const entriesToFlush = [...buffer];
|
|
200
|
+
buffer.length = 0;
|
|
201
|
+
return destination.flush(entriesToFlush);
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
get bufferSize(): number {
|
|
205
|
+
return buffer.length;
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return bufferedLogger;
|
|
210
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { FlushDestination, LogEntry, SerializedLogEntry } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for the JSON destination
|
|
5
|
+
*/
|
|
6
|
+
export interface JsonDestinationOptions {
|
|
7
|
+
/** Additional metadata to include in the output */
|
|
8
|
+
metadata?: Record<string, unknown>;
|
|
9
|
+
/** Pretty print the JSON output (default: false) */
|
|
10
|
+
pretty?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serializes a LogEntry to a SerializedLogEntry with ISO timestamp
|
|
15
|
+
*/
|
|
16
|
+
function serializeLogEntry(entry: LogEntry): SerializedLogEntry {
|
|
17
|
+
return {
|
|
18
|
+
level: entry.level,
|
|
19
|
+
message: entry.message,
|
|
20
|
+
context: entry.context,
|
|
21
|
+
timestamp: entry.timestamp.toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a flush destination that returns a JSON string
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const buffered = createBufferedLogger({ logger });
|
|
31
|
+
*
|
|
32
|
+
* buffered.info('Request started');
|
|
33
|
+
* buffered.info('Processing', { step: 1 });
|
|
34
|
+
* buffered.info('Complete');
|
|
35
|
+
*
|
|
36
|
+
* const json = await buffered.flush(jsonDestination());
|
|
37
|
+
* // Returns: {"entries":[...], "count": 3}
|
|
38
|
+
*
|
|
39
|
+
* // With metadata:
|
|
40
|
+
* const json = await buffered.flush(jsonDestination({
|
|
41
|
+
* metadata: { requestId: '123', userId: 'user-1' }
|
|
42
|
+
* }));
|
|
43
|
+
* // Returns: {"entries":[...], "count": 3, "requestId": "123", "userId": "user-1"}
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function jsonDestination(options?: JsonDestinationOptions): FlushDestination<string> {
|
|
47
|
+
return {
|
|
48
|
+
async flush(entries: LogEntry[]): Promise<string> {
|
|
49
|
+
const serializedEntries = entries.map(serializeLogEntry);
|
|
50
|
+
|
|
51
|
+
const output: Record<string, unknown> = {
|
|
52
|
+
entries: serializedEntries,
|
|
53
|
+
count: serializedEntries.length,
|
|
54
|
+
...options?.metadata,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (options?.pretty) {
|
|
58
|
+
return JSON.stringify(output, null, 2);
|
|
59
|
+
}
|
|
60
|
+
return JSON.stringify(output);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Logger as PinoLogger } from 'pino';
|
|
2
|
+
import { createPinoInstance } from './pino-config';
|
|
3
|
+
import { getRequestContext } from '../context/async-context';
|
|
4
|
+
import type {
|
|
5
|
+
Logger,
|
|
6
|
+
LogContext,
|
|
7
|
+
CreateLoggerOptions,
|
|
8
|
+
LoggerConfigureOptions,
|
|
9
|
+
} from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wraps a Pino logger instance with our Logger interface
|
|
13
|
+
*/
|
|
14
|
+
function wrapPinoLogger(pinoLogger: PinoLogger): Logger {
|
|
15
|
+
/**
|
|
16
|
+
* Merges context with any request context from AsyncLocalStorage
|
|
17
|
+
*/
|
|
18
|
+
function mergeContext(context?: LogContext): LogContext | undefined {
|
|
19
|
+
const requestContext = getRequestContext();
|
|
20
|
+
if (!requestContext && !context) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
...requestContext,
|
|
25
|
+
...context,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const logger: Logger = {
|
|
30
|
+
trace(message: string, context?: LogContext): void {
|
|
31
|
+
const merged = mergeContext(context);
|
|
32
|
+
if (merged) {
|
|
33
|
+
pinoLogger.trace(merged, message);
|
|
34
|
+
} else {
|
|
35
|
+
pinoLogger.trace(message);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
debug(message: string, context?: LogContext): void {
|
|
40
|
+
const merged = mergeContext(context);
|
|
41
|
+
if (merged) {
|
|
42
|
+
pinoLogger.debug(merged, message);
|
|
43
|
+
} else {
|
|
44
|
+
pinoLogger.debug(message);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
info(message: string, context?: LogContext): void {
|
|
49
|
+
const merged = mergeContext(context);
|
|
50
|
+
if (merged) {
|
|
51
|
+
pinoLogger.info(merged, message);
|
|
52
|
+
} else {
|
|
53
|
+
pinoLogger.info(message);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
warn(message: string, context?: LogContext): void {
|
|
58
|
+
const merged = mergeContext(context);
|
|
59
|
+
if (merged) {
|
|
60
|
+
pinoLogger.warn(merged, message);
|
|
61
|
+
} else {
|
|
62
|
+
pinoLogger.warn(message);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
error(message: string, context?: LogContext): void {
|
|
67
|
+
const merged = mergeContext(context);
|
|
68
|
+
if (merged) {
|
|
69
|
+
pinoLogger.error(merged, message);
|
|
70
|
+
} else {
|
|
71
|
+
pinoLogger.error(message);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
fatal(message: string, context?: LogContext): void {
|
|
76
|
+
const merged = mergeContext(context);
|
|
77
|
+
if (merged) {
|
|
78
|
+
pinoLogger.fatal(merged, message);
|
|
79
|
+
} else {
|
|
80
|
+
pinoLogger.fatal(message);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
child(bindings: Record<string, unknown>): Logger {
|
|
85
|
+
return wrapPinoLogger(pinoLogger.child(bindings));
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
get pino(): PinoLogger {
|
|
89
|
+
return pinoLogger;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return logger;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a new logger instance with the given options
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const logger = createLogger({
|
|
102
|
+
* serviceName: 'my-service',
|
|
103
|
+
* level: 'debug',
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* logger.info('Server started', { port: 3000 });
|
|
107
|
+
*
|
|
108
|
+
* // Create a child logger with bound context
|
|
109
|
+
* const requestLogger = logger.child({ requestId: '123' });
|
|
110
|
+
* requestLogger.info('Processing request');
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function createLogger(options: CreateLoggerOptions): Logger {
|
|
114
|
+
const pinoLogger = createPinoInstance(options);
|
|
115
|
+
return wrapPinoLogger(pinoLogger);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Static Logger class for backward compatibility
|
|
120
|
+
* Provides a singleton logger that can be configured once
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* // Configure once at startup
|
|
125
|
+
* Logger.configure({ serviceName: 'my-service' });
|
|
126
|
+
*
|
|
127
|
+
* // Use anywhere
|
|
128
|
+
* Logger.info('message', { key: 'value' });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export class StaticLogger {
|
|
132
|
+
private static instance: Logger | null = null;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Configure the static logger
|
|
136
|
+
* Should be called once at application startup
|
|
137
|
+
*/
|
|
138
|
+
static configure(options: LoggerConfigureOptions): void {
|
|
139
|
+
StaticLogger.instance = createLogger({
|
|
140
|
+
serviceName: options.serviceName,
|
|
141
|
+
level: options.level,
|
|
142
|
+
pretty: options.pretty,
|
|
143
|
+
base: options.base,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get the configured logger instance
|
|
149
|
+
* Creates a default logger if not configured
|
|
150
|
+
*/
|
|
151
|
+
private static getLogger(): Logger {
|
|
152
|
+
if (!StaticLogger.instance) {
|
|
153
|
+
// Create a default logger with a warning
|
|
154
|
+
StaticLogger.instance = createLogger({
|
|
155
|
+
serviceName: 'unconfigured',
|
|
156
|
+
});
|
|
157
|
+
StaticLogger.instance.warn(
|
|
158
|
+
'Logger used before configure() was called. Call Logger.configure() at application startup.'
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return StaticLogger.instance;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static trace(message: string, context?: LogContext): void {
|
|
165
|
+
StaticLogger.getLogger().trace(message, context);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static debug(message: string, context?: LogContext): void {
|
|
169
|
+
StaticLogger.getLogger().debug(message, context);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static info(message: string, context?: LogContext): void {
|
|
173
|
+
StaticLogger.getLogger().info(message, context);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static warn(message: string, context?: LogContext): void {
|
|
177
|
+
StaticLogger.getLogger().warn(message, context);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static error(message: string, context?: LogContext): void {
|
|
181
|
+
StaticLogger.getLogger().error(message, context);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static fatal(message: string, context?: LogContext): void {
|
|
185
|
+
StaticLogger.getLogger().fatal(message, context);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create a child logger with bound context
|
|
190
|
+
*/
|
|
191
|
+
static child(bindings: Record<string, unknown>): Logger {
|
|
192
|
+
return StaticLogger.getLogger().child(bindings);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get the underlying Pino instance
|
|
197
|
+
*/
|
|
198
|
+
static get pino(): PinoLogger {
|
|
199
|
+
return StaticLogger.getLogger().pino;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Reset the static logger (useful for testing)
|
|
204
|
+
*/
|
|
205
|
+
static reset(): void {
|
|
206
|
+
StaticLogger.instance = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import type { CreateLoggerOptions } from '../types';
|
|
3
|
+
import { getSerializers } from './serializers';
|
|
4
|
+
|
|
5
|
+
/** Default paths to redact from logs */
|
|
6
|
+
const DEFAULT_REDACT_PATHS = [
|
|
7
|
+
'password',
|
|
8
|
+
'token',
|
|
9
|
+
'authorization',
|
|
10
|
+
'apiKey',
|
|
11
|
+
'api_key',
|
|
12
|
+
'secret',
|
|
13
|
+
'credential',
|
|
14
|
+
'*.password',
|
|
15
|
+
'*.token',
|
|
16
|
+
'*.authorization',
|
|
17
|
+
'*.apiKey',
|
|
18
|
+
'*.api_key',
|
|
19
|
+
'*.secret',
|
|
20
|
+
'*.credential',
|
|
21
|
+
'headers.authorization',
|
|
22
|
+
'headers.cookie',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gets the log level from options or environment
|
|
27
|
+
*/
|
|
28
|
+
function getLogLevel(options: CreateLoggerOptions): string {
|
|
29
|
+
return options.level ?? process.env['LOG_LEVEL'] ?? 'info';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates the transport configuration for pretty printing
|
|
34
|
+
*/
|
|
35
|
+
function createPrettyTransport(): pino.TransportSingleOptions {
|
|
36
|
+
return {
|
|
37
|
+
target: 'pino-pretty',
|
|
38
|
+
options: {
|
|
39
|
+
colorize: true,
|
|
40
|
+
translateTime: 'UTC:yyyy-mm-dd HH:MM:ss.l',
|
|
41
|
+
ignore: 'pid,hostname',
|
|
42
|
+
messageFormat: '{msg}',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a Pino logger configuration from options
|
|
49
|
+
*/
|
|
50
|
+
export function createPinoConfig(options: CreateLoggerOptions): pino.LoggerOptions {
|
|
51
|
+
const shouldPrettyPrint = options.pretty ?? false;
|
|
52
|
+
const logLevel = getLogLevel(options);
|
|
53
|
+
const redactPaths = [...DEFAULT_REDACT_PATHS, ...(options.redactPaths ?? [])];
|
|
54
|
+
|
|
55
|
+
const config: pino.LoggerOptions = {
|
|
56
|
+
level: logLevel,
|
|
57
|
+
base: {
|
|
58
|
+
service: options.serviceName,
|
|
59
|
+
version: options.version ?? process.env['npm_package_version'],
|
|
60
|
+
env: process.env['NODE_ENV'],
|
|
61
|
+
...options.base,
|
|
62
|
+
},
|
|
63
|
+
timestamp: options.timestamp !== false ? pino.stdTimeFunctions.isoTime : false,
|
|
64
|
+
formatters: {
|
|
65
|
+
level: (label) => ({ level: label }),
|
|
66
|
+
bindings: (bindings) => {
|
|
67
|
+
// In production, keep hostname and pid; in dev, they're ignored by pino-pretty
|
|
68
|
+
return bindings;
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
redact: {
|
|
72
|
+
paths: redactPaths,
|
|
73
|
+
censor: '[REDACTED]',
|
|
74
|
+
},
|
|
75
|
+
serializers: getSerializers(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Only add transport in development mode with pretty printing
|
|
79
|
+
if (shouldPrettyPrint) {
|
|
80
|
+
config.transport = createPrettyTransport();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return config;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a configured Pino logger instance
|
|
88
|
+
*/
|
|
89
|
+
export function createPinoInstance(options: CreateLoggerOptions): pino.Logger {
|
|
90
|
+
const config = createPinoConfig(options);
|
|
91
|
+
return pino(config);
|
|
92
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standard error serializer that handles Error objects
|
|
5
|
+
* and extracts useful information including cause chain
|
|
6
|
+
*/
|
|
7
|
+
export function errorSerializer(error: unknown): Record<string, unknown> | unknown {
|
|
8
|
+
if (error instanceof Error) {
|
|
9
|
+
return {
|
|
10
|
+
type: error.constructor.name,
|
|
11
|
+
message: error.message,
|
|
12
|
+
stack: error.stack,
|
|
13
|
+
...(error.cause !== undefined && { cause: errorSerializer(error.cause) }),
|
|
14
|
+
// Include any additional properties on the error
|
|
15
|
+
...Object.fromEntries(
|
|
16
|
+
Object.entries(error).filter(
|
|
17
|
+
([key]) => !['message', 'stack', 'cause'].includes(key)
|
|
18
|
+
)
|
|
19
|
+
),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Request serializer for HTTP requests
|
|
27
|
+
* Extracts commonly needed fields while excluding sensitive data
|
|
28
|
+
*/
|
|
29
|
+
export function requestSerializer(req: {
|
|
30
|
+
id?: string;
|
|
31
|
+
method?: string;
|
|
32
|
+
url?: string;
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
remoteAddress?: string;
|
|
35
|
+
}): Record<string, unknown> {
|
|
36
|
+
return {
|
|
37
|
+
id: req.id,
|
|
38
|
+
method: req.method,
|
|
39
|
+
url: req.url,
|
|
40
|
+
// Only include non-sensitive headers
|
|
41
|
+
headers: req.headers
|
|
42
|
+
? {
|
|
43
|
+
'user-agent': req.headers['user-agent'],
|
|
44
|
+
'content-type': req.headers['content-type'],
|
|
45
|
+
'content-length': req.headers['content-length'],
|
|
46
|
+
host: req.headers['host'],
|
|
47
|
+
}
|
|
48
|
+
: undefined,
|
|
49
|
+
remoteAddress: req.remoteAddress,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Response serializer for HTTP responses
|
|
55
|
+
*/
|
|
56
|
+
export function responseSerializer(res: {
|
|
57
|
+
statusCode?: number;
|
|
58
|
+
headers?: Record<string, string>;
|
|
59
|
+
}): Record<string, unknown> {
|
|
60
|
+
return {
|
|
61
|
+
statusCode: res.statusCode,
|
|
62
|
+
headers: res.headers
|
|
63
|
+
? {
|
|
64
|
+
'content-type': res.headers['content-type'],
|
|
65
|
+
'content-length': res.headers['content-length'],
|
|
66
|
+
}
|
|
67
|
+
: undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get all custom serializers
|
|
73
|
+
*/
|
|
74
|
+
export function getSerializers(): pino.LoggerOptions['serializers'] {
|
|
75
|
+
return {
|
|
76
|
+
err: pino.stdSerializers.err,
|
|
77
|
+
error: errorSerializer,
|
|
78
|
+
req: requestSerializer,
|
|
79
|
+
res: responseSerializer,
|
|
80
|
+
};
|
|
81
|
+
}
|