@fend/firo 0.0.1
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 +386 -0
- package/dist/index.cjs +352 -0
- package/dist/index.d.cts +166 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +312 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# firo 🌲
|
|
2
|
+
|
|
3
|
+
**Spruce up your logs!**
|
|
4
|
+
|
|
5
|
+
The logger for Node.js, Bun and Deno you've been looking for.
|
|
6
|
+
|
|
7
|
+
Beautiful **dev** output - out of the box. High-load ready NDJSON for **prod**.
|
|
8
|
+
|
|
9
|
+
Think of it as pino, but with brilliant DX. **firo** (from *Fir*) is the elegant, refined sibling of the logging forest.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Dev mode** — colored, timestamped, human-readable output with context badges
|
|
16
|
+
- **Prod mode** — structured NDJSON, one record per line, ready for log aggregators
|
|
17
|
+
- **Async mode** — non-blocking buffered output for high-load production
|
|
18
|
+
- **Context system** — attach key/value pairs that beautifully appear in every subsequent log line
|
|
19
|
+
- **Child loggers** — inherit parent context, fully isolated from each other
|
|
20
|
+
- **Per-call context** — attach extra fields to a single log call without mutating state
|
|
21
|
+
- **Severity Level filtering** — globally or per-mode thresholds to reduce noise
|
|
22
|
+
- **30 named colors** — `FIRO_COLORS` palette with IDE autocomplete, plus raw ANSI/256-color/truecolor support
|
|
23
|
+
- **Zero dependencies** — small and fast, no bloat, no native addons. Works on Node.js, Bun and Deno.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# for node.js, one of:
|
|
29
|
+
npm install @fend/firo
|
|
30
|
+
yarn add @fend/firo
|
|
31
|
+
pnpm add @fend/firo
|
|
32
|
+
npx jsr add @fend/firo
|
|
33
|
+
|
|
34
|
+
# or, for deno:
|
|
35
|
+
deno add jsr:@fend/firo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { createLogger } from '@fend/firo'
|
|
43
|
+
|
|
44
|
+
const log = createLogger()
|
|
45
|
+
|
|
46
|
+
// log() is shorthand for log.info()
|
|
47
|
+
log('Server started')
|
|
48
|
+
|
|
49
|
+
log.warn('Disk usage high', { used: '92%' })
|
|
50
|
+
log.error('Connection lost', new Error('ECONNREFUSED'))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Dev output:
|
|
54
|
+
```
|
|
55
|
+
[14:32:01.204] Server started
|
|
56
|
+
[14:32:01.205] [WARN] Disk usage high { used: '92%' }
|
|
57
|
+
[14:32:01.206] [ERROR] Connection lost Error: ECONNREFUSED
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Modes
|
|
61
|
+
|
|
62
|
+
### Dev (default)
|
|
63
|
+
|
|
64
|
+
Colored, human-readable. Errors go to `stderr`, everything else to `stdout`.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const log = createLogger({ mode: 'dev' })
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Prod
|
|
71
|
+
|
|
72
|
+
Structured NDJSON. Everything goes to `stdout` — let your infrastructure route it.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const log = createLogger({ mode: 'prod' })
|
|
76
|
+
|
|
77
|
+
log.info('Request handled', { status: 200 })
|
|
78
|
+
// {"timestamp":"2024-01-15T14:32:01.204Z","level":"info","message":"Request handled","data":{"status":200}}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Async mode (Prod only)
|
|
82
|
+
|
|
83
|
+
For high-load applications, you can enable asynchronous buffered output. This avoids blocking the event loop when writing to `stdout`, which is critical for maintaining low latency.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const log = createLogger({
|
|
87
|
+
mode: 'prod',
|
|
88
|
+
async: true // Enables non-blocking buffered output
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
When `async` is enabled, logs are queued and flushed when the stream is ready (handling backpressure). We also ensure all buffered logs are flushed synchronously if the process exits or crashes.
|
|
93
|
+
|
|
94
|
+
## Best practices
|
|
95
|
+
|
|
96
|
+
### AsyncLocalStorage (Traceability)
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { AsyncLocalStorage } from 'node:util'
|
|
102
|
+
import { createLogger } from '@fend/firo'
|
|
103
|
+
|
|
104
|
+
const logger = createLogger()
|
|
105
|
+
const storage = new AsyncLocalStorage()
|
|
106
|
+
|
|
107
|
+
// Middleware example
|
|
108
|
+
function middleware(req, res, next) {
|
|
109
|
+
const reqLog = logger.child({
|
|
110
|
+
requestId: req.headers['x-request-id'] || 'gen-123',
|
|
111
|
+
method: req.method
|
|
112
|
+
})
|
|
113
|
+
storage.run(reqLog, next)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Deeply nested function
|
|
117
|
+
function someService() {
|
|
118
|
+
const log = storage.getStore() ?? logger
|
|
119
|
+
log.info('Service action performed')
|
|
120
|
+
// Output: [requestId:gen-123] [method:GET] Service action performed
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Log levels
|
|
125
|
+
|
|
126
|
+
Four levels, in order: `debug` → `info` → `warn` → `error`.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
log.debug('Cache miss', { user: 42, requestId: 'req-123' })
|
|
130
|
+
log.info('Request received')
|
|
131
|
+
log.warn('Retry attempt', { n: 3 })
|
|
132
|
+
log.error('Unhandled exception', err)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Debug lines are dimmed in dev mode to reduce visual noise.
|
|
136
|
+
|
|
137
|
+
### Filtering
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
// Suppress debug in dev, keep everything in prod
|
|
141
|
+
const log = createLogger({
|
|
142
|
+
minLevelInDev: 'info',
|
|
143
|
+
minLevelInProd: 'warn',
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Or a single threshold for both modes
|
|
147
|
+
const log = createLogger({ minLevel: 'warn' })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Context
|
|
151
|
+
|
|
152
|
+
Attach persistent key/value pairs to a logger instance. They appear in every log line.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const log = createLogger()
|
|
156
|
+
|
|
157
|
+
log.addContext('service', 'auth')
|
|
158
|
+
log.addContext('env', 'production')
|
|
159
|
+
|
|
160
|
+
log.info('Started')
|
|
161
|
+
// dev: [14:32:01.204] [service:auth] [env:production] Started
|
|
162
|
+
// prod: {"level":"info","service":"auth","env":"production","message":"Started",...}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Context options
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// Hide the key, show only the value — useful for IDs
|
|
169
|
+
log.addContext({ key: 'userId', value: 'u-789', options: { omitKey: true } })
|
|
170
|
+
// renders as [u-789] instead of [userId:u-789]
|
|
171
|
+
|
|
172
|
+
// Pin a specific color (0–9)
|
|
173
|
+
log.addContext({ key: 'region', value: 'eu-west', options: { colorIndex: 3 } })
|
|
174
|
+
|
|
175
|
+
// Use any ANSI color — 256-color, truecolor, anything
|
|
176
|
+
log.addContext({ key: 'trace', value: 'abc', options: { color: '38;5;214' } }) // 256-color orange
|
|
177
|
+
log.addContext({ key: 'span', value: 'xyz', options: { color: '38;2;255;100;0' } }) // truecolor
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Remove context
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
log.removeFromContext('env')
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Read context
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const ctx = log.getContext() // ContextItem[]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Child loggers
|
|
193
|
+
|
|
194
|
+
Create a scoped logger that inherits the parent's context at the moment of creation. Parent and child are fully isolated — mutations on one do not affect the other.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const log = createLogger()
|
|
198
|
+
log.addContext('service', 'api')
|
|
199
|
+
|
|
200
|
+
const reqLog = log.child({ requestId: 'req-123', method: 'POST' })
|
|
201
|
+
reqLog.info('Request received')
|
|
202
|
+
// [service:api] [requestId:req-123] [method:POST] Request received
|
|
203
|
+
|
|
204
|
+
// Parent is unchanged
|
|
205
|
+
log.info('Still here')
|
|
206
|
+
// [service:api] Still here
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Children can be nested arbitrarily:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const txLog = reqLog.child({ txId: 'tx-999' })
|
|
213
|
+
txLog.info('Transaction committed')
|
|
214
|
+
// [service:api] [requestId:req-123] [method:POST] [txId:tx-999] Transaction committed
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Per-call context
|
|
218
|
+
|
|
219
|
+
Add context to a single log call without touching the logger's state:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
log.info('User action', payload, {
|
|
223
|
+
ctx: [{ key: 'userId', value: 42, options: { omitKey: true } }]
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Works on all log methods including `error`:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
log.error('Payment failed', err, {
|
|
231
|
+
ctx: [{ key: 'orderId', value: 7 }]
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Error signatures
|
|
236
|
+
|
|
237
|
+
`error()` accepts multiple call signatures:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
// Message only
|
|
241
|
+
log.error('Something went wrong')
|
|
242
|
+
|
|
243
|
+
// Message + Error object
|
|
244
|
+
log.error('Query failed', new Error('timeout'))
|
|
245
|
+
|
|
246
|
+
// Error object only
|
|
247
|
+
log.error(new Error('Unhandled'))
|
|
248
|
+
|
|
249
|
+
// Anything — will be coerced to Error
|
|
250
|
+
log.error(someUnknownThing)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Custom transport
|
|
254
|
+
|
|
255
|
+
Provide your own transport function to take full control of output:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import type { TransportFn } from '@fend/firo'
|
|
259
|
+
|
|
260
|
+
const myTransport: TransportFn = (level, context, msg, data, opts) => {
|
|
261
|
+
// level: 'debug' | 'info' | 'warn' | 'error'
|
|
262
|
+
// context: ContextItemWithOptions[]
|
|
263
|
+
// msg: string | Error | unknown
|
|
264
|
+
// data: Error | unknown
|
|
265
|
+
// opts: LogOptions | undefined
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const log = createLogger({ transport: myTransport })
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Dev transport options
|
|
272
|
+
|
|
273
|
+
Fine-tune the dev transport's timestamp format. For example, to remove seconds and milliseconds:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { createLogger } from '@fend/firo'
|
|
277
|
+
|
|
278
|
+
const log = createLogger({
|
|
279
|
+
devTransportConfig: {
|
|
280
|
+
timeOptions: {
|
|
281
|
+
hour: '2-digit',
|
|
282
|
+
minute: '2-digit',
|
|
283
|
+
second: undefined,
|
|
284
|
+
fractionalSecondDigits: undefined
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Color palette
|
|
291
|
+
|
|
292
|
+
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.
|
|
293
|
+
|
|
294
|
+

|
|
295
|
+
|
|
296
|
+
### How it works
|
|
297
|
+
|
|
298
|
+
By default, firo auto-assigns colors from 10 terminal-safe base colors using a hash of the context key. Similar keys like `user-1` and `user-2` land on different colors automatically.
|
|
299
|
+
|
|
300
|
+
But the real fun starts when you reach for `FIRO_COLORS` — a named palette of 30 colors with full IDE autocomplete:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { createLogger, FIRO_COLORS } from '@fend/firo'
|
|
304
|
+
|
|
305
|
+
const log = createLogger()
|
|
306
|
+
|
|
307
|
+
log.addContext('region', 'eu-west', { color: FIRO_COLORS.coral })
|
|
308
|
+
log.addContext('service', 'auth', { color: FIRO_COLORS.skyBlue })
|
|
309
|
+
log.addContext('env', 'staging', { color: FIRO_COLORS.lavender })
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
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`.
|
|
313
|
+
|
|
314
|
+
### Want even more variety?
|
|
315
|
+
|
|
316
|
+
You can also pass any raw ANSI code as a string — 256-color, truecolor, go wild:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
log.addContext('trace', 'abc', { color: '38;5;214' }) // 256-color
|
|
320
|
+
log.addContext('span', 'xyz', { color: '38;2;255;105;180' }) // truecolor pink
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Use all 30 colors for auto-hash
|
|
324
|
+
|
|
325
|
+
By default, auto-hash only picks from the 10 basic terminal-safe colors. If your terminal supports 256 colors (most modern terminals do), unleash the full palette:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const log = createLogger({ useAllColors: true })
|
|
329
|
+
|
|
330
|
+
// Now every context key auto-gets one of 30 distinct colors
|
|
331
|
+
log.addContext('service', 'api')
|
|
332
|
+
log.addContext('region', 'eu-west')
|
|
333
|
+
log.addContext('pod', 'web-3')
|
|
334
|
+
// Each badge is a different, beautiful color — no configuration needed
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Why not pino?
|
|
338
|
+
|
|
339
|
+
**Pino** is Italian for *Pine*. It's a great, sturdy tree, especially in production.
|
|
340
|
+
|
|
341
|
+
But sometimes you need to **Spruce** up your development experience.
|
|
342
|
+
|
|
343
|
+
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.
|
|
344
|
+
|
|
345
|
+
**firo** is the **Fir** of logging: elegant, refined, and designed to look great in your terminal, while remaining a rock-solid performer in the production forest.
|
|
346
|
+
|
|
347
|
+
- **Context first:** Badges like `[requestId:abc]` stay on the same line — no messy JSON trees.
|
|
348
|
+
- **Message first:** `log.info('message', data)` — because why you're looking at the log is more important than the supporting data.
|
|
349
|
+
- **Compact by default:** Objects are printed inline, one line, not twenty.
|
|
350
|
+
- **Visual hierarchy:** Debug lines are dimmed; high-signal logs stay readable.
|
|
351
|
+
- **Zero config:** Beautiful output from the first second.
|
|
352
|
+
|
|
353
|
+
In prod it emits clean NDJSON, same as pino. Your log aggregator won't know the difference.
|
|
354
|
+
|
|
355
|
+
## API reference
|
|
356
|
+
|
|
357
|
+
### Logger methods
|
|
358
|
+
|
|
359
|
+
| Method | Description |
|
|
360
|
+
|---|---|
|
|
361
|
+
| `debug(msg, data?, opts?)` | Debug-level log (dimmed in dev) |
|
|
362
|
+
| `info(msg, data?, opts?)` | Info-level log |
|
|
363
|
+
| `warn(msg, data?, opts?)` | Warning |
|
|
364
|
+
| `error(msg, err?, opts?)` | Error — also accepts `error(err)` |
|
|
365
|
+
| `child(ctx)` | Create a child logger with additional context |
|
|
366
|
+
| `addContext(key, value, opts?)` | Add a context entry |
|
|
367
|
+
| `addContext(item)` | Add a context entry (object form) |
|
|
368
|
+
| `removeFromContext(key)` | Remove a context entry by key |
|
|
369
|
+
| `getContext()` | Return the current context array |
|
|
370
|
+
|
|
371
|
+
### `createLogger(config?)`
|
|
372
|
+
|
|
373
|
+
| Option | Type | Default | Description |
|
|
374
|
+
|---|---|---|---|
|
|
375
|
+
| `mode` | `'dev' \| 'prod'` | `'dev'` | Selects the built-in transport |
|
|
376
|
+
| `minLevel` | `LogLevel` | `'debug'` | Minimum level for both modes |
|
|
377
|
+
| `minLevelInDev` | `LogLevel` | — | Overrides `minLevel` in dev mode |
|
|
378
|
+
| `minLevelInProd` | `LogLevel` | — | Overrides `minLevel` in prod mode |
|
|
379
|
+
| `transport` | `TransportFn` | — | Custom transport, overrides `mode` |
|
|
380
|
+
| `devTransportConfig` | `DevTransportConfig` | — | Options for the built-in dev transport |
|
|
381
|
+
| `async` | `boolean` | `false` | Enable non-blocking output (Prod mode only) |
|
|
382
|
+
| `useAllColors` | `boolean` | `false` | Use all 30 palette colors for auto-hash (instead of 10 safe) |
|
|
383
|
+
|
|
384
|
+
## License
|
|
385
|
+
|
|
386
|
+
MIT License
|