@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 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
+ ![firo output](image.png)
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
+ ![firo color palette](color_madness.png)
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