@fend/firo 0.0.6 → 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 +179 -159
- package/dist/index.cjs +24 -23
- package/dist/index.d.cts +30 -31
- package/dist/index.d.ts +30 -31
- package/dist/index.js +22 -21
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
[](https://github.com/fend25/firo/actions/workflows/publish.yml)
|
|
8
8
|
[](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.
|
|
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
|
|
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(
|
|
176
|
+
log.addContext('region', { value: 'west', colorIndex: 3 })
|
|
176
177
|
|
|
177
178
|
// Use any ANSI color — 256-color, truecolor, anything
|
|
178
|
-
log.addContext(
|
|
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
|
-
|
|
182
|
+
// Hide in dev — useful for traceIds that clutter the terminal
|
|
183
|
+
log.addContext('traceId', { value: 'abc-123-xyz', hideIn: 'dev' })
|
|
183
184
|
|
|
184
|
-
|
|
185
|
-
log.
|
|
185
|
+
// Hide in prod — dev-only debugging context
|
|
186
|
+
log.addContext('debugTag', { value: 'perf-test', hideIn: 'prod' })
|
|
186
187
|
```
|
|
187
188
|
|
|
188
|
-
###
|
|
189
|
+
### Context API
|
|
189
190
|
|
|
190
191
|
```ts
|
|
191
|
-
|
|
192
|
+
log.getContext() // ContextItem[]
|
|
193
|
+
log.hasInContext('key') // boolean
|
|
194
|
+
log.removeFromContext('env')
|
|
192
195
|
```
|
|
193
196
|
|
|
194
197
|
## Child loggers
|
|
@@ -234,90 +237,75 @@ log.error('Payment failed', err, {
|
|
|
234
237
|
})
|
|
235
238
|
```
|
|
236
239
|
|
|
237
|
-
##
|
|
240
|
+
## Dev formatter options
|
|
238
241
|
|
|
239
|
-
|
|
242
|
+
Fine-tune the dev formatter's timestamp format. For example, to remove seconds and milliseconds:
|
|
240
243
|
|
|
241
244
|
```ts
|
|
242
|
-
|
|
243
|
-
// because stack trace with a couple of extra levels of indirection is definitely better than no stack trace at all
|
|
244
|
-
log.error('Something went wrong')
|
|
245
|
+
import { createFiro } from '@fend/firo'
|
|
245
246
|
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
```
|
|
248
258
|
|
|
249
|
-
|
|
250
|
-
log.error(new Error('Unhandled'))
|
|
259
|
+
## Color palette
|
|
251
260
|
|
|
252
|
-
|
|
253
|
-
log.error(new Error('DB down'), { query: 'SELECT ...', reqId: 123 })
|
|
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
|
-
|
|
256
|
-
log.error(someUnknownThing)
|
|
257
|
-
```
|
|
263
|
+

|
|
258
264
|
|
|
259
|
-
|
|
265
|
+
### How it works
|
|
260
266
|
|
|
261
|
-
|
|
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.
|
|
268
|
+
|
|
269
|
+
You can also pin a specific color using `FIRO_COLORS` — a named palette with full IDE autocomplete:
|
|
262
270
|
|
|
263
271
|
```ts
|
|
264
|
-
import
|
|
272
|
+
import { createFiro, FIRO_COLORS } from '@fend/firo'
|
|
265
273
|
|
|
266
|
-
const
|
|
267
|
-
// level: 'debug' | 'info' | 'warn' | 'error'
|
|
268
|
-
// context: ContextItemWithOptions[]
|
|
269
|
-
// msg: string | Error | unknown
|
|
270
|
-
// data: Error | unknown
|
|
271
|
-
// opts: LogOptions | undefined
|
|
272
|
-
}
|
|
274
|
+
const log = createFiro()
|
|
273
275
|
|
|
274
|
-
|
|
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 })
|
|
275
279
|
```
|
|
276
280
|
|
|
277
|
-
|
|
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`.
|
|
278
282
|
|
|
279
|
-
|
|
283
|
+
### Want even more variety?
|
|
280
284
|
|
|
281
|
-
|
|
282
|
-
import { FiroUtils } from '@fend/firo'
|
|
285
|
+
You can also pass any raw ANSI code as a string — 256-color, truecolor, go wild:
|
|
283
286
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
FiroUtils.jsonReplacer // replacer for JSON.stringify (handles bigint)
|
|
288
|
-
FiroUtils.extractMessage(msg) // extract message string from string | Error | unknown
|
|
289
|
-
FiroUtils.colorize(text, idx) // wrap text in ANSI color by palette index
|
|
290
|
-
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
|
|
291
290
|
```
|
|
292
291
|
|
|
293
|
-
|
|
292
|
+
### Restrict to safe colors
|
|
294
293
|
|
|
295
|
-
|
|
294
|
+
If your terminal doesn't support 256 colors, you can restrict auto-hash to 10 basic terminal-safe colors:
|
|
296
295
|
|
|
297
296
|
```ts
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const log = createFiro({
|
|
301
|
-
devTransportConfig: {
|
|
302
|
-
timeOptions: {
|
|
303
|
-
hour: '2-digit',
|
|
304
|
-
minute: '2-digit',
|
|
305
|
-
second: undefined,
|
|
306
|
-
fractionalSecondDigits: undefined
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
})
|
|
297
|
+
const log = createFiro({ useSafeColors: true })
|
|
310
298
|
```
|
|
311
299
|
|
|
312
|
-
## Prod
|
|
300
|
+
## Prod formatter options
|
|
313
301
|
|
|
314
|
-
Configure the prod (JSON)
|
|
302
|
+
Configure the prod (JSON) formatter's timestamp format:
|
|
315
303
|
|
|
316
304
|
```ts
|
|
317
305
|
// Epoch ms (faster, same as pino)
|
|
318
306
|
const log = createFiro({
|
|
319
307
|
mode: 'prod',
|
|
320
|
-
|
|
308
|
+
prodFormatterConfig: { timestamp: 'epoch' }
|
|
321
309
|
})
|
|
322
310
|
// {"timestamp":1711100000000,"level":"info","message":"hello"}
|
|
323
311
|
|
|
@@ -328,7 +316,7 @@ const log = createFiro({ mode: 'prod' })
|
|
|
328
316
|
|
|
329
317
|
### Custom destination
|
|
330
318
|
|
|
331
|
-
By default, prod
|
|
319
|
+
By default, prod formatter writes to `process.stdout`. You can redirect output to any object with a `.write(string)` method:
|
|
332
320
|
|
|
333
321
|
```ts
|
|
334
322
|
import { createFiro } from '@fend/firo'
|
|
@@ -337,63 +325,89 @@ import { createWriteStream } from 'node:fs'
|
|
|
337
325
|
// Write to a file
|
|
338
326
|
const log = createFiro({
|
|
339
327
|
mode: 'prod',
|
|
340
|
-
|
|
328
|
+
prodFormatterConfig: { dest: createWriteStream('/var/log/app.log') }
|
|
341
329
|
})
|
|
342
330
|
|
|
343
331
|
// Use SonicBoom for async buffered writes (same as pino)
|
|
344
332
|
import SonicBoom from 'sonic-boom'
|
|
345
333
|
const log = createFiro({
|
|
346
334
|
mode: 'prod',
|
|
347
|
-
|
|
335
|
+
prodFormatterConfig: { dest: new SonicBoom({ fd: 1 }) }
|
|
348
336
|
})
|
|
349
337
|
```
|
|
350
338
|
|
|
351
|
-
##
|
|
339
|
+
## Custom formatter
|
|
352
340
|
|
|
353
|
-
|
|
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.
|
|
354
342
|
|
|
355
|
-
|
|
343
|
+
```ts
|
|
344
|
+
import type { FormatterFn } from '@fend/firo'
|
|
356
345
|
|
|
357
|
-
|
|
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
|
+
}
|
|
358
353
|
|
|
359
|
-
|
|
354
|
+
const log = createFiro({ formatter: myFormatter })
|
|
355
|
+
```
|
|
360
356
|
|
|
361
|
-
You
|
|
357
|
+
You don't have to start from scratch — all the helpers we use internally are yours too:
|
|
362
358
|
|
|
363
|
-
|
|
364
|
-
import { createFiro, FIRO_COLORS } from '@fend/firo'
|
|
359
|
+
#### FiroUtils
|
|
365
360
|
|
|
366
|
-
|
|
361
|
+
`FiroUtils` exposes helper functions useful for building custom formatters:
|
|
367
362
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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)
|
|
371
373
|
```
|
|
372
374
|
|
|
373
|
-
|
|
375
|
+
## Best practices
|
|
374
376
|
|
|
375
|
-
###
|
|
377
|
+
### AsyncLocalStorage (Traceability)
|
|
376
378
|
|
|
377
|
-
|
|
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.
|
|
378
380
|
|
|
379
381
|
```ts
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
```
|
|
382
|
+
import { AsyncLocalStorage } from 'node:util'
|
|
383
|
+
import { createFiro } from '@fend/firo'
|
|
383
384
|
|
|
384
|
-
|
|
385
|
+
const logger = createFiro()
|
|
386
|
+
const storage = new AsyncLocalStorage()
|
|
385
387
|
|
|
386
|
-
|
|
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
|
+
}
|
|
387
396
|
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
}
|
|
390
404
|
```
|
|
391
405
|
|
|
392
406
|
## Why not pino?
|
|
393
407
|
|
|
394
|
-
**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.
|
|
395
409
|
|
|
396
|
-
But sometimes you need to **Spruce** up your development experience.
|
|
410
|
+
But sometimes you need to **Spruce** up your development experience.
|
|
397
411
|
|
|
398
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.
|
|
399
413
|
|
|
@@ -405,36 +419,35 @@ The problem with pino is development. Its default output is raw JSON — one gia
|
|
|
405
419
|
- **Visual hierarchy:** Debug lines are dimmed; high-signal logs stay readable.
|
|
406
420
|
- **Zero config:** Beautiful output from the first second.
|
|
407
421
|
|
|
408
|
-
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.
|
|
409
423
|
|
|
410
424
|
## Performance
|
|
411
425
|
|
|
412
|
-
|
|
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.
|
|
413
441
|
|
|
414
|
-
|
|
415
|
-
| ---------------------- | ------- | ------ |
|
|
416
|
-
| simple string | 782,488 | 127.8 |
|
|
417
|
-
| string + small obj | 656,512 | 152.3 |
|
|
418
|
-
| string + bigger obj | 513,087 | 194.9 |
|
|
419
|
-
| with 3 context items | 570,441 | 175.3 |
|
|
420
|
-
| child logger (2 ctx) | 568,977 | 175.8 |
|
|
421
|
-
| 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.
|
|
422
443
|
|
|
423
|
-
For
|
|
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.
|
|
424
445
|
|
|
425
|
-
|
|
426
|
-
| ---------------- | ------ |
|
|
427
|
-
| pino | 114.8 |
|
|
428
|
-
| **firo** | **127.8** |
|
|
429
|
-
| bole | 172.7 |
|
|
430
|
-
| pino (NodeStream)| 159.2 |
|
|
431
|
-
| debug | 220.5 |
|
|
432
|
-
| winston | 270.2 |
|
|
433
|
-
| 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.*
|
|
434
447
|
|
|
435
|
-
|
|
448
|
+
<sub>* Okay, not exactly drop-in — we 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>
|
|
436
449
|
|
|
437
|
-
Run
|
|
450
|
+
Run the benchmark yourself: `pnpm bench`
|
|
438
451
|
|
|
439
452
|
## API reference
|
|
440
453
|
|
|
@@ -457,14 +470,21 @@ Run it yourself: `pnpm bench`
|
|
|
457
470
|
|
|
458
471
|
| Option | Type | Default | Description |
|
|
459
472
|
|---|---|---|---|
|
|
460
|
-
| `mode` | `'dev' \| 'prod'` | `'dev'` | Selects the built-in
|
|
461
|
-
| `minLevel` | `LogLevel` | `'debug'` | Minimum level
|
|
462
|
-
| `
|
|
463
|
-
| `
|
|
464
|
-
| `
|
|
465
|
-
| `
|
|
466
|
-
|
|
467
|
-
|
|
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 |
|
|
468
488
|
|
|
469
489
|
## License
|
|
470
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
|
-
|
|
35
|
+
createDevFormatter: () => createDevFormatter,
|
|
36
36
|
createFiro: () => createFiro,
|
|
37
|
-
|
|
37
|
+
createProdFormatter: () => createProdFormatter
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(index_exports);
|
|
40
40
|
|
|
@@ -95,12 +95,12 @@ var FIRO_COLORS = {
|
|
|
95
95
|
};
|
|
96
96
|
var COLORS_LIST = Object.values(FIRO_COLORS);
|
|
97
97
|
var SAFE_COLORS_COUNT = 10;
|
|
98
|
-
var getColorIndex = (str,
|
|
98
|
+
var getColorIndex = (str, useSafeColors = false) => {
|
|
99
99
|
let hash = 0;
|
|
100
100
|
for (let i = 0, len = str.length; i < len; i++) {
|
|
101
101
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
102
102
|
}
|
|
103
|
-
const range =
|
|
103
|
+
const range = useSafeColors ? SAFE_COLORS_COUNT : COLORS_LIST.length;
|
|
104
104
|
return Math.abs(hash % range);
|
|
105
105
|
};
|
|
106
106
|
var colorize = (text, colorIndex, color) => {
|
|
@@ -152,10 +152,10 @@ var colorizeLevel = (level, text) => {
|
|
|
152
152
|
}
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
-
// src/
|
|
155
|
+
// src/formatter_dev.ts
|
|
156
156
|
var import_node_util2 = require("util");
|
|
157
157
|
var import_node_process = __toESM(require("process"), 1);
|
|
158
|
-
var
|
|
158
|
+
var createDevFormatter = (config = {}) => {
|
|
159
159
|
const locale = config.locale ?? void 0;
|
|
160
160
|
const timeOpts = {
|
|
161
161
|
hour12: false,
|
|
@@ -165,10 +165,10 @@ var createDevTransport = (config = {}) => {
|
|
|
165
165
|
fractionalSecondDigits: 3,
|
|
166
166
|
...config.timeOptions || {}
|
|
167
167
|
};
|
|
168
|
-
const
|
|
168
|
+
const formatter = (level, context, msg, data, opts) => {
|
|
169
169
|
const now = /* @__PURE__ */ new Date();
|
|
170
170
|
const timestamp = now.toLocaleTimeString(locale, timeOpts);
|
|
171
|
-
const contextStr = context.map((ctx) => {
|
|
171
|
+
const contextStr = context.filter((ctx) => ctx.hideIn !== "dev").map((ctx) => {
|
|
172
172
|
const key = ctx.omitKey ? "" : `${ctx.key}:`;
|
|
173
173
|
const content = `${key}${ctx.value}`;
|
|
174
174
|
return colorize(`[${content}]`, ctx.colorIndex, ctx.color);
|
|
@@ -195,10 +195,10 @@ var createDevTransport = (config = {}) => {
|
|
|
195
195
|
if (level === "error") import_node_process.default.stderr.write(finalLine);
|
|
196
196
|
else import_node_process.default.stdout.write(finalLine);
|
|
197
197
|
};
|
|
198
|
-
return
|
|
198
|
+
return formatter;
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
// src/
|
|
201
|
+
// src/formatter_prod.ts
|
|
202
202
|
var import_node_util3 = require("util");
|
|
203
203
|
var import_node_process2 = __toESM(require("process"), 1);
|
|
204
204
|
var buildRecord = (level, context, msg, getTimestamp, data) => {
|
|
@@ -207,6 +207,7 @@ var buildRecord = (level, context, msg, getTimestamp, data) => {
|
|
|
207
207
|
level
|
|
208
208
|
};
|
|
209
209
|
for (let i = 0, len = context.length; i < len; i++) {
|
|
210
|
+
if (context[i].hideIn === "prod") continue;
|
|
210
211
|
logRecord[context[i].key] = context[i].value;
|
|
211
212
|
}
|
|
212
213
|
logRecord.message = extractMessage(msg);
|
|
@@ -230,7 +231,7 @@ var buildRecord = (level, context, msg, getTimestamp, data) => {
|
|
|
230
231
|
}
|
|
231
232
|
return logRecord;
|
|
232
233
|
};
|
|
233
|
-
var
|
|
234
|
+
var createProdFormatter = (config = {}) => {
|
|
234
235
|
const getTimestamp = config.timestamp === "epoch" ? () => Date.now() : () => (/* @__PURE__ */ new Date()).toISOString();
|
|
235
236
|
const dest = config.dest ?? import_node_process2.default.stdout;
|
|
236
237
|
return (level, context, msg, data) => {
|
|
@@ -257,10 +258,10 @@ var createProdTransport = (config = {}) => {
|
|
|
257
258
|
|
|
258
259
|
// src/index.ts
|
|
259
260
|
var createFiro = (config = {}, parentContext = []) => {
|
|
260
|
-
const
|
|
261
|
+
const useSafeColors = config.useSafeColors ?? false;
|
|
261
262
|
const fill = (item) => ({
|
|
262
263
|
...item,
|
|
263
|
-
colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key,
|
|
264
|
+
colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key, useSafeColors),
|
|
264
265
|
color: item.color,
|
|
265
266
|
omitKey: item.omitKey ?? false
|
|
266
267
|
});
|
|
@@ -269,8 +270,8 @@ var createFiro = (config = {}, parentContext = []) => {
|
|
|
269
270
|
return [...context2, ...invokeContext.map(fill)];
|
|
270
271
|
};
|
|
271
272
|
const context = [...parentContext.map(fill)];
|
|
272
|
-
const
|
|
273
|
-
const minLevelName = config.
|
|
273
|
+
const formatter = config.formatter ?? (config.mode === "prod" ? createProdFormatter(config.prodFormatterConfig) : createDevFormatter(config.devFormatterConfig));
|
|
274
|
+
const minLevelName = config.minLevel;
|
|
274
275
|
const minLevel = LOG_LEVELS[minLevelName ?? "debug"];
|
|
275
276
|
const getContext = () => context;
|
|
276
277
|
const hasInContext = (key) => context.some((ctx) => ctx.key === key);
|
|
@@ -298,25 +299,25 @@ var createFiro = (config = {}, parentContext = []) => {
|
|
|
298
299
|
const { value: extValue, ...opts } = value;
|
|
299
300
|
return { key, value: extValue, ...opts };
|
|
300
301
|
}
|
|
301
|
-
return { key, value, colorIndex: getColorIndex(key,
|
|
302
|
+
return { key, value, colorIndex: getColorIndex(key, useSafeColors) };
|
|
302
303
|
});
|
|
303
|
-
return createFiro({
|
|
304
|
+
return createFiro({ formatter, minLevel: minLevelName, useSafeColors }, [...context, ...newItems]);
|
|
304
305
|
};
|
|
305
306
|
const debug = (msg, data, opts) => {
|
|
306
307
|
if (minLevel > LOG_LEVELS.debug) return;
|
|
307
|
-
|
|
308
|
+
formatter("debug", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
|
|
308
309
|
};
|
|
309
310
|
const info = (msg, data, opts) => {
|
|
310
311
|
if (minLevel > LOG_LEVELS.info) return;
|
|
311
|
-
|
|
312
|
+
formatter("info", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
|
|
312
313
|
};
|
|
313
314
|
const warn = (msg, data, opts) => {
|
|
314
315
|
if (minLevel > LOG_LEVELS.warn) return;
|
|
315
|
-
|
|
316
|
+
formatter("warn", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
|
|
316
317
|
};
|
|
317
318
|
const error = (msgOrError, err, opts) => {
|
|
318
319
|
if (minLevel > LOG_LEVELS.error) return;
|
|
319
|
-
|
|
320
|
+
formatter("error", appendContextWithInvokeContext(context, opts?.ctx), msgOrError, err, opts);
|
|
320
321
|
};
|
|
321
322
|
const logInstance = ((msg, data, opts) => {
|
|
322
323
|
info(msg, data, opts);
|
|
@@ -337,7 +338,7 @@ var createFiro = (config = {}, parentContext = []) => {
|
|
|
337
338
|
0 && (module.exports = {
|
|
338
339
|
FIRO_COLORS,
|
|
339
340
|
FiroUtils,
|
|
340
|
-
|
|
341
|
+
createDevFormatter,
|
|
341
342
|
createFiro,
|
|
342
|
-
|
|
343
|
+
createProdFormatter
|
|
343
344
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -17,6 +17,8 @@ type ContextOptions = {
|
|
|
17
17
|
color?: string;
|
|
18
18
|
/** If true, the key name is hidden, and only the value is printed. */
|
|
19
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';
|
|
20
22
|
};
|
|
21
23
|
/** A single key-value context entry. */
|
|
22
24
|
type ContextItem = {
|
|
@@ -32,6 +34,7 @@ type ContextItemWithOptions = ContextItem & {
|
|
|
32
34
|
colorIndex: number;
|
|
33
35
|
omitKey: boolean;
|
|
34
36
|
color?: string;
|
|
37
|
+
hideIn?: 'dev' | 'prod';
|
|
35
38
|
};
|
|
36
39
|
/** Options that can be passed to a single log call. */
|
|
37
40
|
type LogOptions = {
|
|
@@ -41,7 +44,7 @@ type LogOptions = {
|
|
|
41
44
|
ctx?: ContextItem[];
|
|
42
45
|
};
|
|
43
46
|
/** The signature of a function responsible for formatting and emitting log records. */
|
|
44
|
-
type
|
|
47
|
+
type FormatterFn = (level: LogLevel, context: ContextItemWithOptions[], message: string | Error | unknown, data?: Error | unknown, options?: LogOptions) => void;
|
|
45
48
|
/** Named color palette for context badges. Use with `color` option: `{ color: FIRO_COLORS.skyBlue }` */
|
|
46
49
|
declare const FIRO_COLORS: {
|
|
47
50
|
readonly cyan: "36";
|
|
@@ -76,7 +79,7 @@ declare const FIRO_COLORS: {
|
|
|
76
79
|
readonly periwinkle: "38;5;147";
|
|
77
80
|
};
|
|
78
81
|
/** Hash a string to a stable color palette index. Similar strings land on different colors. */
|
|
79
|
-
declare const getColorIndex: (str: string,
|
|
82
|
+
declare const getColorIndex: (str: string, useSafeColors?: boolean) => number;
|
|
80
83
|
/** Wrap text in an ANSI color escape sequence by palette index or raw ANSI code. */
|
|
81
84
|
declare const colorize: (text: string, colorIndex: number, color?: string) => string;
|
|
82
85
|
/** JSON.stringify replacer that converts BigInt values to strings. */
|
|
@@ -87,7 +90,7 @@ declare const safeStringify: (obj: unknown) => string;
|
|
|
87
90
|
declare const wrapToError: (obj: unknown) => Error;
|
|
88
91
|
/** Serialize an error-like value to a plain object with `message`, `stack`, `name`, and recursively serialized `cause`. */
|
|
89
92
|
declare const serializeError: (_err: unknown) => Record<string, unknown>;
|
|
90
|
-
/** Extract a human-readable message string from any log input. Useful for building custom
|
|
93
|
+
/** Extract a human-readable message string from any log input. Useful for building custom formatters. */
|
|
91
94
|
declare const extractMessage: (msg: string | Error | unknown) => string;
|
|
92
95
|
/** Wrap text in an ANSI color based on log level: red for error, yellow for warn, dim for debug. */
|
|
93
96
|
declare const colorizeLevel: (level: LogLevel, text: string) => string;
|
|
@@ -98,10 +101,10 @@ type utils_ContextItemWithOptions = ContextItemWithOptions;
|
|
|
98
101
|
type utils_ContextOptions = ContextOptions;
|
|
99
102
|
type utils_ContextValue = ContextValue;
|
|
100
103
|
declare const utils_FIRO_COLORS: typeof FIRO_COLORS;
|
|
104
|
+
type utils_FormatterFn = FormatterFn;
|
|
101
105
|
declare const utils_LOG_LEVELS: typeof LOG_LEVELS;
|
|
102
106
|
type utils_LogLevel = LogLevel;
|
|
103
107
|
type utils_LogOptions = LogOptions;
|
|
104
|
-
type utils_TransportFn = TransportFn;
|
|
105
108
|
declare const utils_colorize: typeof colorize;
|
|
106
109
|
declare const utils_colorizeLevel: typeof colorizeLevel;
|
|
107
110
|
declare const utils_extractMessage: typeof extractMessage;
|
|
@@ -111,29 +114,29 @@ declare const utils_safeStringify: typeof safeStringify;
|
|
|
111
114
|
declare const utils_serializeError: typeof serializeError;
|
|
112
115
|
declare const utils_wrapToError: typeof wrapToError;
|
|
113
116
|
declare namespace utils {
|
|
114
|
-
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,
|
|
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 };
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
|
118
|
-
* Configuration options for the development
|
|
121
|
+
* Configuration options for the development formatter.
|
|
119
122
|
*/
|
|
120
|
-
type
|
|
123
|
+
type DevFormatterConfig = {
|
|
121
124
|
/** The locale used for formatting the timestamp. Defaults to the system locale. */
|
|
122
125
|
locale?: string;
|
|
123
126
|
/** Standard Intl.DateTimeFormatOptions to customize the timestamp output. */
|
|
124
127
|
timeOptions?: Intl.DateTimeFormatOptions;
|
|
125
128
|
};
|
|
126
129
|
/**
|
|
127
|
-
* Creates a built-in
|
|
130
|
+
* Creates a built-in formatter optimized for local development.
|
|
128
131
|
* Emits colored, human-readable strings to stdout/stderr.
|
|
129
132
|
*
|
|
130
|
-
* @param config Optional configuration for the
|
|
131
|
-
* @returns A `
|
|
133
|
+
* @param config Optional configuration for the formatter, like timestamp formats.
|
|
134
|
+
* @returns A `FormatterFn` that writes to the console.
|
|
132
135
|
*/
|
|
133
|
-
declare const
|
|
136
|
+
declare const createDevFormatter: (config?: DevFormatterConfig) => FormatterFn;
|
|
134
137
|
|
|
135
138
|
type TimestampFormat = 'iso' | 'epoch';
|
|
136
|
-
type
|
|
139
|
+
type ProdFormatterConfig = {
|
|
137
140
|
/** Timestamp format: 'iso' (default) for ISO 8601 string, 'epoch' for ms since Unix epoch. */
|
|
138
141
|
timestamp?: TimestampFormat;
|
|
139
142
|
/** Output destination. Any object with a `.write(string)` method. Defaults to `process.stdout`. */
|
|
@@ -142,33 +145,29 @@ type ProdTransportConfig = {
|
|
|
142
145
|
};
|
|
143
146
|
};
|
|
144
147
|
/**
|
|
145
|
-
* Creates a built-in
|
|
148
|
+
* Creates a built-in formatter optimized for production.
|
|
146
149
|
* Emits strictly structured NDJSON (Newline Delimited JSON) to stdout.
|
|
147
150
|
*
|
|
148
|
-
* @returns A `
|
|
151
|
+
* @returns A `FormatterFn` that writes JSON to standard output.
|
|
149
152
|
*/
|
|
150
|
-
declare const
|
|
153
|
+
declare const createProdFormatter: (config?: ProdFormatterConfig) => FormatterFn;
|
|
151
154
|
|
|
152
155
|
/**
|
|
153
156
|
* Configuration options for creating a logger instance.
|
|
154
157
|
*/
|
|
155
158
|
type LoggerConfig = {
|
|
156
|
-
/** The minimum log level
|
|
159
|
+
/** The minimum log level. */
|
|
157
160
|
minLevel?: LogLevel;
|
|
158
|
-
/**
|
|
159
|
-
minLevelInDev?: LogLevel;
|
|
160
|
-
/** Minimum log level to emit in 'prod' mode. */
|
|
161
|
-
minLevelInProd?: LogLevel;
|
|
162
|
-
/** Specifies the built-in transport to use. Defaults to 'dev'. */
|
|
161
|
+
/** Selects the built-in formatter. Defaults to 'dev'. */
|
|
163
162
|
mode?: 'dev' | 'prod';
|
|
164
|
-
/** Provide a custom
|
|
165
|
-
|
|
166
|
-
/** Options for fine-tuning the built-in development
|
|
167
|
-
|
|
168
|
-
/** Options for the built-in prod
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
|
|
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;
|
|
172
171
|
};
|
|
173
172
|
/**
|
|
174
173
|
* The logger instance returned by `createFiro`.
|
|
@@ -209,9 +208,9 @@ interface Firo {
|
|
|
209
208
|
/**
|
|
210
209
|
* Creates a new logger instance with the specified configuration.
|
|
211
210
|
*
|
|
212
|
-
* @param config Optional configuration for log levels,
|
|
211
|
+
* @param config Optional configuration for log levels, format, and formatters.
|
|
213
212
|
* @returns A fully configured `Firo` instance.
|
|
214
213
|
*/
|
|
215
214
|
declare const createFiro: (config?: LoggerConfig, parentContext?: ContextItem[]) => Firo;
|
|
216
215
|
|
|
217
|
-
export { type ContextExtension, type ContextItem, type ContextItemWithOptions, type ContextOptions, type ContextValue, type
|
|
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
|
@@ -17,6 +17,8 @@ type ContextOptions = {
|
|
|
17
17
|
color?: string;
|
|
18
18
|
/** If true, the key name is hidden, and only the value is printed. */
|
|
19
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';
|
|
20
22
|
};
|
|
21
23
|
/** A single key-value context entry. */
|
|
22
24
|
type ContextItem = {
|
|
@@ -32,6 +34,7 @@ type ContextItemWithOptions = ContextItem & {
|
|
|
32
34
|
colorIndex: number;
|
|
33
35
|
omitKey: boolean;
|
|
34
36
|
color?: string;
|
|
37
|
+
hideIn?: 'dev' | 'prod';
|
|
35
38
|
};
|
|
36
39
|
/** Options that can be passed to a single log call. */
|
|
37
40
|
type LogOptions = {
|
|
@@ -41,7 +44,7 @@ type LogOptions = {
|
|
|
41
44
|
ctx?: ContextItem[];
|
|
42
45
|
};
|
|
43
46
|
/** The signature of a function responsible for formatting and emitting log records. */
|
|
44
|
-
type
|
|
47
|
+
type FormatterFn = (level: LogLevel, context: ContextItemWithOptions[], message: string | Error | unknown, data?: Error | unknown, options?: LogOptions) => void;
|
|
45
48
|
/** Named color palette for context badges. Use with `color` option: `{ color: FIRO_COLORS.skyBlue }` */
|
|
46
49
|
declare const FIRO_COLORS: {
|
|
47
50
|
readonly cyan: "36";
|
|
@@ -76,7 +79,7 @@ declare const FIRO_COLORS: {
|
|
|
76
79
|
readonly periwinkle: "38;5;147";
|
|
77
80
|
};
|
|
78
81
|
/** Hash a string to a stable color palette index. Similar strings land on different colors. */
|
|
79
|
-
declare const getColorIndex: (str: string,
|
|
82
|
+
declare const getColorIndex: (str: string, useSafeColors?: boolean) => number;
|
|
80
83
|
/** Wrap text in an ANSI color escape sequence by palette index or raw ANSI code. */
|
|
81
84
|
declare const colorize: (text: string, colorIndex: number, color?: string) => string;
|
|
82
85
|
/** JSON.stringify replacer that converts BigInt values to strings. */
|
|
@@ -87,7 +90,7 @@ declare const safeStringify: (obj: unknown) => string;
|
|
|
87
90
|
declare const wrapToError: (obj: unknown) => Error;
|
|
88
91
|
/** Serialize an error-like value to a plain object with `message`, `stack`, `name`, and recursively serialized `cause`. */
|
|
89
92
|
declare const serializeError: (_err: unknown) => Record<string, unknown>;
|
|
90
|
-
/** Extract a human-readable message string from any log input. Useful for building custom
|
|
93
|
+
/** Extract a human-readable message string from any log input. Useful for building custom formatters. */
|
|
91
94
|
declare const extractMessage: (msg: string | Error | unknown) => string;
|
|
92
95
|
/** Wrap text in an ANSI color based on log level: red for error, yellow for warn, dim for debug. */
|
|
93
96
|
declare const colorizeLevel: (level: LogLevel, text: string) => string;
|
|
@@ -98,10 +101,10 @@ type utils_ContextItemWithOptions = ContextItemWithOptions;
|
|
|
98
101
|
type utils_ContextOptions = ContextOptions;
|
|
99
102
|
type utils_ContextValue = ContextValue;
|
|
100
103
|
declare const utils_FIRO_COLORS: typeof FIRO_COLORS;
|
|
104
|
+
type utils_FormatterFn = FormatterFn;
|
|
101
105
|
declare const utils_LOG_LEVELS: typeof LOG_LEVELS;
|
|
102
106
|
type utils_LogLevel = LogLevel;
|
|
103
107
|
type utils_LogOptions = LogOptions;
|
|
104
|
-
type utils_TransportFn = TransportFn;
|
|
105
108
|
declare const utils_colorize: typeof colorize;
|
|
106
109
|
declare const utils_colorizeLevel: typeof colorizeLevel;
|
|
107
110
|
declare const utils_extractMessage: typeof extractMessage;
|
|
@@ -111,29 +114,29 @@ declare const utils_safeStringify: typeof safeStringify;
|
|
|
111
114
|
declare const utils_serializeError: typeof serializeError;
|
|
112
115
|
declare const utils_wrapToError: typeof wrapToError;
|
|
113
116
|
declare namespace utils {
|
|
114
|
-
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,
|
|
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 };
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
|
118
|
-
* Configuration options for the development
|
|
121
|
+
* Configuration options for the development formatter.
|
|
119
122
|
*/
|
|
120
|
-
type
|
|
123
|
+
type DevFormatterConfig = {
|
|
121
124
|
/** The locale used for formatting the timestamp. Defaults to the system locale. */
|
|
122
125
|
locale?: string;
|
|
123
126
|
/** Standard Intl.DateTimeFormatOptions to customize the timestamp output. */
|
|
124
127
|
timeOptions?: Intl.DateTimeFormatOptions;
|
|
125
128
|
};
|
|
126
129
|
/**
|
|
127
|
-
* Creates a built-in
|
|
130
|
+
* Creates a built-in formatter optimized for local development.
|
|
128
131
|
* Emits colored, human-readable strings to stdout/stderr.
|
|
129
132
|
*
|
|
130
|
-
* @param config Optional configuration for the
|
|
131
|
-
* @returns A `
|
|
133
|
+
* @param config Optional configuration for the formatter, like timestamp formats.
|
|
134
|
+
* @returns A `FormatterFn` that writes to the console.
|
|
132
135
|
*/
|
|
133
|
-
declare const
|
|
136
|
+
declare const createDevFormatter: (config?: DevFormatterConfig) => FormatterFn;
|
|
134
137
|
|
|
135
138
|
type TimestampFormat = 'iso' | 'epoch';
|
|
136
|
-
type
|
|
139
|
+
type ProdFormatterConfig = {
|
|
137
140
|
/** Timestamp format: 'iso' (default) for ISO 8601 string, 'epoch' for ms since Unix epoch. */
|
|
138
141
|
timestamp?: TimestampFormat;
|
|
139
142
|
/** Output destination. Any object with a `.write(string)` method. Defaults to `process.stdout`. */
|
|
@@ -142,33 +145,29 @@ type ProdTransportConfig = {
|
|
|
142
145
|
};
|
|
143
146
|
};
|
|
144
147
|
/**
|
|
145
|
-
* Creates a built-in
|
|
148
|
+
* Creates a built-in formatter optimized for production.
|
|
146
149
|
* Emits strictly structured NDJSON (Newline Delimited JSON) to stdout.
|
|
147
150
|
*
|
|
148
|
-
* @returns A `
|
|
151
|
+
* @returns A `FormatterFn` that writes JSON to standard output.
|
|
149
152
|
*/
|
|
150
|
-
declare const
|
|
153
|
+
declare const createProdFormatter: (config?: ProdFormatterConfig) => FormatterFn;
|
|
151
154
|
|
|
152
155
|
/**
|
|
153
156
|
* Configuration options for creating a logger instance.
|
|
154
157
|
*/
|
|
155
158
|
type LoggerConfig = {
|
|
156
|
-
/** The minimum log level
|
|
159
|
+
/** The minimum log level. */
|
|
157
160
|
minLevel?: LogLevel;
|
|
158
|
-
/**
|
|
159
|
-
minLevelInDev?: LogLevel;
|
|
160
|
-
/** Minimum log level to emit in 'prod' mode. */
|
|
161
|
-
minLevelInProd?: LogLevel;
|
|
162
|
-
/** Specifies the built-in transport to use. Defaults to 'dev'. */
|
|
161
|
+
/** Selects the built-in formatter. Defaults to 'dev'. */
|
|
163
162
|
mode?: 'dev' | 'prod';
|
|
164
|
-
/** Provide a custom
|
|
165
|
-
|
|
166
|
-
/** Options for fine-tuning the built-in development
|
|
167
|
-
|
|
168
|
-
/** Options for the built-in prod
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
|
|
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;
|
|
172
171
|
};
|
|
173
172
|
/**
|
|
174
173
|
* The logger instance returned by `createFiro`.
|
|
@@ -209,9 +208,9 @@ interface Firo {
|
|
|
209
208
|
/**
|
|
210
209
|
* Creates a new logger instance with the specified configuration.
|
|
211
210
|
*
|
|
212
|
-
* @param config Optional configuration for log levels,
|
|
211
|
+
* @param config Optional configuration for log levels, format, and formatters.
|
|
213
212
|
* @returns A fully configured `Firo` instance.
|
|
214
213
|
*/
|
|
215
214
|
declare const createFiro: (config?: LoggerConfig, parentContext?: ContextItem[]) => Firo;
|
|
216
215
|
|
|
217
|
-
export { type ContextExtension, type ContextItem, type ContextItemWithOptions, type ContextOptions, type ContextValue, type
|
|
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
|
@@ -61,12 +61,12 @@ var FIRO_COLORS = {
|
|
|
61
61
|
};
|
|
62
62
|
var COLORS_LIST = Object.values(FIRO_COLORS);
|
|
63
63
|
var SAFE_COLORS_COUNT = 10;
|
|
64
|
-
var getColorIndex = (str,
|
|
64
|
+
var getColorIndex = (str, useSafeColors = false) => {
|
|
65
65
|
let hash = 0;
|
|
66
66
|
for (let i = 0, len = str.length; i < len; i++) {
|
|
67
67
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
68
68
|
}
|
|
69
|
-
const range =
|
|
69
|
+
const range = useSafeColors ? SAFE_COLORS_COUNT : COLORS_LIST.length;
|
|
70
70
|
return Math.abs(hash % range);
|
|
71
71
|
};
|
|
72
72
|
var colorize = (text, colorIndex, color) => {
|
|
@@ -118,10 +118,10 @@ var colorizeLevel = (level, text) => {
|
|
|
118
118
|
}
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
-
// src/
|
|
121
|
+
// src/formatter_dev.ts
|
|
122
122
|
import { inspect as inspect2 } from "util";
|
|
123
123
|
import process from "process";
|
|
124
|
-
var
|
|
124
|
+
var createDevFormatter = (config = {}) => {
|
|
125
125
|
const locale = config.locale ?? void 0;
|
|
126
126
|
const timeOpts = {
|
|
127
127
|
hour12: false,
|
|
@@ -131,10 +131,10 @@ var createDevTransport = (config = {}) => {
|
|
|
131
131
|
fractionalSecondDigits: 3,
|
|
132
132
|
...config.timeOptions || {}
|
|
133
133
|
};
|
|
134
|
-
const
|
|
134
|
+
const formatter = (level, context, msg, data, opts) => {
|
|
135
135
|
const now = /* @__PURE__ */ new Date();
|
|
136
136
|
const timestamp = now.toLocaleTimeString(locale, timeOpts);
|
|
137
|
-
const contextStr = context.map((ctx) => {
|
|
137
|
+
const contextStr = context.filter((ctx) => ctx.hideIn !== "dev").map((ctx) => {
|
|
138
138
|
const key = ctx.omitKey ? "" : `${ctx.key}:`;
|
|
139
139
|
const content = `${key}${ctx.value}`;
|
|
140
140
|
return colorize(`[${content}]`, ctx.colorIndex, ctx.color);
|
|
@@ -161,10 +161,10 @@ var createDevTransport = (config = {}) => {
|
|
|
161
161
|
if (level === "error") process.stderr.write(finalLine);
|
|
162
162
|
else process.stdout.write(finalLine);
|
|
163
163
|
};
|
|
164
|
-
return
|
|
164
|
+
return formatter;
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
-
// src/
|
|
167
|
+
// src/formatter_prod.ts
|
|
168
168
|
import { inspect as inspect3 } from "util";
|
|
169
169
|
import process2 from "process";
|
|
170
170
|
var buildRecord = (level, context, msg, getTimestamp, data) => {
|
|
@@ -173,6 +173,7 @@ var buildRecord = (level, context, msg, getTimestamp, data) => {
|
|
|
173
173
|
level
|
|
174
174
|
};
|
|
175
175
|
for (let i = 0, len = context.length; i < len; i++) {
|
|
176
|
+
if (context[i].hideIn === "prod") continue;
|
|
176
177
|
logRecord[context[i].key] = context[i].value;
|
|
177
178
|
}
|
|
178
179
|
logRecord.message = extractMessage(msg);
|
|
@@ -196,7 +197,7 @@ var buildRecord = (level, context, msg, getTimestamp, data) => {
|
|
|
196
197
|
}
|
|
197
198
|
return logRecord;
|
|
198
199
|
};
|
|
199
|
-
var
|
|
200
|
+
var createProdFormatter = (config = {}) => {
|
|
200
201
|
const getTimestamp = config.timestamp === "epoch" ? () => Date.now() : () => (/* @__PURE__ */ new Date()).toISOString();
|
|
201
202
|
const dest = config.dest ?? process2.stdout;
|
|
202
203
|
return (level, context, msg, data) => {
|
|
@@ -223,10 +224,10 @@ var createProdTransport = (config = {}) => {
|
|
|
223
224
|
|
|
224
225
|
// src/index.ts
|
|
225
226
|
var createFiro = (config = {}, parentContext = []) => {
|
|
226
|
-
const
|
|
227
|
+
const useSafeColors = config.useSafeColors ?? false;
|
|
227
228
|
const fill = (item) => ({
|
|
228
229
|
...item,
|
|
229
|
-
colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key,
|
|
230
|
+
colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key, useSafeColors),
|
|
230
231
|
color: item.color,
|
|
231
232
|
omitKey: item.omitKey ?? false
|
|
232
233
|
});
|
|
@@ -235,8 +236,8 @@ var createFiro = (config = {}, parentContext = []) => {
|
|
|
235
236
|
return [...context2, ...invokeContext.map(fill)];
|
|
236
237
|
};
|
|
237
238
|
const context = [...parentContext.map(fill)];
|
|
238
|
-
const
|
|
239
|
-
const minLevelName = config.
|
|
239
|
+
const formatter = config.formatter ?? (config.mode === "prod" ? createProdFormatter(config.prodFormatterConfig) : createDevFormatter(config.devFormatterConfig));
|
|
240
|
+
const minLevelName = config.minLevel;
|
|
240
241
|
const minLevel = LOG_LEVELS[minLevelName ?? "debug"];
|
|
241
242
|
const getContext = () => context;
|
|
242
243
|
const hasInContext = (key) => context.some((ctx) => ctx.key === key);
|
|
@@ -264,25 +265,25 @@ var createFiro = (config = {}, parentContext = []) => {
|
|
|
264
265
|
const { value: extValue, ...opts } = value;
|
|
265
266
|
return { key, value: extValue, ...opts };
|
|
266
267
|
}
|
|
267
|
-
return { key, value, colorIndex: getColorIndex(key,
|
|
268
|
+
return { key, value, colorIndex: getColorIndex(key, useSafeColors) };
|
|
268
269
|
});
|
|
269
|
-
return createFiro({
|
|
270
|
+
return createFiro({ formatter, minLevel: minLevelName, useSafeColors }, [...context, ...newItems]);
|
|
270
271
|
};
|
|
271
272
|
const debug = (msg, data, opts) => {
|
|
272
273
|
if (minLevel > LOG_LEVELS.debug) return;
|
|
273
|
-
|
|
274
|
+
formatter("debug", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
|
|
274
275
|
};
|
|
275
276
|
const info = (msg, data, opts) => {
|
|
276
277
|
if (minLevel > LOG_LEVELS.info) return;
|
|
277
|
-
|
|
278
|
+
formatter("info", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
|
|
278
279
|
};
|
|
279
280
|
const warn = (msg, data, opts) => {
|
|
280
281
|
if (minLevel > LOG_LEVELS.warn) return;
|
|
281
|
-
|
|
282
|
+
formatter("warn", appendContextWithInvokeContext(context, opts?.ctx), msg, data, opts);
|
|
282
283
|
};
|
|
283
284
|
const error = (msgOrError, err, opts) => {
|
|
284
285
|
if (minLevel > LOG_LEVELS.error) return;
|
|
285
|
-
|
|
286
|
+
formatter("error", appendContextWithInvokeContext(context, opts?.ctx), msgOrError, err, opts);
|
|
286
287
|
};
|
|
287
288
|
const logInstance = ((msg, data, opts) => {
|
|
288
289
|
info(msg, data, opts);
|
|
@@ -302,7 +303,7 @@ var createFiro = (config = {}, parentContext = []) => {
|
|
|
302
303
|
export {
|
|
303
304
|
FIRO_COLORS,
|
|
304
305
|
utils_exports as FiroUtils,
|
|
305
|
-
|
|
306
|
+
createDevFormatter,
|
|
306
307
|
createFiro,
|
|
307
|
-
|
|
308
|
+
createProdFormatter
|
|
308
309
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fend/firo",
|
|
3
|
-
"version": "0.0.
|
|
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",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^25.5.0",
|
|
45
45
|
"bumpp": "^11.0.1",
|
|
46
|
+
"pino": "^10.3.1",
|
|
46
47
|
"tsup": "^8.5.1",
|
|
47
48
|
"tsx": "^4.0.0",
|
|
48
49
|
"typescript": "^5.9.3"
|