@geekmidas/logger 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,307 @@
1
+ import { Writable } from 'node:stream';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { DEFAULT_REDACT_PATHS } from '../pino';
4
+ import type { RedactOptions } from '../types';
5
+
6
+ /**
7
+ * Type for the resolved pino redact config.
8
+ */
9
+ type PinoRedactConfig =
10
+ | string[]
11
+ | {
12
+ paths: string[];
13
+ censor?: string | ((value: unknown, path: string[]) => unknown);
14
+ remove?: boolean;
15
+ };
16
+
17
+ /**
18
+ * Resolves redact options to pino-compatible config, applying merge logic.
19
+ */
20
+ function resolveRedactConfig(
21
+ redact: RedactOptions | undefined,
22
+ ): PinoRedactConfig | undefined {
23
+ if (redact === undefined) {
24
+ return undefined;
25
+ }
26
+
27
+ // Array syntax - merge with defaults
28
+ if (Array.isArray(redact)) {
29
+ return [...DEFAULT_REDACT_PATHS, ...redact];
30
+ }
31
+
32
+ // Object syntax - check resolution mode
33
+ const { resolution = 'merge', paths, censor, remove } = redact;
34
+
35
+ const resolvedPaths =
36
+ resolution === 'override' ? paths : [...DEFAULT_REDACT_PATHS, ...paths];
37
+
38
+ const config: PinoRedactConfig = { paths: resolvedPaths };
39
+ if (censor !== undefined) config.censor = censor;
40
+ if (remove !== undefined) config.remove = remove;
41
+
42
+ return config;
43
+ }
44
+
45
+ /**
46
+ * Creates a writable stream that captures pino output as parsed JSON objects.
47
+ */
48
+ function createCaptureStream() {
49
+ const logs: Record<string, unknown>[] = [];
50
+
51
+ const stream = new Writable({
52
+ write(chunk, _encoding, callback) {
53
+ try {
54
+ const line = chunk.toString().trim();
55
+ if (line) {
56
+ logs.push(JSON.parse(line));
57
+ }
58
+ } catch {
59
+ // Ignore non-JSON lines (e.g., pretty output)
60
+ }
61
+ callback();
62
+ },
63
+ });
64
+
65
+ return { stream, logs };
66
+ }
67
+
68
+ /**
69
+ * Creates a logger that writes to a capture stream for testing.
70
+ * Note: We can't use pretty mode here as it's not JSON parseable.
71
+ */
72
+ function createTestLogger(redact: RedactOptions | undefined) {
73
+ const { stream, logs } = createCaptureStream();
74
+
75
+ // Import pino directly to create with custom destination
76
+ const { pino } = require('pino');
77
+
78
+ // Apply our merge logic before passing to pino
79
+ const resolvedRedact = resolveRedactConfig(redact);
80
+
81
+ const logger = pino(
82
+ {
83
+ redact: resolvedRedact,
84
+ // Disable pretty for JSON parsing
85
+ formatters: {
86
+ level: (label: string) => ({ level: label }),
87
+ },
88
+ },
89
+ stream,
90
+ );
91
+
92
+ return { logger, logs };
93
+ }
94
+
95
+ describe('Pino Redaction Integration', () => {
96
+ describe('with redact: true (default paths)', () => {
97
+ it('should redact password field', () => {
98
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
99
+
100
+ logger.info({ password: 'secret123', username: 'john' }, 'Login attempt');
101
+ logger.flush?.();
102
+
103
+ expect(logs).toHaveLength(1);
104
+ expect(logs[0].password).toBe('[Redacted]');
105
+ expect(logs[0].username).toBe('john');
106
+ });
107
+
108
+ it('should redact token field', () => {
109
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
110
+
111
+ logger.info({ token: 'jwt.token.here', userId: 123 }, 'Auth check');
112
+ logger.flush?.();
113
+
114
+ expect(logs).toHaveLength(1);
115
+ expect(logs[0].token).toBe('[Redacted]');
116
+ expect(logs[0].userId).toBe(123);
117
+ });
118
+
119
+ it('should redact apiKey field', () => {
120
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
121
+
122
+ logger.info({ apiKey: 'sk-1234567890', service: 'openai' }, 'API call');
123
+ logger.flush?.();
124
+
125
+ expect(logs).toHaveLength(1);
126
+ expect(logs[0].apiKey).toBe('[Redacted]');
127
+ expect(logs[0].service).toBe('openai');
128
+ });
129
+
130
+ it('should redact nested sensitive fields with wildcards', () => {
131
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
132
+
133
+ logger.info(
134
+ {
135
+ user: { password: 'secret', name: 'John' },
136
+ config: { secret: 'shh', debug: true },
137
+ },
138
+ 'Nested data',
139
+ );
140
+ logger.flush?.();
141
+
142
+ expect(logs).toHaveLength(1);
143
+ expect(logs[0].user).toEqual({ password: '[Redacted]', name: 'John' });
144
+ expect(logs[0].config).toEqual({ secret: '[Redacted]', debug: true });
145
+ });
146
+
147
+ it('should redact authorization headers', () => {
148
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
149
+
150
+ logger.info(
151
+ {
152
+ headers: {
153
+ authorization: 'Bearer xyz123',
154
+ 'content-type': 'application/json',
155
+ },
156
+ },
157
+ 'Request headers',
158
+ );
159
+ logger.flush?.();
160
+
161
+ expect(logs).toHaveLength(1);
162
+ expect(logs[0].headers).toEqual({
163
+ authorization: '[Redacted]',
164
+ 'content-type': 'application/json',
165
+ });
166
+ });
167
+
168
+ it('should redact credit card fields', () => {
169
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
170
+
171
+ logger.info(
172
+ {
173
+ creditCard: '4111-1111-1111-1111',
174
+ cvv: '123',
175
+ cardHolder: 'John Doe',
176
+ },
177
+ 'Payment info',
178
+ );
179
+ logger.flush?.();
180
+
181
+ expect(logs).toHaveLength(1);
182
+ expect(logs[0].creditCard).toBe('[Redacted]');
183
+ expect(logs[0].cvv).toBe('[Redacted]');
184
+ expect(logs[0].cardHolder).toBe('John Doe');
185
+ });
186
+ });
187
+
188
+ describe('with custom paths (merge mode - default)', () => {
189
+ it('should merge custom paths with defaults', () => {
190
+ const { logger, logs } = createTestLogger(['customSecret', 'data.key']);
191
+
192
+ logger.info(
193
+ {
194
+ customSecret: 'hidden',
195
+ password: 'also-hidden-from-defaults',
196
+ data: { key: 'hidden', value: 'visible' },
197
+ },
198
+ 'Merged redaction',
199
+ );
200
+ logger.flush?.();
201
+
202
+ expect(logs).toHaveLength(1);
203
+ expect(logs[0].customSecret).toBe('[Redacted]');
204
+ // password is redacted because it's in DEFAULT_REDACT_PATHS
205
+ expect(logs[0].password).toBe('[Redacted]');
206
+ expect(logs[0].data).toEqual({ key: '[Redacted]', value: 'visible' });
207
+ });
208
+
209
+ it('should support wildcard paths merged with defaults', () => {
210
+ const { logger, logs } = createTestLogger(['items[*].customField']);
211
+
212
+ logger.info(
213
+ {
214
+ password: 'hidden-by-default',
215
+ items: [
216
+ { id: 1, customField: 'a' },
217
+ { id: 2, customField: 'b' },
218
+ ],
219
+ },
220
+ 'Array redaction',
221
+ );
222
+ logger.flush?.();
223
+
224
+ expect(logs).toHaveLength(1);
225
+ expect(logs[0].password).toBe('[Redacted]');
226
+ expect(logs[0].items).toEqual([
227
+ { id: 1, customField: '[Redacted]' },
228
+ { id: 2, customField: '[Redacted]' },
229
+ ]);
230
+ });
231
+ });
232
+
233
+ describe('with resolution: override', () => {
234
+ it('should redact only specified paths when override', () => {
235
+ const { logger, logs } = createTestLogger({
236
+ paths: ['customSecret', 'data.key'],
237
+ resolution: 'override',
238
+ });
239
+
240
+ logger.info(
241
+ {
242
+ customSecret: 'hidden',
243
+ password: 'visible-because-override',
244
+ data: { key: 'hidden', value: 'visible' },
245
+ },
246
+ 'Override redaction',
247
+ );
248
+ logger.flush?.();
249
+
250
+ expect(logs).toHaveLength(1);
251
+ expect(logs[0].customSecret).toBe('[Redacted]');
252
+ // password is NOT redacted because we're overriding defaults
253
+ expect(logs[0].password).toBe('visible-because-override');
254
+ expect(logs[0].data).toEqual({ key: '[Redacted]', value: 'visible' });
255
+ });
256
+ });
257
+
258
+ describe('with object config', () => {
259
+ it('should use custom censor string', () => {
260
+ const { logger, logs } = createTestLogger({
261
+ paths: ['password'],
262
+ censor: '***HIDDEN***',
263
+ });
264
+
265
+ logger.info({ password: 'secret', user: 'john' }, 'Custom censor');
266
+ logger.flush?.();
267
+
268
+ expect(logs).toHaveLength(1);
269
+ expect(logs[0].password).toBe('***HIDDEN***');
270
+ expect(logs[0].user).toBe('john');
271
+ });
272
+
273
+ it('should remove field when remove: true', () => {
274
+ const { logger, logs } = createTestLogger({
275
+ paths: ['password', 'secret'],
276
+ remove: true,
277
+ });
278
+
279
+ logger.info(
280
+ { password: 'secret', secret: 'shh', username: 'john' },
281
+ 'Remove mode',
282
+ );
283
+ logger.flush?.();
284
+
285
+ expect(logs).toHaveLength(1);
286
+ expect(logs[0]).not.toHaveProperty('password');
287
+ expect(logs[0]).not.toHaveProperty('secret');
288
+ expect(logs[0].username).toBe('john');
289
+ });
290
+ });
291
+
292
+ describe('without redaction', () => {
293
+ it('should not redact when redact is undefined', () => {
294
+ const { logger, logs } = createTestLogger(undefined);
295
+
296
+ logger.info(
297
+ { password: 'visible', token: 'also-visible' },
298
+ 'No redaction',
299
+ );
300
+ logger.flush?.();
301
+
302
+ expect(logs).toHaveLength(1);
303
+ expect(logs[0].password).toBe('visible');
304
+ expect(logs[0].token).toBe('also-visible');
305
+ });
306
+ });
307
+ });
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { DEFAULT_REDACT_PATHS } from '../pino';
2
3
  import { LogLevel } from '../types';
