@fend/firo 0.0.5 → 0.0.7

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
@@ -7,11 +7,11 @@
7
7
  [![Build](https://github.com/fend25/firo/actions/workflows/publish.yml/badge.svg)](https://github.com/fend25/firo/actions/workflows/publish.yml)
8
8
  [![Best logger ever](https://img.shields.io/badge/best_logger-ever-166FFF)](https://github.com/fend25/firo)
9
9
 
10
- **Spruce up your logs!**
10
+ **Spruce up your logs!**
11
11
 
12
12
  The logger for Node.js, Bun and Deno you've been looking for.
13
13
 
14
- Beautiful **dev** output - out of the box. Structured NDJSON for **prod**.
14
+ Beautiful **dev** output - out of the box. Fast, structured NDJSON for **prod**.
15
15
 
16
16
  Think of it as pino, but with brilliant DX.
17
17
 
@@ -93,36 +93,6 @@ log.info('Request handled', { status: 200 })
93
93
  // {"timestamp":"2024-01-15T14:32:01.204Z","level":"info","message":"Request handled","data":{"status":200}}
94
94
  ```
95
95
 
96
- ## Best practices
97
-
98
- ### AsyncLocalStorage (Traceability)
99
-
100
- The best way to use **firo** in web frameworks is to store a child logger in `AsyncLocalStorage`. This gives you automatic traceability (e.g. `requestId`) across your entire call stack without passing the logger as an argument.
101
-
102
- ```ts
103
- import { AsyncLocalStorage } from 'node:util'
104
- import { createFiro } from '@fend/firo'
105
-
106
- const logger = createFiro()
107
- const storage = new AsyncLocalStorage()
108
-
109
- // Middleware example
110
- function middleware(req, res, next) {
111
- const reqLog = logger.child({
112
- requestId: req.headers['x-request-id'] || 'gen-123',
113
- method: req.method
114
- })
115
- storage.run(reqLog, next)
116
- }
117
-
118
- // Deeply nested function
119
- function someService() {
120
- const log = storage.getStore() ?? logger
121
- log.info('Service action performed')
122
- // Output: [requestId:gen-123] [method:GET] Service action performed
123
- }
124
- ```
125
-
126
96
  ## Log levels
127
97
 
128
98
  Four levels, in order: `debug` → `info` → `warn` → `error`.
@@ -139,16 +109,31 @@ Debug lines are dimmed in dev mode to reduce visual noise.
139
109
  ### Filtering
140
110
 
141
111
  ```ts
142
- // Suppress debug in dev, keep everything in prod
143
- const log = createFiro({
144
- minLevelInDev: 'info',
145
- minLevelInProd: 'warn',
146
- })
147
-
148
- // Or a single threshold for both modes
149
112
  const log = createFiro({ minLevel: 'warn' })
150
113
  ```
151
114
 
115
+ ## Error signatures
116
+
117
+ `error()` accepts multiple call signatures:
118
+
119
+ ```ts
120
+ // Message only will be automatically wrapped in an Error object to intentionally capture and preserve the stack trace
121
+ // because stack trace with a couple of extra levels of indirection is definitely better than no stack trace at all
122
+ log.error('Something went wrong')
123
+
124
+ // Message + Error object
125
+ log.error('Query failed', new Error('timeout'))
126
+
127
+ // Error object only
128
+ log.error(new Error('Unhandled'))
129
+
130
+ // Error + extra data
131
+ log.error(new Error('DB down'), { query: 'SELECT ...', reqId: 123 })
132
+
133
+ // Anything — will be coerced to Error
134
+ log.error(someUnknownThing)
135
+ ```
136
+
152
137
  ## Context
153
138
 
154
139
  Attach persistent key/value pairs to a logger instance. They appear in every log line.
@@ -166,29 +151,47 @@ log.info('Started')
166
151
 
167
152
  ### Context options
168
153
 
154
+ Three ways to add context:
155
+
156
+ ```ts
157
+ // 1. Simple key-value — just the basics
158
+ log.addContext('service', 'auth')
159
+
160
+ // 2. Key + value with options — when you need control
161
+ log.addContext('traceId', { value: 'abc-123-xyz', hideIn: 'dev' })
162
+ log.addContext('region', { value: 'west', color: '38;5;214' })
163
+
164
+ // 3. Object form — everything in one object
165
+ log.addContext({ key: 'userId', value: 'u-789', omitKey: true })
166
+ log.addContext({ key: 'span', value: 'xyz', color: '38;2;255;100;0' })
167
+ ```
168
+
169
+ Available options (styles 2 and 3):
170
+
169
171
  ```ts
170
- // Hide the key, show only the value useful for IDs
172
+ // Hide the key, show only the value: [u-789] instead of [userId:u-789]
171
173
  log.addContext({ key: 'userId', value: 'u-789', omitKey: true })
172
- // renders as [u-789] instead of [userId:u-789]
173
174
 
174
175
  // Pin a specific color by palette index (0–29)
175
- log.addContext({ key: 'region', value: 'west', colorIndex: 3 })
176
+ log.addContext('region', { value: 'west', colorIndex: 3 })
176
177
 
177
178
  // Use any ANSI color — 256-color, truecolor, anything
178
- log.addContext({ key: 'trace', value: 'abc', color: '38;5;214' }) // 256-color orange
179
+ log.addContext('trace', { value: 'abc', color: '38;5;214' }) // 256-color orange
179
180
  log.addContext({ key: 'span', value: 'xyz', color: '38;2;255;100;0' }) // truecolor
180
- ```
181
181
 
182
- ### Remove context
182
+ // Hide in dev — useful for traceIds that clutter the terminal
183
+ log.addContext('traceId', { value: 'abc-123-xyz', hideIn: 'dev' })
183
184
 
184
- ```ts
185
- log.removeFromContext('env')
185
+ // Hide in prod — dev-only debugging context
186
+ log.addContext('debugTag', { value: 'perf-test', hideIn: 'prod' })
186
187
  ```
187
188
 
188
- ### Read context
189
+ ### Context API
189
190
 
190
191
  ```ts
191
- const ctx = log.getContext() // ContextItem[]
192
+ log.getContext() // ContextItem[]
193
+ log.hasInContext('key') // boolean
194
+ log.removeFromContext('env')
192
195
  ```
193
196
 
194
197
  ## Child loggers
@@ -234,85 +237,75 @@ log.error('Payment failed', err, {
234
237
  })
235
238
  ```
236
239
 
237
- ## Error signatures
240
+ ## Dev formatter options
238
241
 
239
- `error()` accepts multiple call signatures:
242
+ Fine-tune the dev formatter's timestamp format. For example, to remove seconds and milliseconds:
240
243
 
241
244
  ```ts
242
- // Message only
243
- log.error('Something went wrong')
245
+ import { createFiro } from '@fend/firo'
244
246
 
245
- // Message + Error object
246
- log.error('Query failed', new Error('timeout'))
247
+ const log = createFiro({
248
+ devFormatterConfig: {
249
+ timeOptions: {
250
+ hour: '2-digit',
251
+ minute: '2-digit',
252
+ second: undefined,
253
+ fractionalSecondDigits: undefined
254
+ }
255
+ }
256
+ })
257
+ ```
247
258
 
248
- // Error object only
249
- log.error(new Error('Unhandled'))
259
+ ## Color palette
250
260
 
251
- // Anythingwill be coerced to Error
252
- log.error(someUnknownThing)
253
- ```
261
+ Most loggers give you monochrome walls of text. Firo gives you **30 handpicked colors** that make context badges instantly scannable you stop reading and start seeing.
254
262
 
255
- ## Custom transport
263
+ ![firo color palette](https://github.com/fend25/firo/blob/main/img/color_madness.png?raw=true)
264
+
265
+ ### How it works
266
+
267
+ By default, firo auto-assigns colors from all 30 palette colors using a hash of the context key. Similar keys like `user-1` and `user-2` land on different colors automatically.
256
268
 
257
- Provide your own transport function to take full control of output:
269
+ You can also pin a specific color using `FIRO_COLORS` — a named palette with full IDE autocomplete:
258
270
 
259
271
  ```ts
260
- import type { TransportFn } from '@fend/firo'
272
+ import { createFiro, FIRO_COLORS } from '@fend/firo'
261
273
 
262
- const myTransport: TransportFn = (level, context, msg, data, opts) => {
263
- // level: 'debug' | 'info' | 'warn' | 'error'
264
- // context: ContextItemWithOptions[]
265
- // msg: string | Error | unknown
266
- // data: Error | unknown
267
- // opts: LogOptions | undefined
268
- }
274
+ const log = createFiro()
269
275
 
270
- const log = createFiro({ transport: myTransport })
276
+ log.addContext('region', { value: 'west', color: FIRO_COLORS.coral })
277
+ log.addContext('service', { value: 'auth', color: FIRO_COLORS.skyBlue })
278
+ log.addContext('env', { value: 'staging', color: FIRO_COLORS.lavender })
271
279
  ```
272
280
 
273
- ### FiroUtils
281
+ Available colors: `cyan`, `green`, `yellow`, `magenta`, `blue`, `brightCyan`, `brightGreen`, `brightYellow`, `brightMagenta`, `brightBlue`, `orange`, `pink`, `lilac`, `skyBlue`, `mint`, `salmon`, `lemon`, `lavender`, `sage`, `coral`, `teal`, `rose`, `pistachio`, `mauve`, `aqua`, `gold`, `thistle`, `seafoam`, `tangerine`, `periwinkle`.
274
282
 
275
- `FiroUtils` exposes helper functions useful for building custom transports:
283
+ ### Want even more variety?
276
284
 
277
- ```ts
278
- import { FiroUtils } from '@fend/firo'
285
+ You can also pass any raw ANSI code as a string — 256-color, truecolor, go wild:
279
286
 
280
- FiroUtils.wrapToError(value) // coerce unknown → Error
281
- FiroUtils.serializeError(err) // Error → plain object { message, stack, name }
282
- FiroUtils.safeStringify(obj) // JSON.stringify with bigint support + fallback
283
- FiroUtils.jsonReplacer // replacer for JSON.stringify (handles bigint)
284
- FiroUtils.colorize(text, idx) // wrap text in ANSI color by palette index
285
- FiroUtils.colorizeLevel(level, t) // wrap text in level color (red/yellow/dim)
287
+ ```ts
288
+ log.addContext('trace', { value: 'abc', color: '38;5;214' }) // 256-color
289
+ log.addContext('span', { value: 'xyz', color: '38;2;255;105;180' }) // truecolor pink
286
290
  ```
287
291
 
288
- ## Dev transport options
292
+ ### Restrict to safe colors
289
293
 
290
- Fine-tune the dev transport's timestamp format. For example, to remove seconds and milliseconds:
294
+ If your terminal doesn't support 256 colors, you can restrict auto-hash to 10 basic terminal-safe colors:
291
295
 
292
296
  ```ts
293
- import { createFiro } from '@fend/firo'
294
-
295
- const log = createFiro({
296
- devTransportConfig: {
297
- timeOptions: {
298
- hour: '2-digit',
299
- minute: '2-digit',
300
- second: undefined,
301
- fractionalSecondDigits: undefined
302
- }
303
- }
304
- })
297
+ const log = createFiro({ useSafeColors: true })
305
298
  ```
306
299
 
307
- ## Prod transport options
300
+ ## Prod formatter options
308
301
 
309
- Configure the prod (JSON) transport's timestamp format:
302
+ Configure the prod (JSON) formatter's timestamp format:
310
303
 
311
304
  ```ts
312
305
  // Epoch ms (faster, same as pino)
313
306
  const log = createFiro({
314
307
  mode: 'prod',
315
- prodTransportConfig: { timestamp: 'epoch' }
308
+ prodFormatterConfig: { timestamp: 'epoch' }
316
309
  })
317
310
  // {"timestamp":1711100000000,"level":"info","message":"hello"}
318
311
 
@@ -323,7 +316,7 @@ const log = createFiro({ mode: 'prod' })
323
316
 
324
317
  ### Custom destination
325
318
 
326
- By default, prod transport writes to `process.stdout`. You can redirect output to any object with a `.write(string)` method:
319
+ By default, prod formatter writes to `process.stdout`. You can redirect output to any object with a `.write(string)` method:
327
320
 
328
321
  ```ts
329
322
  import { createFiro } from '@fend/firo'
@@ -332,63 +325,89 @@ import { createWriteStream } from 'node:fs'
332
325
  // Write to a file
333
326
  const log = createFiro({
334
327
  mode: 'prod',
335
- prodTransportConfig: { dest: createWriteStream('/var/log/app.log') }
328
+ prodFormatterConfig: { dest: createWriteStream('/var/log/app.log') }
336
329
  })
337
330
 
338
331
  // Use SonicBoom for async buffered writes (same as pino)
339
332
  import SonicBoom from 'sonic-boom'
340
333
  const log = createFiro({
341
334
  mode: 'prod',
342
- prodTransportConfig: { dest: new SonicBoom({ fd: 1 }) }
335
+ prodFormatterConfig: { dest: new SonicBoom({ fd: 1 }) }
343
336
  })
344
337
  ```
345
338
 
346
- ## Color palette
339
+ ## Custom formatter
347
340
 
348
- Most loggers give you monochrome walls of text. firo gives you **30 handpicked colors** that make context badges instantly scannable you stop reading and start seeing.
341
+ If for some reason all the options are not enough and you need to take full control of the output, you can provide your own formatter function.
349
342
 
350
- ![firo color palette](https://github.com/fend25/firo/blob/main/img/color_madness.png?raw=true)
343
+ ```ts
344
+ import type { FormatterFn } from '@fend/firo'
351
345
 
352
- ### How it works
346
+ const myFormatter: FormatterFn = (level, context, msg, data, opts) => {
347
+ // level: 'debug' | 'info' | 'warn' | 'error'
348
+ // context: ContextItemWithOptions[]
349
+ // msg: string | Error | unknown
350
+ // data: Error | unknown
351
+ // opts: LogOptions | undefined
352
+ }
353
353
 
354
- By default, firo auto-assigns colors from all 30 palette colors using a hash of the context key. Similar keys like `user-1` and `user-2` land on different colors automatically.
354
+ const log = createFiro({ formatter: myFormatter })
355
+ ```
355
356
 
356
- You can also pin a specific color using `FIRO_COLORS` a named palette with full IDE autocomplete:
357
+ You don't have to start from scratch all the helpers we use internally are yours too:
357
358
 
358
- ```ts
359
- import { createFiro, FIRO_COLORS } from '@fend/firo'
359
+ #### FiroUtils
360
360
 
361
- const log = createFiro()
361
+ `FiroUtils` exposes helper functions useful for building custom formatters:
362
362
 
363
- log.addContext('region', { value: 'west', color: FIRO_COLORS.coral })
364
- log.addContext('service', { value: 'auth', color: FIRO_COLORS.skyBlue })
365
- log.addContext('env', { value: 'staging', color: FIRO_COLORS.lavender })
363
+ ```ts
364
+ import { FiroUtils } from '@fend/firo'
365
+
366
+ FiroUtils.wrapToError(value) // coerce unknown → Error
367
+ FiroUtils.serializeError(err) // Error → plain object { message, stack, name, cause?, ... }
368
+ FiroUtils.safeStringify(obj) // JSON.stringify with bigint support + fallback
369
+ FiroUtils.jsonReplacer // replacer for JSON.stringify (handles bigint)
370
+ FiroUtils.extractMessage(msg) // extract message string from string | Error | unknown
371
+ FiroUtils.colorize(text, idx, c?) // wrap text in ANSI color by palette index or raw code
372
+ FiroUtils.colorizeLevel(level, t) // wrap text in level color (red/yellow/dim)
366
373
  ```
367
374
 
368
- Available colors: `cyan`, `green`, `yellow`, `magenta`, `blue`, `brightCyan`, `brightGreen`, `brightYellow`, `brightMagenta`, `brightBlue`, `orange`, `pink`, `lilac`, `skyBlue`, `mint`, `salmon`, `lemon`, `lavender`, `sage`, `coral`, `teal`, `rose`, `pistachio`, `mauve`, `aqua`, `gold`, `thistle`, `seafoam`, `tangerine`, `periwinkle`.
375
+ ## Best practices
369
376
 
370
- ### Want even more variety?
377
+ ### AsyncLocalStorage (Traceability)
371
378
 
372
- You can also pass any raw ANSI code as a string 256-color, truecolor, go wild:
379
+ The best way to use **firo** in web frameworks is to store a child logger in `AsyncLocalStorage`. This gives you automatic traceability (e.g. `requestId`) across your entire call stack without passing the logger as an argument.
373
380
 
374
381
  ```ts
375
- log.addContext('trace', { value: 'abc', color: '38;5;214' }) // 256-color
376
- log.addContext('span', { value: 'xyz', color: '38;2;255;105;180' }) // truecolor pink
377
- ```
382
+ import { AsyncLocalStorage } from 'node:util'
383
+ import { createFiro } from '@fend/firo'
378
384
 
379
- ### Restrict to safe colors
385
+ const logger = createFiro()
386
+ const storage = new AsyncLocalStorage()
380
387
 
381
- If your terminal doesn't support 256 colors, you can restrict auto-hash to 10 basic terminal-safe colors:
388
+ // Middleware traceId is essential in prod logs but noisy in dev terminal
389
+ function middleware(req, res, next) {
390
+ const reqLog = logger.child({
391
+ traceId: { value: req.headers['x-trace-id'] || crypto.randomUUID(), hideIn: 'dev' },
392
+ method: req.method
393
+ })
394
+ storage.run(reqLog, next)
395
+ }
382
396
 
383
- ```ts
384
- const log = createFiro({ useAllColors: false })
397
+ // Deeply nested function — no logger passing needed
398
+ function someService() {
399
+ const log = storage.getStore() ?? logger
400
+ log.info('Service action performed')
401
+ // dev: [method:GET] Service action performed
402
+ // prod: {"traceId":"a1b2c3","method":"GET","message":"Service action performed"}
403
+ }
385
404
  ```
386
405
 
387
406
  ## Why not pino?
388
407
 
389
- **Pino** is Italian for *Pine*. It's a great, sturdy tree, especially in production.
408
+ **Pino** is Italian for *Pine*. It's a great, sturdy tree, especially in production.
390
409
 
391
- But sometimes you need to **Spruce** up your development experience.
410
+ But sometimes you need to **Spruce** up your development experience.
392
411
 
393
412
  The problem with pino is development. Its default output is raw JSON — one giant line per log entry, completely unreadable. You reach for `pino-pretty`, and suddenly you're maintaining infrastructure just to see what your app is doing.
394
413
 
@@ -400,36 +419,35 @@ The problem with pino is development. Its default output is raw JSON — one gia
400
419
  - **Visual hierarchy:** Debug lines are dimmed; high-signal logs stay readable.
401
420
  - **Zero config:** Beautiful output from the first second.
402
421
 
403
- In prod it emits clean NDJSON, same as pino. Your log aggregator won't know the difference.
422
+ In prod it emits clean NDJSON, same as pino. Your log aggregator won't know the difference. And the speed tax? Smaller than you'd think.
404
423
 
405
424
  ## Performance
406
425
 
407
- Prod mode (NDJSON) with `timestamp: 'epoch'`, Apple M1, Node.js 25. Average time per 100K log lines (lower is better):
426
+ Firo vs [pino](https://github.com/pinojs/pino) head-to-head, both writing to stdout, same machine, same conditions.
427
+
428
+ | Scenario | pino ops/sec | firo ops/sec | pino ms | firo ms | diff |
429
+ | ------------------------------ | -----------: | -----------: | ------: | ------: | -------: |
430
+ | simple string | 941,986 | 812,970 | 106.2 | 123.0 | +15.82% |
431
+ | string + small obj | 749,782 | 673,332 | 133.4 | 148.5 | +11.32% |
432
+ | string + bigger obj | 582,000 | 523,643 | 171.8 | 191.0 | +11.18% |
433
+ | with 3 context items | 818,123 | 589,433 | 122.2 | 169.7 | +38.87% |
434
+ | child logger (2 ctx) | 807,551 | 592,472 | 123.8 | 168.8 | +36.35% |
435
+ | deep child (7 ctx) + rich data | 408,246 | 314,244 | 245.0 | 318.2 | +29.88% |
436
+ | error with Error obj | 389,665 | 458,247 | 256.6 | 218.2 | -14.96% |
437
+
438
+ <sub>Apple M1, Node.js 25, 10 runs × 100K logs per scenario.</sub>
439
+
440
+ Pino is backed by 10 years of relentless optimization: [SonicBoom](https://github.com/pinojs/sonic-boom) async writer, [fast-json-stringify](https://github.com/fastify/fast-json-stringify) with schema-compiled serialization, pre-serialized child context stored as raw JSON fragments, C++ worker threads. It is an obsessively optimized piece of engineering and fully deserves its reputation as the fastest logger in Node.js.
408
441
 
409
- | Scenario | ops/sec | ms |
410
- | ---------------------- | ------- | ------ |
411
- | simple string | 782,488 | 127.8 |
412
- | string + small obj | 656,512 | 152.3 |
413
- | string + bigger obj | 513,087 | 194.9 |
414
- | with 3 context items | 570,441 | 175.3 |
415
- | child logger (2 ctx) | 568,977 | 175.8 |
416
- | error with Error obj | 470,758 | 212.4 |
442
+ Firo uses the most vanilla tools imaginable — `JSON.stringify` and `process.stdout.write`, shipping since 2009. Zero dependencies. Zero tricks. ~30% behind pino on a realistic deep-child scenario with nested payloads. 15% ahead on error serialization.
417
443
 
418
- For comparison, here's [pino's own benchmark](https://github.com/pinojs/pino/blob/main/docs/benchmarks.md) (100K `"hello world"` logs):
444
+ For context, here's where the other loggers stand according to [pino's own benchmarks](https://github.com/pinojs/pino/blob/main/docs/benchmarks.md) (basic "hello world", same machine): winston 174ms, bunyan 228ms, bole 107ms. firo's 123ms puts it comfortably ahead of winston and bunyan, neck and neck with bole — and all of that with a DX that none of them can match.
419
445
 
420
- | Logger | ms |
421
- | ---------------- | ------ |
422
- | pino | 114.8 |
423
- | **firo** | **127.8** |
424
- | bole | 172.7 |
425
- | pino (NodeStream)| 159.2 |
426
- | debug | 220.5 |
427
- | winston | 270.2 |
428
- | bunyan | 377.4 |
446
+ So yes — if you're looking for a pino alternative with gorgeous DX, structured context, and beautiful dev output, firo is right there performance-wise. Almost a drop-in replacement.*
429
447
 
430
- Pino is faster thanks to [SonicBoom](https://github.com/pinojs/sonic-boom)an optimized async writer with buffering and [fast-json-stringify](https://github.com/fastify/fast-json-stringify) for schema-compiled serialization. firo uses plain `JSON.stringify` + `process.stdout.write` and lands within 8% of pino a difference of ~130 nanoseconds per log line.
448
+ <sub>* Okay, not exactly drop-inwe put the message first and the data second, like normal humans. `log.info("hello", data)` instead of `log.info(data, "hello")`. We'll let you decide which API sparks more joy.</sub>
431
449
 
432
- Run it yourself: `pnpm bench`
450
+ Run the benchmark yourself: `pnpm bench`
433
451
 
434
452
  ## API reference
435
453
 
@@ -452,14 +470,21 @@ Run it yourself: `pnpm bench`
452
470
 
453
471
  | Option | Type | Default | Description |
454
472
  |---|---|---|---|
455
- | `mode` | `'dev' \| 'prod'` | `'dev'` | Selects the built-in transport |
456
- | `minLevel` | `LogLevel` | `'debug'` | Minimum level for both modes |
457
- | `minLevelInDev` | `LogLevel` | — | Overrides `minLevel` in dev mode |
458
- | `minLevelInProd` | `LogLevel` | — | Overrides `minLevel` in prod mode |
459
- | `transport` | `TransportFn` | — | Custom transport, overrides `mode` |
460
- | `devTransportConfig` | `DevTransportConfig` | | Options for the built-in dev transport |
461
- | `prodTransportConfig` | `ProdTransportConfig` | — | Options for the built-in JSON prod transport |
462
- | `useAllColors` | `boolean` | `true` | Use all 30 palette colors for auto-hash (set `false` for 10 safe colors) |
473
+ | `mode` | `'dev' \| 'prod'` | `'dev'` | Selects the built-in formatter |
474
+ | `minLevel` | `LogLevel` | `'debug'` | Minimum log level |
475
+ | `formatter` | `FormatterFn` | — | Custom formatter, overrides `mode` |
476
+ | `devFormatterConfig` | `DevFormatterConfig` | — | Options for the built-in dev formatter |
477
+ | `prodFormatterConfig` | `ProdFormatterConfig` | — | Options for the built-in JSON prod formatter |
478
+ | `useSafeColors` | `boolean` | `false` | Restrict auto-hash to 10 terminal-safe colors (set `true` for basic terminals) |
479
+
480
+ ### Context options
481
+
482
+ | Option | Type | Default | Description |
483
+ |---|---|---|---|
484
+ | `colorIndex` | `number` | auto | Color palette index (0–29) |
485
+ | `color` | `string` | — | Raw ANSI color code (e.g. `'38;5;214'`). Takes priority over `colorIndex` |
486
+ | `omitKey` | `boolean` | `false` | Hide the key, show only the value as `[value]` |
487
+ | `hideIn` | `'dev' \| 'prod'` | — | Hide this context item in dev or prod mode |
463
488
 
464
489
  ## License
465
490
 
package/dist/index.cjs CHANGED
@@ -32,9 +32,9 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  FIRO_COLORS: () => FIRO_COLORS,
34
34
  FiroUtils: () => utils_exports,
35
- createDevTransport: () => createDevTransport,
35
+ createDevFormatter: () => createDevFormatter,
36
36
  createFiro: () => createFiro,
37
- createProdTransport: () => createProdTransport
37
+ createProdFormatter: () => createProdFormatter
38
38
  });
39
39
  module.exports = __toCommonJS(index_exports);
40
40
 
@@ -45,6 +45,7 @@ __export(utils_exports, {
45
45
  LOG_LEVELS: () => LOG_LEVELS,
46
46
  colorize: () => colorize,
47
47
  colorizeLevel: () => colorizeLevel,
48
+ extractMessage: () => extractMessage,
48
49
  getColorIndex: () => getColorIndex,
49
50
  jsonReplacer: () => jsonReplacer,
50
51
  safeStringify: () => safeStringify,
@@ -94,12 +95,12 @@ var FIRO_COLORS = {
94
95
  };
95
96
  var COLORS_LIST = Object.values(FIRO_COLORS);
96
97
  var SAFE_COLORS_COUNT = 10;
97
- var getColorIndex = (str, useAllColors = false) => {
98
+ var getColorIndex = (str, useSafeColors = false) => {
98
99
  let hash = 0;
99
100
  for (let i = 0, len = str.length; i < len; i++) {
100
101
  hash = str.charCodeAt(i) + ((hash << 5) - hash);
101
102
  }
102
- const range = useAllColors ? COLORS_LIST.length : SAFE_COLORS_COUNT;
103
+ const range = useSafeColors ? SAFE_COLORS_COUNT : COLORS_LIST.length;
103
104
  return Math.abs(hash % range);
104
105
  };
105
106
  var colorize = (text, colorIndex, color) => {
@@ -122,13 +123,18 @@ var wrapToError = (obj) => {
122
123
  };
123
124
  var serializeError = (_err) => {
124
125
  const err = wrapToError(_err);
125
- return {
126
+ const result = {
126
127
  message: err.message,
127
128
  stack: err.stack,
128
129
  name: err.name,
129
130
  ...err
130
131
  };
132
+ if (err.cause !== void 0) {
133
+ result.cause = err.cause instanceof Error ? serializeError(err.cause) : err.cause;
134
+ }
135
+ return result;
131
136
  };
137
+ var extractMessage = (msg) => typeof msg === "string" ? msg : msg instanceof Error ? msg.message : typeof msg === "object" && msg !== null ? safeStringify(msg) : String(msg);
132
138
  var colorizeLevel = (level, text) => {
133
139
  if (level === "info") return text;
134
140
  switch (level) {
@@ -146,10 +152,10 @@ var colorizeLevel = (level, text) => {
146
152
  }
147
153
  };
148
154
 
149
- // src/transport_dev.ts
155
+ // src/formatter_dev.ts
150
156
  var import_node_util2 = require("util");
151
157
  var import_node_process = __toESM(require("process"), 1);
152
- var createDevTransport = (config = {}) => {
158
+ var createDevFormatter = (config = {}) => {
153
159
  const locale = config.locale ?? void 0;
154
160
  const timeOpts = {
155
161
  hour12: false,
@@ -159,10 +165,10 @@ var createDevTransport = (config = {}) => {
159
165
  fractionalSecondDigits: 3,
160
166
  ...config.timeOptions || {}
161
167
  };
162
- const transport = (level, context, msg, data, opts) => {
168
+ const formatter = (level, context, msg, data, opts) => {
163
169
  const now = /* @__PURE__ */ new Date();
164
170
  const timestamp = now.toLocaleTimeString(locale, timeOpts);
165
- const contextStr = context.map((ctx) => {
171
+ const contextStr = context.filter((ctx) => ctx.hideIn !== "dev").map((ctx) => {
166
172
  const key = ctx.omitKey ? "" : `${ctx.key}:`;
167
173
  const content = `${key}${ctx.value}`;
168
174
  return colorize(`[${content}]`, ctx.colorIndex, ctx.color);
@@ -189,25 +195,23 @@ var createDevTransport = (config = {}) => {
189
195
  if (level === "error") import_node_process.default.stderr.write(finalLine);
190
196
  else import_node_process.default.stdout.write(finalLine);
191
197
  };
192
- return transport;
198
+ return formatter;
193
199
  };
194
200
 
195
- // src/transport_prod.ts
201
+ // src/formatter_prod.ts
196
202
  var import_node_util3 = require("util");
197
203
  var import_node_process2 = __toESM(require("process"), 1);
198
204
  var buildRecord = (level, context, msg, getTimestamp, data) => {
199
- const contextObj = context.reduce((acc, item) => {
200
- acc[item.key] = item.value;
201
- return acc;
202
- }, {});
203
205
  const logRecord = {
204
206
  timestamp: getTimestamp(),
205
- level,
206
- ...contextObj
207
+ level
207
208
  };
209
+ for (let i = 0, len = context.length; i < len; i++) {
210
+ if (context[i].hideIn === "prod") continue;
211
+ logRecord[context[i].key] = context[i].value;
212
+ }
213
+ logRecord.message = extractMessage(msg);
208
214
  if (level === "error") {
209
- const message = typeof msg === "string" ? msg : msg instanceof Error ? msg.message : typeof msg === "object" && msg !== null ? safeStringify(msg) : String(msg);
210
- logRecord.message = message;
211
215
  if (data instanceof Error) {
212
216
  logRecord.error = serializeError(data);
213
217
  } else if (msg instanceof Error) {
@@ -218,14 +222,16 @@ var buildRecord = (level, context, msg, getTimestamp, data) => {
218
222
  if (data !== void 0) logRecord.data = data;
219
223
  }
220
224
  } else {
221
- logRecord.message = typeof msg === "object" && msg !== null ? safeStringify(msg) : String(msg);
225
+ if (msg instanceof Error) {
226
+ logRecord.error = serializeError(msg);
227
+ }
222
228
  if (data !== void 0) {
223
229
  logRecord.data = data instanceof Error ? serializeError(data) : data;
224
230
  }
225
231
  }
226
232
  return logRecord;
227
233
  };
228
- var createProdTransport = (config = {}) => {
234
+ var createProdFormatter = (config = {}) => {
229
235
  const getTimestamp = config.timestamp === "epoch" ? () => Date.now() : () => (/* @__PURE__ */ new Date()).toISOString();
230
236
  const dest = config.dest ?? import_node_process2.default.stdout;
231
237
  return (level, context, msg, data) => {
@@ -252,10 +258,10 @@ var createProdTransport = (config = {}) => {
252
258
 
253
259
  // src/index.ts
254
260
  var createFiro = (config = {}, parentContext = []) => {
255
- const useAllColors = config.useAllColors ?? true;
261
+ const useSafeColors = config.useSafeColors ?? false;
256
262
  const fill = (item) => ({
257
263
  ...item,
258
- colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key, useAllColors),
264
+ colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key, useSafeColors),
259
265
  color: item.color,
260
266
  omitKey: item.omitKey ?? false
261
267
  });
@@ -264,8 +270,8 @@ var createFiro = (config = {}, parentContext = []) => {
264
270
  return [...context2, ...invokeContext.map(fill)];
265
271
  };
266
272
  const context = [...parentContext.map(fill)];
267
- const transport = config.transport ?? (config.mode === "prod" ? createProdTransport(config.prodTransportConfig) : createDevTransport(config.devTransportConfig));
268
- const minLevelName = config.mode === "prod" ? config.minLevelInProd ?? config.minLevel : config.minLevelInDev ?? config.minLevel;
273
+ const formatter = config.formatter ?? (config.mode === "prod" ? createProdFormatter(config.prodFormatterConfig) : createDevFormatter(config.devFormatterConfig));
274
+ const minLevelName = config.minLevel;
269
275
  const minLevel = LOG_LEVELS[minLevelName ?? "debug"];
270
276
  const getContext = () => context;
271
277
  const hasInContext = (key) => context.some((ctx) => ctx.key === key);
@@ -293,25 +299,25 @@ var createFiro = (config = {}, parentContext = []) => {
293
299
  const { value: extValue, ...opts } = value;
294
300
  return { key, value: extValue, ...opts };
295
301
  }
296
- return { key, value, colorIndex: getColorIndex(key, useAllColors) };
302
+ return { key, value, colorIndex: getColorIndex(key, useSafeColors) };
297
303
  });
298
- return createFiro({ transport, minLevel: minLevelName, useAllColors }, [...context, ...newItems]);
304
+ return createFiro({ formatter, minLevel: minLevelName, useSafeColors }, [...context, ...newItems]);
299
305
  };
300
306
  const debug = (msg, data, opts) => {
301
307
  if (minLevel > LOG_LEVELS.debug) return;
302
- transport("debug", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
308
+ formatter("debug", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
303
309
  };
304
310
  const info = (msg, data, opts) => {
305
311
  if (minLevel > LOG_LEVELS.info) return;
306
- transport("info", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
312
+ formatter("info", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
307
313
  };
308
314
  const warn = (msg, data, opts) => {
309
315
  if (minLevel > LOG_LEVELS.warn) return;
310
- transport("warn", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
316
+ formatter("warn", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
311
317
  };
312
318
  const error = (msgOrError, err, opts) => {
313
319
  if (minLevel > LOG_LEVELS.error) return;
314
- transport("error", appendContextWithInvokeContext(context, opts?.ctx), msgOrError, err, opts);
320
+ formatter("error", appendContextWithInvokeContext(context, opts?.ctx), msgOrError, err, opts);
315
321
  };
316
322
  const logInstance = ((msg, data, opts) => {
317
323
  info(msg, data, opts);
@@ -332,7 +338,7 @@ var createFiro = (config = {}, parentContext = []) => {
332
338
  0 && (module.exports = {
333
339
  FIRO_COLORS,
334
340
  FiroUtils,
335
- createDevTransport,
341
+ createDevFormatter,
336
342
  createFiro,
337
- createProdTransport
343
+ createProdFormatter
338
344
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Available log severity levels. */
2
2
  type LogLevel = 'debug' | 'info' | 'warn' | 'error';
3
+ /** Numeric severity values for each log level, used for threshold filtering. */
3
4
  declare const LOG_LEVELS: {
4
5
  readonly debug: 20;
5
6
  readonly info: 30;
@@ -16,6 +17,8 @@ type ContextOptions = {
16
17
  color?: string;
17
18
  /** If true, the key name is hidden, and only the value is printed. */
18
19
  omitKey?: boolean;
20
+ /** Hide this context item in 'dev' or 'prod' mode. Useful for keeping traceIds out of dev output. */
21
+ hideIn?: 'dev' | 'prod';
19
22
  };
20
23
  /** A single key-value context entry. */
21
24
  type ContextItem = {
@@ -31,6 +34,7 @@ type ContextItemWithOptions = ContextItem & {
31
34
  colorIndex: number;
32
35
  omitKey: boolean;
33
36
  color?: string;
37
+ hideIn?: 'dev' | 'prod';
34
38
  };
35
39
  /** Options that can be passed to a single log call. */
36
40
  type LogOptions = {
@@ -40,7 +44,7 @@ type LogOptions = {
40
44
  ctx?: ContextItem[];
41
45
  };
42
46
  /** The signature of a function responsible for formatting and emitting log records. */
43
- type TransportFn = (level: LogLevel, context: ContextItemWithOptions[], message: string | Error | unknown, data?: Error | unknown, options?: LogOptions) => void;
47
+ type FormatterFn = (level: LogLevel, context: ContextItemWithOptions[], message: string | Error | unknown, data?: Error | unknown, options?: LogOptions) => void;
44
48
  /** Named color palette for context badges. Use with `color` option: `{ color: FIRO_COLORS.skyBlue }` */
45
49
  declare const FIRO_COLORS: {
46
50
  readonly cyan: "36";
@@ -74,12 +78,21 @@ declare const FIRO_COLORS: {
74
78
  readonly tangerine: "38;5;208";
75
79
  readonly periwinkle: "38;5;147";
76
80
  };
77
- declare const getColorIndex: (str: string, useAllColors?: boolean) => number;
81
+ /** Hash a string to a stable color palette index. Similar strings land on different colors. */
82
+ declare const getColorIndex: (str: string, useSafeColors?: boolean) => number;
83
+ /** Wrap text in an ANSI color escape sequence by palette index or raw ANSI code. */
78
84
  declare const colorize: (text: string, colorIndex: number, color?: string) => string;
85
+ /** JSON.stringify replacer that converts BigInt values to strings. */
79
86
  declare const jsonReplacer: (_key: string, value: unknown) => unknown;
87
+ /** Safely stringify any value to JSON with BigInt support. Falls back to `util.inspect` on circular references. */
80
88
  declare const safeStringify: (obj: unknown) => string;
89
+ /** Coerce any value to an Error instance. If already an Error, returns as-is. */
81
90
  declare const wrapToError: (obj: unknown) => Error;
82
- declare const serializeError: (_err: unknown) => any;
91
+ /** Serialize an error-like value to a plain object with `message`, `stack`, `name`, and recursively serialized `cause`. */
92
+ declare const serializeError: (_err: unknown) => Record<string, unknown>;
93
+ /** Extract a human-readable message string from any log input. Useful for building custom formatters. */
94
+ declare const extractMessage: (msg: string | Error | unknown) => string;
95
+ /** Wrap text in an ANSI color based on log level: red for error, yellow for warn, dim for debug. */
83
96
  declare const colorizeLevel: (level: LogLevel, text: string) => string;
84
97
 
85
98
  type utils_ContextExtension = ContextExtension;
@@ -88,41 +101,42 @@ type utils_ContextItemWithOptions = ContextItemWithOptions;
88
101
  type utils_ContextOptions = ContextOptions;
89
102
  type utils_ContextValue = ContextValue;
90
103
  declare const utils_FIRO_COLORS: typeof FIRO_COLORS;
104
+ type utils_FormatterFn = FormatterFn;
91
105
  declare const utils_LOG_LEVELS: typeof LOG_LEVELS;
92
106
  type utils_LogLevel = LogLevel;
93
107
  type utils_LogOptions = LogOptions;
94
- type utils_TransportFn = TransportFn;
95
108
  declare const utils_colorize: typeof colorize;
96
109
  declare const utils_colorizeLevel: typeof colorizeLevel;
110
+ declare const utils_extractMessage: typeof extractMessage;
97
111
  declare const utils_getColorIndex: typeof getColorIndex;
98
112
  declare const utils_jsonReplacer: typeof jsonReplacer;
99
113
  declare const utils_safeStringify: typeof safeStringify;
100
114
  declare const utils_serializeError: typeof serializeError;
101
115
  declare const utils_wrapToError: typeof wrapToError;
102
116
  declare namespace utils {
103
- export { type utils_ContextExtension as ContextExtension, type utils_ContextItem as ContextItem, type utils_ContextItemWithOptions as ContextItemWithOptions, type utils_ContextOptions as ContextOptions, type utils_ContextValue as ContextValue, utils_FIRO_COLORS as FIRO_COLORS, utils_LOG_LEVELS as LOG_LEVELS, type utils_LogLevel as LogLevel, type utils_LogOptions as LogOptions, type utils_TransportFn as TransportFn, utils_colorize as colorize, utils_colorizeLevel as colorizeLevel, utils_getColorIndex as getColorIndex, utils_jsonReplacer as jsonReplacer, utils_safeStringify as safeStringify, utils_serializeError as serializeError, utils_wrapToError as wrapToError };
117
+ export { type utils_ContextExtension as ContextExtension, type utils_ContextItem as ContextItem, type utils_ContextItemWithOptions as ContextItemWithOptions, type utils_ContextOptions as ContextOptions, type utils_ContextValue as ContextValue, utils_FIRO_COLORS as FIRO_COLORS, type utils_FormatterFn as FormatterFn, utils_LOG_LEVELS as LOG_LEVELS, type utils_LogLevel as LogLevel, type utils_LogOptions as LogOptions, utils_colorize as colorize, utils_colorizeLevel as colorizeLevel, utils_extractMessage as extractMessage, utils_getColorIndex as getColorIndex, utils_jsonReplacer as jsonReplacer, utils_safeStringify as safeStringify, utils_serializeError as serializeError, utils_wrapToError as wrapToError };
104
118
  }
105
119
 
106
120
  /**
107
- * Configuration options for the development transport.
121
+ * Configuration options for the development formatter.
108
122
  */
109
- type DevTransportConfig = {
123
+ type DevFormatterConfig = {
110
124
  /** The locale used for formatting the timestamp. Defaults to the system locale. */
111
125
  locale?: string;
112
126
  /** Standard Intl.DateTimeFormatOptions to customize the timestamp output. */
113
127
  timeOptions?: Intl.DateTimeFormatOptions;
114
128
  };
115
129
  /**
116
- * Creates a built-in transport optimized for local development.
130
+ * Creates a built-in formatter optimized for local development.
117
131
  * Emits colored, human-readable strings to stdout/stderr.
118
132
  *
119
- * @param config Optional configuration for the transport, like timestamp formats.
120
- * @returns A `TransportFn` that writes to the console.
133
+ * @param config Optional configuration for the formatter, like timestamp formats.
134
+ * @returns A `FormatterFn` that writes to the console.
121
135
  */
122
- declare const createDevTransport: (config?: DevTransportConfig) => TransportFn;
136
+ declare const createDevFormatter: (config?: DevFormatterConfig) => FormatterFn;
123
137
 
124
138
  type TimestampFormat = 'iso' | 'epoch';
125
- type ProdTransportConfig = {
139
+ type ProdFormatterConfig = {
126
140
  /** Timestamp format: 'iso' (default) for ISO 8601 string, 'epoch' for ms since Unix epoch. */
127
141
  timestamp?: TimestampFormat;
128
142
  /** Output destination. Any object with a `.write(string)` method. Defaults to `process.stdout`. */
@@ -131,33 +145,29 @@ type ProdTransportConfig = {
131
145
  };
132
146
  };
133
147
  /**
134
- * Creates a built-in transport optimized for production.
148
+ * Creates a built-in formatter optimized for production.
135
149
  * Emits strictly structured NDJSON (Newline Delimited JSON) to stdout.
136
150
  *
137
- * @returns A `TransportFn` that writes JSON to standard output.
151
+ * @returns A `FormatterFn` that writes JSON to standard output.
138
152
  */
139
- declare const createProdTransport: (config?: ProdTransportConfig) => TransportFn;
153
+ declare const createProdFormatter: (config?: ProdFormatterConfig) => FormatterFn;
140
154
 
141
155
  /**
142
156
  * Configuration options for creating a logger instance.
143
157
  */
144
158
  type LoggerConfig = {
145
- /** The minimum log level for both modes. Overridden by mode-specific thresholds. */
159
+ /** The minimum log level. */
146
160
  minLevel?: LogLevel;
147
- /** Minimum log level to emit in 'dev' mode. */
148
- minLevelInDev?: LogLevel;
149
- /** Minimum log level to emit in 'prod' mode. */
150
- minLevelInProd?: LogLevel;
151
- /** Specifies the built-in transport to use. Defaults to 'dev'. */
161
+ /** Selects the built-in formatter. Defaults to 'dev'. */
152
162
  mode?: 'dev' | 'prod';
153
- /** Provide a custom transport function to override the built-in behaviors. */
154
- transport?: TransportFn;
155
- /** Options for fine-tuning the built-in development transport (e.g. timestamp format). */
156
- devTransportConfig?: DevTransportConfig;
157
- /** Options for the built-in prod transport (e.g. timestamp format). */
158
- prodTransportConfig?: ProdTransportConfig;
159
- /** Use the full extended color palette (30 colors including 256-color) for auto-assigned context badges. Defaults to true. Set to false to restrict to 10 terminal-safe colors. */
160
- useAllColors?: boolean;
163
+ /** Provide a custom formatter function to override the built-in behaviors. */
164
+ formatter?: FormatterFn;
165
+ /** Options for fine-tuning the built-in development formatter (e.g. timestamp format). */
166
+ devFormatterConfig?: DevFormatterConfig;
167
+ /** Options for the built-in prod formatter (e.g. timestamp format). */
168
+ prodFormatterConfig?: ProdFormatterConfig;
169
+ /** Restrict auto-assigned context badge colors to 10 terminal-safe colors. Defaults to false (all 30 palette colors are used). */
170
+ useSafeColors?: boolean;
161
171
  };
162
172
  /**
163
173
  * The logger instance returned by `createFiro`.
@@ -174,6 +184,8 @@ interface Firo {
174
184
  warn: (msg: string, data?: unknown, opts?: LogOptions) => void;
175
185
  /** Log an error object directly. */
176
186
  error(err: Error | unknown): void;
187
+ /** Log an error object with additional data. */
188
+ error(err: Error, data?: unknown, opts?: LogOptions): void;
177
189
  /** Log a message alongside an error or custom data object. */
178
190
  error(msg: string, err?: Error | unknown, opts?: LogOptions): void;
179
191
  /**
@@ -196,9 +208,9 @@ interface Firo {
196
208
  /**
197
209
  * Creates a new logger instance with the specified configuration.
198
210
  *
199
- * @param config Optional configuration for log levels, mode, and transports.
211
+ * @param config Optional configuration for log levels, format, and formatters.
200
212
  * @returns A fully configured `Firo` instance.
201
213
  */
202
214
  declare const createFiro: (config?: LoggerConfig, parentContext?: ContextItem[]) => Firo;
203
215
 
204
- export { type ContextExtension, type ContextItem, type ContextItemWithOptions, type ContextOptions, type ContextValue, type DevTransportConfig, FIRO_COLORS, type Firo, utils as FiroUtils, type LogLevel, type LogOptions, type LoggerConfig, type ProdTransportConfig, type TimestampFormat, type TransportFn, createDevTransport, createFiro, createProdTransport };
216
+ export { type ContextExtension, type ContextItem, type ContextItemWithOptions, type ContextOptions, type ContextValue, type DevFormatterConfig, FIRO_COLORS, type Firo, utils as FiroUtils, type FormatterFn, type LogLevel, type LogOptions, type LoggerConfig, type ProdFormatterConfig, type TimestampFormat, createDevFormatter, createFiro, createProdFormatter };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Available log severity levels. */
2
2
  type LogLevel = 'debug' | 'info' | 'warn' | 'error';
3
+ /** Numeric severity values for each log level, used for threshold filtering. */
3
4
  declare const LOG_LEVELS: {
4
5
  readonly debug: 20;
5
6
  readonly info: 30;
@@ -16,6 +17,8 @@ type ContextOptions = {
16
17
  color?: string;
17
18
  /** If true, the key name is hidden, and only the value is printed. */
18
19
  omitKey?: boolean;
20
+ /** Hide this context item in 'dev' or 'prod' mode. Useful for keeping traceIds out of dev output. */
21
+ hideIn?: 'dev' | 'prod';
19
22
  };
20
23
  /** A single key-value context entry. */
21
24
  type ContextItem = {
@@ -31,6 +34,7 @@ type ContextItemWithOptions = ContextItem & {
31
34
  colorIndex: number;
32
35
  omitKey: boolean;
33
36
  color?: string;
37
+ hideIn?: 'dev' | 'prod';
34
38
  };
35
39
  /** Options that can be passed to a single log call. */
36
40
  type LogOptions = {
@@ -40,7 +44,7 @@ type LogOptions = {
40
44
  ctx?: ContextItem[];
41
45
  };
42
46
  /** The signature of a function responsible for formatting and emitting log records. */
43
- type TransportFn = (level: LogLevel, context: ContextItemWithOptions[], message: string | Error | unknown, data?: Error | unknown, options?: LogOptions) => void;
47
+ type FormatterFn = (level: LogLevel, context: ContextItemWithOptions[], message: string | Error | unknown, data?: Error | unknown, options?: LogOptions) => void;
44
48
  /** Named color palette for context badges. Use with `color` option: `{ color: FIRO_COLORS.skyBlue }` */
45
49
  declare const FIRO_COLORS: {
46
50
  readonly cyan: "36";
@@ -74,12 +78,21 @@ declare const FIRO_COLORS: {
74
78
  readonly tangerine: "38;5;208";
75
79
  readonly periwinkle: "38;5;147";
76
80
  };
77
- declare const getColorIndex: (str: string, useAllColors?: boolean) => number;
81
+ /** Hash a string to a stable color palette index. Similar strings land on different colors. */
82
+ declare const getColorIndex: (str: string, useSafeColors?: boolean) => number;
83
+ /** Wrap text in an ANSI color escape sequence by palette index or raw ANSI code. */
78
84
  declare const colorize: (text: string, colorIndex: number, color?: string) => string;
85
+ /** JSON.stringify replacer that converts BigInt values to strings. */
79
86
  declare const jsonReplacer: (_key: string, value: unknown) => unknown;
87
+ /** Safely stringify any value to JSON with BigInt support. Falls back to `util.inspect` on circular references. */
80
88
  declare const safeStringify: (obj: unknown) => string;
89
+ /** Coerce any value to an Error instance. If already an Error, returns as-is. */
81
90
  declare const wrapToError: (obj: unknown) => Error;
82
- declare const serializeError: (_err: unknown) => any;
91
+ /** Serialize an error-like value to a plain object with `message`, `stack`, `name`, and recursively serialized `cause`. */
92
+ declare const serializeError: (_err: unknown) => Record<string, unknown>;
93
+ /** Extract a human-readable message string from any log input. Useful for building custom formatters. */
94
+ declare const extractMessage: (msg: string | Error | unknown) => string;
95
+ /** Wrap text in an ANSI color based on log level: red for error, yellow for warn, dim for debug. */
83
96
  declare const colorizeLevel: (level: LogLevel, text: string) => string;
84
97
 
85
98
  type utils_ContextExtension = ContextExtension;
@@ -88,41 +101,42 @@ type utils_ContextItemWithOptions = ContextItemWithOptions;
88
101
  type utils_ContextOptions = ContextOptions;
89
102
  type utils_ContextValue = ContextValue;
90
103
  declare const utils_FIRO_COLORS: typeof FIRO_COLORS;
104
+ type utils_FormatterFn = FormatterFn;
91
105
  declare const utils_LOG_LEVELS: typeof LOG_LEVELS;
92
106
  type utils_LogLevel = LogLevel;
93
107
  type utils_LogOptions = LogOptions;
94
- type utils_TransportFn = TransportFn;
95
108
  declare const utils_colorize: typeof colorize;
96
109
  declare const utils_colorizeLevel: typeof colorizeLevel;
110
+ declare const utils_extractMessage: typeof extractMessage;
97
111
  declare const utils_getColorIndex: typeof getColorIndex;
98
112
  declare const utils_jsonReplacer: typeof jsonReplacer;
99
113
  declare const utils_safeStringify: typeof safeStringify;
100
114
  declare const utils_serializeError: typeof serializeError;
101
115
  declare const utils_wrapToError: typeof wrapToError;
102
116
  declare namespace utils {
103
- export { type utils_ContextExtension as ContextExtension, type utils_ContextItem as ContextItem, type utils_ContextItemWithOptions as ContextItemWithOptions, type utils_ContextOptions as ContextOptions, type utils_ContextValue as ContextValue, utils_FIRO_COLORS as FIRO_COLORS, utils_LOG_LEVELS as LOG_LEVELS, type utils_LogLevel as LogLevel, type utils_LogOptions as LogOptions, type utils_TransportFn as TransportFn, utils_colorize as colorize, utils_colorizeLevel as colorizeLevel, utils_getColorIndex as getColorIndex, utils_jsonReplacer as jsonReplacer, utils_safeStringify as safeStringify, utils_serializeError as serializeError, utils_wrapToError as wrapToError };
117
+ export { type utils_ContextExtension as ContextExtension, type utils_ContextItem as ContextItem, type utils_ContextItemWithOptions as ContextItemWithOptions, type utils_ContextOptions as ContextOptions, type utils_ContextValue as ContextValue, utils_FIRO_COLORS as FIRO_COLORS, type utils_FormatterFn as FormatterFn, utils_LOG_LEVELS as LOG_LEVELS, type utils_LogLevel as LogLevel, type utils_LogOptions as LogOptions, utils_colorize as colorize, utils_colorizeLevel as colorizeLevel, utils_extractMessage as extractMessage, utils_getColorIndex as getColorIndex, utils_jsonReplacer as jsonReplacer, utils_safeStringify as safeStringify, utils_serializeError as serializeError, utils_wrapToError as wrapToError };
104
118
  }
105
119
 
106
120
  /**
107
- * Configuration options for the development transport.
121
+ * Configuration options for the development formatter.
108
122
  */
109
- type DevTransportConfig = {
123
+ type DevFormatterConfig = {
110
124
  /** The locale used for formatting the timestamp. Defaults to the system locale. */
111
125
  locale?: string;
112
126
  /** Standard Intl.DateTimeFormatOptions to customize the timestamp output. */
113
127
  timeOptions?: Intl.DateTimeFormatOptions;
114
128
  };
115
129
  /**
116
- * Creates a built-in transport optimized for local development.
130
+ * Creates a built-in formatter optimized for local development.
117
131
  * Emits colored, human-readable strings to stdout/stderr.
118
132
  *
119
- * @param config Optional configuration for the transport, like timestamp formats.
120
- * @returns A `TransportFn` that writes to the console.
133
+ * @param config Optional configuration for the formatter, like timestamp formats.
134
+ * @returns A `FormatterFn` that writes to the console.
121
135
  */
122
- declare const createDevTransport: (config?: DevTransportConfig) => TransportFn;
136
+ declare const createDevFormatter: (config?: DevFormatterConfig) => FormatterFn;
123
137
 
124
138
  type TimestampFormat = 'iso' | 'epoch';
125
- type ProdTransportConfig = {
139
+ type ProdFormatterConfig = {
126
140
  /** Timestamp format: 'iso' (default) for ISO 8601 string, 'epoch' for ms since Unix epoch. */
127
141
  timestamp?: TimestampFormat;
128
142
  /** Output destination. Any object with a `.write(string)` method. Defaults to `process.stdout`. */
@@ -131,33 +145,29 @@ type ProdTransportConfig = {
131
145
  };
132
146
  };
133
147
  /**
134
- * Creates a built-in transport optimized for production.
148
+ * Creates a built-in formatter optimized for production.
135
149
  * Emits strictly structured NDJSON (Newline Delimited JSON) to stdout.
136
150
  *
137
- * @returns A `TransportFn` that writes JSON to standard output.
151
+ * @returns A `FormatterFn` that writes JSON to standard output.
138
152
  */
139
- declare const createProdTransport: (config?: ProdTransportConfig) => TransportFn;
153
+ declare const createProdFormatter: (config?: ProdFormatterConfig) => FormatterFn;
140
154
 
141
155
  /**
142
156
  * Configuration options for creating a logger instance.
143
157
  */
144
158
  type LoggerConfig = {
145
- /** The minimum log level for both modes. Overridden by mode-specific thresholds. */
159
+ /** The minimum log level. */
146
160
  minLevel?: LogLevel;
147
- /** Minimum log level to emit in 'dev' mode. */
148
- minLevelInDev?: LogLevel;
149
- /** Minimum log level to emit in 'prod' mode. */
150
- minLevelInProd?: LogLevel;
151
- /** Specifies the built-in transport to use. Defaults to 'dev'. */
161
+ /** Selects the built-in formatter. Defaults to 'dev'. */
152
162
  mode?: 'dev' | 'prod';
153
- /** Provide a custom transport function to override the built-in behaviors. */
154
- transport?: TransportFn;
155
- /** Options for fine-tuning the built-in development transport (e.g. timestamp format). */
156
- devTransportConfig?: DevTransportConfig;
157
- /** Options for the built-in prod transport (e.g. timestamp format). */
158
- prodTransportConfig?: ProdTransportConfig;
159
- /** Use the full extended color palette (30 colors including 256-color) for auto-assigned context badges. Defaults to true. Set to false to restrict to 10 terminal-safe colors. */
160
- useAllColors?: boolean;
163
+ /** Provide a custom formatter function to override the built-in behaviors. */
164
+ formatter?: FormatterFn;
165
+ /** Options for fine-tuning the built-in development formatter (e.g. timestamp format). */
166
+ devFormatterConfig?: DevFormatterConfig;
167
+ /** Options for the built-in prod formatter (e.g. timestamp format). */
168
+ prodFormatterConfig?: ProdFormatterConfig;
169
+ /** Restrict auto-assigned context badge colors to 10 terminal-safe colors. Defaults to false (all 30 palette colors are used). */
170
+ useSafeColors?: boolean;
161
171
  };
162
172
  /**
163
173
  * The logger instance returned by `createFiro`.
@@ -174,6 +184,8 @@ interface Firo {
174
184
  warn: (msg: string, data?: unknown, opts?: LogOptions) => void;
175
185
  /** Log an error object directly. */
176
186
  error(err: Error | unknown): void;
187
+ /** Log an error object with additional data. */
188
+ error(err: Error, data?: unknown, opts?: LogOptions): void;
177
189
  /** Log a message alongside an error or custom data object. */
178
190
  error(msg: string, err?: Error | unknown, opts?: LogOptions): void;
179
191
  /**
@@ -196,9 +208,9 @@ interface Firo {
196
208
  /**
197
209
  * Creates a new logger instance with the specified configuration.
198
210
  *
199
- * @param config Optional configuration for log levels, mode, and transports.
211
+ * @param config Optional configuration for log levels, format, and formatters.
200
212
  * @returns A fully configured `Firo` instance.
201
213
  */
202
214
  declare const createFiro: (config?: LoggerConfig, parentContext?: ContextItem[]) => Firo;
203
215
 
204
- export { type ContextExtension, type ContextItem, type ContextItemWithOptions, type ContextOptions, type ContextValue, type DevTransportConfig, FIRO_COLORS, type Firo, utils as FiroUtils, type LogLevel, type LogOptions, type LoggerConfig, type ProdTransportConfig, type TimestampFormat, type TransportFn, createDevTransport, createFiro, createProdTransport };
216
+ export { type ContextExtension, type ContextItem, type ContextItemWithOptions, type ContextOptions, type ContextValue, type DevFormatterConfig, FIRO_COLORS, type Firo, utils as FiroUtils, type FormatterFn, type LogLevel, type LogOptions, type LoggerConfig, type ProdFormatterConfig, type TimestampFormat, createDevFormatter, createFiro, createProdFormatter };
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ __export(utils_exports, {
11
11
  LOG_LEVELS: () => LOG_LEVELS,
12
12
  colorize: () => colorize,
13
13
  colorizeLevel: () => colorizeLevel,
14
+ extractMessage: () => extractMessage,
14
15
  getColorIndex: () => getColorIndex,
15
16
  jsonReplacer: () => jsonReplacer,
16
17
  safeStringify: () => safeStringify,
@@ -60,12 +61,12 @@ var FIRO_COLORS = {
60
61
  };
61
62
  var COLORS_LIST = Object.values(FIRO_COLORS);
62
63
  var SAFE_COLORS_COUNT = 10;
63
- var getColorIndex = (str, useAllColors = false) => {
64
+ var getColorIndex = (str, useSafeColors = false) => {
64
65
  let hash = 0;
65
66
  for (let i = 0, len = str.length; i < len; i++) {
66
67
  hash = str.charCodeAt(i) + ((hash << 5) - hash);
67
68
  }
68
- const range = useAllColors ? COLORS_LIST.length : SAFE_COLORS_COUNT;
69
+ const range = useSafeColors ? SAFE_COLORS_COUNT : COLORS_LIST.length;
69
70
  return Math.abs(hash % range);
70
71
  };
71
72
  var colorize = (text, colorIndex, color) => {
@@ -88,13 +89,18 @@ var wrapToError = (obj) => {
88
89
  };
89
90
  var serializeError = (_err) => {
90
91
  const err = wrapToError(_err);
91
- return {
92
+ const result = {
92
93
  message: err.message,
93
94
  stack: err.stack,
94
95
  name: err.name,
95
96
  ...err
96
97
  };
98
+ if (err.cause !== void 0) {
99
+ result.cause = err.cause instanceof Error ? serializeError(err.cause) : err.cause;
100
+ }
101
+ return result;
97
102
  };
103
+ var extractMessage = (msg) => typeof msg === "string" ? msg : msg instanceof Error ? msg.message : typeof msg === "object" && msg !== null ? safeStringify(msg) : String(msg);
98
104
  var colorizeLevel = (level, text) => {
99
105
  if (level === "info") return text;
100
106
  switch (level) {
@@ -112,10 +118,10 @@ var colorizeLevel = (level, text) => {
112
118
  }
113
119
  };
114
120
 
115
- // src/transport_dev.ts
121
+ // src/formatter_dev.ts
116
122
  import { inspect as inspect2 } from "util";
117
123
  import process from "process";
118
- var createDevTransport = (config = {}) => {
124
+ var createDevFormatter = (config = {}) => {
119
125
  const locale = config.locale ?? void 0;
120
126
  const timeOpts = {
121
127
  hour12: false,
@@ -125,10 +131,10 @@ var createDevTransport = (config = {}) => {
125
131
  fractionalSecondDigits: 3,
126
132
  ...config.timeOptions || {}
127
133
  };
128
- const transport = (level, context, msg, data, opts) => {
134
+ const formatter = (level, context, msg, data, opts) => {
129
135
  const now = /* @__PURE__ */ new Date();
130
136
  const timestamp = now.toLocaleTimeString(locale, timeOpts);
131
- const contextStr = context.map((ctx) => {
137
+ const contextStr = context.filter((ctx) => ctx.hideIn !== "dev").map((ctx) => {
132
138
  const key = ctx.omitKey ? "" : `${ctx.key}:`;
133
139
  const content = `${key}${ctx.value}`;
134
140
  return colorize(`[${content}]`, ctx.colorIndex, ctx.color);
@@ -155,25 +161,23 @@ var createDevTransport = (config = {}) => {
155
161
  if (level === "error") process.stderr.write(finalLine);
156
162
  else process.stdout.write(finalLine);
157
163
  };
158
- return transport;
164
+ return formatter;
159
165
  };
160
166
 
161
- // src/transport_prod.ts
167
+ // src/formatter_prod.ts
162
168
  import { inspect as inspect3 } from "util";
163
169
  import process2 from "process";
164
170
  var buildRecord = (level, context, msg, getTimestamp, data) => {
165
- const contextObj = context.reduce((acc, item) => {
166
- acc[item.key] = item.value;
167
- return acc;
168
- }, {});
169
171
  const logRecord = {
170
172
  timestamp: getTimestamp(),
171
- level,
172
- ...contextObj
173
+ level
173
174
  };
175
+ for (let i = 0, len = context.length; i < len; i++) {
176
+ if (context[i].hideIn === "prod") continue;
177
+ logRecord[context[i].key] = context[i].value;
178
+ }
179
+ logRecord.message = extractMessage(msg);
174
180
  if (level === "error") {
175
- const message = typeof msg === "string" ? msg : msg instanceof Error ? msg.message : typeof msg === "object" && msg !== null ? safeStringify(msg) : String(msg);
176
- logRecord.message = message;
177
181
  if (data instanceof Error) {
178
182
  logRecord.error = serializeError(data);
179
183
  } else if (msg instanceof Error) {
@@ -184,14 +188,16 @@ var buildRecord = (level, context, msg, getTimestamp, data) => {
184
188
  if (data !== void 0) logRecord.data = data;
185
189
  }
186
190
  } else {
187
- logRecord.message = typeof msg === "object" && msg !== null ? safeStringify(msg) : String(msg);
191
+ if (msg instanceof Error) {
192
+ logRecord.error = serializeError(msg);
193
+ }
188
194
  if (data !== void 0) {
189
195
  logRecord.data = data instanceof Error ? serializeError(data) : data;
190
196
  }
191
197
  }
192
198
  return logRecord;
193
199
  };
194
- var createProdTransport = (config = {}) => {
200
+ var createProdFormatter = (config = {}) => {
195
201
  const getTimestamp = config.timestamp === "epoch" ? () => Date.now() : () => (/* @__PURE__ */ new Date()).toISOString();
196
202
  const dest = config.dest ?? process2.stdout;
197
203
  return (level, context, msg, data) => {
@@ -218,10 +224,10 @@ var createProdTransport = (config = {}) => {
218
224
 
219
225
  // src/index.ts
220
226
  var createFiro = (config = {}, parentContext = []) => {
221
- const useAllColors = config.useAllColors ?? true;
227
+ const useSafeColors = config.useSafeColors ?? false;
222
228
  const fill = (item) => ({
223
229
  ...item,
224
- colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key, useAllColors),
230
+ colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key, useSafeColors),
225
231
  color: item.color,
226
232
  omitKey: item.omitKey ?? false
227
233
  });
@@ -230,8 +236,8 @@ var createFiro = (config = {}, parentContext = []) => {
230
236
  return [...context2, ...invokeContext.map(fill)];
231
237
  };
232
238
  const context = [...parentContext.map(fill)];
233
- const transport = config.transport ?? (config.mode === "prod" ? createProdTransport(config.prodTransportConfig) : createDevTransport(config.devTransportConfig));
234
- const minLevelName = config.mode === "prod" ? config.minLevelInProd ?? config.minLevel : config.minLevelInDev ?? config.minLevel;
239
+ const formatter = config.formatter ?? (config.mode === "prod" ? createProdFormatter(config.prodFormatterConfig) : createDevFormatter(config.devFormatterConfig));
240
+ const minLevelName = config.minLevel;
235
241
  const minLevel = LOG_LEVELS[minLevelName ?? "debug"];
236
242
  const getContext = () => context;
237
243
  const hasInContext = (key) => context.some((ctx) => ctx.key === key);
@@ -259,25 +265,25 @@ var createFiro = (config = {}, parentContext = []) => {
259
265
  const { value: extValue, ...opts } = value;
260
266
  return { key, value: extValue, ...opts };
261
267
  }
262
- return { key, value, colorIndex: getColorIndex(key, useAllColors) };
268
+ return { key, value, colorIndex: getColorIndex(key, useSafeColors) };
263
269
  });
264
- return createFiro({ transport, minLevel: minLevelName, useAllColors }, [...context, ...newItems]);
270
+ return createFiro({ formatter, minLevel: minLevelName, useSafeColors }, [...context, ...newItems]);
265
271
  };
266
272
  const debug = (msg, data, opts) => {
267
273
  if (minLevel > LOG_LEVELS.debug) return;
268
- transport("debug", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
274
+ formatter("debug", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
269
275
  };
270
276
  const info = (msg, data, opts) => {
271
277
  if (minLevel > LOG_LEVELS.info) return;
272
- transport("info", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
278
+ formatter("info", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
273
279
  };
274
280
  const warn = (msg, data, opts) => {
275
281
  if (minLevel > LOG_LEVELS.warn) return;
276
- transport("warn", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
282
+ formatter("warn", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
277
283
  };
278
284
  const error = (msgOrError, err, opts) => {
279
285
  if (minLevel > LOG_LEVELS.error) return;
280
- transport("error", appendContextWithInvokeContext(context, opts?.ctx), msgOrError, err, opts);
286
+ formatter("error", appendContextWithInvokeContext(context, opts?.ctx), msgOrError, err, opts);
281
287
  };
282
288
  const logInstance = ((msg, data, opts) => {
283
289
  info(msg, data, opts);
@@ -297,7 +303,7 @@ var createFiro = (config = {}, parentContext = []) => {
297
303
  export {
298
304
  FIRO_COLORS,
299
305
  utils_exports as FiroUtils,
300
- createDevTransport,
306
+ createDevFormatter,
301
307
  createFiro,
302
- createProdTransport
308
+ createProdFormatter
303
309
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fend/firo",
3
- "version": "0.0.5",
4
- "description": "Elegant logger for Node.js, Bun and Deno with brilliant DX.",
3
+ "version": "0.0.7",
4
+ "description": "Elegant logger for Node.js, Bun and Deno with brilliant DX and pino-grade speed",
5
5
  "keywords": [
6
6
  "firo",
7
7
  "logger",
@@ -42,6 +42,8 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^25.5.0",
45
+ "bumpp": "^11.0.1",
46
+ "pino": "^10.3.1",
45
47
  "tsup": "^8.5.1",
46
48
  "tsx": "^4.0.0",
47
49
  "typescript": "^5.9.3"
@@ -52,6 +54,7 @@
52
54
  "check": "tsc --noEmit && node --import tsx --test test/*.test.ts",
53
55
  "typecheck": "tsc --noEmit",
54
56
  "demo": "tsx demo.ts",
55
- "bench": "tsx benchmark/prod.ts > /dev/null"
57
+ "bench": "tsx benchmark/prod.ts > /dev/null",
58
+ "bump": "bumpp --files package.json --files jsr.json --execute \"pnpm check && pnpm build && npm publish --dry-run --access public && deno publish --dry-run\" --commit \"v%s\" --no-push"
56
59
  }
57
60
  }