3
4
 
4
5
  // Mock pino module
@@ -153,4 +154,202 @@ describe('Pino Logger', () => {
153
154
  expect(pinoMock).toHaveBeenCalled();
154
155
  });
155
156
  });
157
+
158
+ describe('Redaction', () => {
159
+ it('should not configure redaction when redact is undefined', async () => {
160
+ const { createLogger } = await import('../pino');
161
+
162
+ createLogger({});
163
+
164
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
165
+ expect(callArgs.redact).toBeUndefined();
166
+ });
167
+
168
+ it('should not configure redaction when redact is false', async () => {
169
+ const { createLogger } = await import('../pino');
170
+
171
+ createLogger({ redact: false });
172
+
173
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
174
+ expect(callArgs.redact).toBeUndefined();
175
+ });
176
+
177
+ it('should use default redact paths when redact is true', async () => {
178
+ const { createLogger } = await import('../pino');
179
+
180
+ createLogger({ redact: true });
181
+
182
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
183
+ expect(callArgs.redact).toEqual(DEFAULT_REDACT_PATHS);
184
+ });
185
+
186
+ it('should merge custom paths with defaults when redact is an array', async () => {
187
+ const { createLogger } = await import('../pino');
188
+ const customPaths = ['custom.field', 'user.ssn'];
189
+
190
+ createLogger({ redact: customPaths });
191
+
192
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
193
+ // Should include both defaults and custom paths
194
+ expect(callArgs.redact).toEqual([
195
+ ...DEFAULT_REDACT_PATHS,
196
+ ...customPaths,
197
+ ]);
198
+ });
199
+
200
+ it('should merge object config paths with defaults by default', async () => {
201
+ const { createLogger } = await import('../pino');
202
+ const customPaths = ['custom.field'];
203
+
204
+ createLogger({
205
+ redact: {
206
+ paths: customPaths,
207
+ censor: '***HIDDEN***',
208
+ },
209
+ });
210
+
211
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
212
+ expect(callArgs.redact.paths).toEqual([
213
+ ...DEFAULT_REDACT_PATHS,
214
+ ...customPaths,
215
+ ]);
216
+ expect(callArgs.redact.censor).toBe('***HIDDEN***');
217
+ });
218
+
219
+ it('should override defaults when resolution is override', async () => {
220
+ const { createLogger } = await import('../pino');
221
+ const customPaths = ['only.this.path'];
222
+
223
+ createLogger({
224
+ redact: {
225
+ paths: customPaths,
226
+ resolution: 'override',
227
+ },
228
+ });
229
+
230
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
231
+ expect(callArgs.redact.paths).toEqual(customPaths);
232
+ // Should not include defaults
233
+ expect(callArgs.redact.paths).not.toContain('password');
234
+ });
235
+
236
+ it('should merge with defaults when resolution is merge', async () => {
237
+ const { createLogger } = await import('../pino');
238
+ const customPaths = ['extra.secret'];
239
+
240
+ createLogger({
241
+ redact: {
242
+ paths: customPaths,
243
+ resolution: 'merge',
244
+ },
245
+ });
246
+
247
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
248
+ expect(callArgs.redact.paths).toEqual([
249
+ ...DEFAULT_REDACT_PATHS,
250
+ ...customPaths,
251
+ ]);
252
+ });
253
+
254
+ it('should support remove option in redact config', async () => {
255
+ const { createLogger } = await import('../pino');
256
+
257
+ createLogger({
258
+ redact: {
259
+ paths: ['temp.data'],
260
+ remove: true,
261
+ },
262
+ });
263
+
264
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
265
+ expect(callArgs.redact.remove).toBe(true);
266
+ });
267
+
268
+ it('should support censor function in redact config', async () => {
269
+ const { createLogger } = await import('../pino');
270
+ const censorFn = () => '***';
271
+
272
+ createLogger({
273
+ redact: {
274
+ paths: ['secret'],
275
+ censor: censorFn,
276
+ },
277
+ });
278
+
279
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
280
+ expect(callArgs.redact.censor).toBe(censorFn);
281
+ });
282
+
283
+ it('should not include resolution field in pino config', async () => {
284
+ const { createLogger } = await import('../pino');
285
+
286
+ createLogger({
287
+ redact: {
288
+ paths: ['secret'],
289
+ resolution: 'override',
290
+ },
291
+ });
292
+
293
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
294
+ expect(callArgs.redact).not.toHaveProperty('resolution');
295
+ });
296
+
297
+ it('should combine redact with other options', async () => {
298
+ const { createLogger } = await import('../pino');
299
+
300
+ createLogger({
301
+ level: LogLevel.Debug,
302
+ redact: true,
303
+ });
304
+
305
+ const callArgs = pinoMock.mock.calls[pinoMock.mock.calls.length - 1][0];
306
+ expect(callArgs.level).toBe(LogLevel.Debug);
307
+ expect(callArgs.redact).toEqual(DEFAULT_REDACT_PATHS);
308
+ });
309
+ });
310
+
311
+ describe('DEFAULT_REDACT_PATHS', () => {
312
+ it('should include common password fields', () => {
313
+ expect(DEFAULT_REDACT_PATHS).toContain('password');
314
+ expect(DEFAULT_REDACT_PATHS).toContain('pass');
315
+ expect(DEFAULT_REDACT_PATHS).toContain('passwd');
316
+ });
317
+
318
+ it('should include token fields', () => {
319
+ expect(DEFAULT_REDACT_PATHS).toContain('token');
320
+ expect(DEFAULT_REDACT_PATHS).toContain('accessToken');
321
+ expect(DEFAULT_REDACT_PATHS).toContain('refreshToken');
322
+ expect(DEFAULT_REDACT_PATHS).toContain('idToken');
323
+ });
324
+
325
+ it('should include API key variations', () => {
326
+ expect(DEFAULT_REDACT_PATHS).toContain('apiKey');
327
+ expect(DEFAULT_REDACT_PATHS).toContain('api_key');
328
+ expect(DEFAULT_REDACT_PATHS).toContain('apikey');
329
+ });
330
+
331
+ it('should include authorization headers', () => {
332
+ expect(DEFAULT_REDACT_PATHS).toContain('headers.authorization');
333
+ expect(DEFAULT_REDACT_PATHS).toContain('headers.Authorization');
334
+ expect(DEFAULT_REDACT_PATHS).toContain('headers.cookie');
335
+ });
336
+
337
+ it('should include wildcard patterns for nested fields', () => {
338
+ expect(DEFAULT_REDACT_PATHS).toContain('*.password');
339
+ expect(DEFAULT_REDACT_PATHS).toContain('*.secret');
340
+ expect(DEFAULT_REDACT_PATHS).toContain('*.token');
341
+ });
342
+
343
+ it('should include sensitive personal data fields', () => {
344
+ expect(DEFAULT_REDACT_PATHS).toContain('ssn');
345
+ expect(DEFAULT_REDACT_PATHS).toContain('creditCard');
346
+ expect(DEFAULT_REDACT_PATHS).toContain('cardNumber');
347
+ expect(DEFAULT_REDACT_PATHS).toContain('cvv');
348
+ });
349
+
350
+ it('should include database connection strings', () => {
351
+ expect(DEFAULT_REDACT_PATHS).toContain('connectionString');
352
+ expect(DEFAULT_REDACT_PATHS).toContain('databaseUrl');
353
+ });
354
+ });
156
355
  });
package/src/console.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { LogFn, Logger } from './types';
1
+ import type { CreateLoggerOptions, LogFn, Logger } from './types';
2
2
 
3
3
  export class ConsoleLogger implements Logger {
4
4
  /**
@@ -16,11 +16,22 @@ export class ConsoleLogger implements Logger {
16
16
  * @private
17
17
  */
18
18
  private createLogFn(logMethod: (...args: any[]) => void): LogFn {
19
- return <T extends object>(obj: T, msg?: string, ...args: any[]): void => {
20
- // Merge the logger's context data with the provided object
19
+ return <T extends object>(
20
+ objOrMsg: T | string,
21
+ msg?: string,
22
+ ...args: any[]
23
+ ): void => {
21
24
  const ts = Date.now();
22
- const mergedData = { ...this.data, ...obj, ts };
23
25
 
26
+ // Handle simple string logging: logger.info('message')
27
+ if (typeof objOrMsg === 'string') {
28
+ const mergedData = { ...this.data, ts };
29
+ logMethod(mergedData, objOrMsg, ...args);
30
+ return;
31
+ }
32
+
33
+ // Handle structured logging: logger.info({ data }, 'message')
34
+ const mergedData = { ...this.data, ...objOrMsg, ts };
24
35
  if (msg) {
25
36
  logMethod(mergedData, msg, ...args);
26
37
  } else {
@@ -91,3 +102,21 @@ export class ConsoleLogger implements Logger {
91
102
  */
92
103
 
93
104
  export const DEFAULT_LOGGER = new ConsoleLogger() as any;
105
+
106
+ /**
107
+ * Creates a console logger with the same API as pino's createLogger.
108
+ *
109
+ * @param options - Logger configuration options
110
+ * @returns A ConsoleLogger instance
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * import { createLogger } from '@geekmidas/logger/console';
115
+ *
116
+ * const logger = createLogger({ level: LogLevel.Debug });
117
+ * logger.info({ action: 'start' }, 'Application starting');
118
+ * ```
119
+ */
120
+ export function createLogger(options: CreateLoggerOptions = {}): Logger {
121
+ return new ConsoleLogger();
122
+ }
package/src/index.ts CHANGED
@@ -2,5 +2,6 @@ export {
2
2
  type CreateLoggerOptions,
3
3
  type LogFn,
4
4
  type Logger,
5
+ type RedactOptions,
5
6
  LogLevel,
6
7
  } from './types';