@fend/firo 0.0.4 → 0.0.5

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
@@ -54,9 +54,9 @@ deno add jsr:@fend/firo
54
54
  ## Quick start
55
55
 
56
56
  ```ts
57
- import { createLogger } from '@fend/firo'
57
+ import { createFiro } from '@fend/firo'
58
58
 
59
- const log = createLogger()
59
+ const log = createFiro()
60
60
 
61
61
  // log() is shorthand for log.info()
62
62
  log('Server started')
@@ -79,7 +79,7 @@ Dev output:
79
79
  Colored, human-readable. Errors go to `stderr`, everything else to `stdout`.
80
80
 
81
81
  ```ts
82
- const log = createLogger({ mode: 'dev' })
82
+ const log = createFiro({ mode: 'dev' })
83
83
  ```
84
84
 
85
85
  ### Prod
@@ -87,7 +87,7 @@ const log = createLogger({ mode: 'dev' })
87
87
  Structured NDJSON. Everything goes to `stdout` — let your infrastructure route it.
88
88
 
89
89
  ```ts
90
- const log = createLogger({ mode: 'prod' })
90
+ const log = createFiro({ mode: 'prod' })
91
91
 
92
92
  log.info('Request handled', { status: 200 })
93
93
  // {"timestamp":"2024-01-15T14:32:01.204Z","level":"info","message":"Request handled","data":{"status":200}}
@@ -101,9 +101,9 @@ The best way to use **firo** in web frameworks is to store a child logger in `As
101
101
 
102
102
  ```ts
103
103
  import { AsyncLocalStorage } from 'node:util'
104
- import { createLogger } from '@fend/firo'
104
+ import { createFiro } from '@fend/firo'
105
105
 
106
- const logger = createLogger()
106
+ const logger = createFiro()
107
107
  const storage = new AsyncLocalStorage()
108
108
 
109
109
  // Middleware example
@@ -140,13 +140,13 @@ Debug lines are dimmed in dev mode to reduce visual noise.
140
140
 
141
141
  ```ts
142
142
  // Suppress debug in dev, keep everything in prod
143
- const log = createLogger({
143
+ const log = createFiro({
144
144
  minLevelInDev: 'info',
145
145
  minLevelInProd: 'warn',
146
146
  })
147
147
 
148
148
  // Or a single threshold for both modes
149
- const log = createLogger({ minLevel: 'warn' })
149
+ const log = createFiro({ minLevel: 'warn' })
150
150
  ```
151
151
 
152
152
  ## Context
@@ -154,7 +154,7 @@ const log = createLogger({ minLevel: 'warn' })
154
154
  Attach persistent key/value pairs to a logger instance. They appear in every log line.
155
155
 
156
156
  ```ts
157
- const log = createLogger()
157
+ const log = createFiro()
158
158
 
159
159
  log.addContext('service', 'auth')
160
160
  log.addContext('env', 'production')
@@ -171,7 +171,7 @@ log.info('Started')
171
171
  log.addContext({ key: 'userId', value: 'u-789', omitKey: true })
172
172
  // renders as [u-789] instead of [userId:u-789]
173
173
 
174
- // Pin a specific color (0–9)
174
+ // Pin a specific color by palette index (0–29)
175
175
  log.addContext({ key: 'region', value: 'west', colorIndex: 3 })
176
176
 
177
177
  // Use any ANSI color — 256-color, truecolor, anything
@@ -196,7 +196,7 @@ const ctx = log.getContext() // ContextItem[]
196
196
  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.
197
197
 
198
198
  ```ts
199
- const log = createLogger()
199
+ const log = createFiro()
200
200
  log.addContext('service', 'api')
201
201
 
202
202
  const reqLog = log.child({ requestId: 'req-123', method: 'POST' })
@@ -267,7 +267,22 @@ const myTransport: TransportFn = (level, context, msg, data, opts) => {
267
267
  // opts: LogOptions | undefined
268
268
  }
269
269
 
270
- const log = createLogger({ transport: myTransport })
270
+ const log = createFiro({ transport: myTransport })
271
+ ```
272
+
273
+ ### FiroUtils
274
+
275
+ `FiroUtils` exposes helper functions useful for building custom transports:
276
+
277
+ ```ts
278
+ import { FiroUtils } from '@fend/firo'
279
+
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)
271
286
  ```
272
287
 
273
288
  ## Dev transport options
@@ -275,9 +290,9 @@ const log = createLogger({ transport: myTransport })
275
290
  Fine-tune the dev transport's timestamp format. For example, to remove seconds and milliseconds:
276
291
 
277
292
  ```ts
278
- import { createLogger } from '@fend/firo'
293
+ import { createFiro } from '@fend/firo'
279
294
 
280
- const log = createLogger({
295
+ const log = createFiro({
281
296
  devTransportConfig: {
282
297
  timeOptions: {
283
298
  hour: '2-digit',
@@ -289,6 +304,45 @@ const log = createLogger({
289
304
  })
290
305
  ```
291
306
 
307
+ ## Prod transport options
308
+
309
+ Configure the prod (JSON) transport's timestamp format:
310
+
311
+ ```ts
312
+ // Epoch ms (faster, same as pino)
313
+ const log = createFiro({
314
+ mode: 'prod',
315
+ prodTransportConfig: { timestamp: 'epoch' }
316
+ })
317
+ // {"timestamp":1711100000000,"level":"info","message":"hello"}
318
+
319
+ // ISO 8601 (default, human-readable)
320
+ const log = createFiro({ mode: 'prod' })
321
+ // {"timestamp":"2024-01-15T14:32:01.204Z","level":"info","message":"hello"}
322
+ ```
323
+
324
+ ### Custom destination
325
+
326
+ By default, prod transport writes to `process.stdout`. You can redirect output to any object with a `.write(string)` method:
327
+
328
+ ```ts
329
+ import { createFiro } from '@fend/firo'
330
+ import { createWriteStream } from 'node:fs'
331
+
332
+ // Write to a file
333
+ const log = createFiro({
334
+ mode: 'prod',
335
+ prodTransportConfig: { dest: createWriteStream('/var/log/app.log') }
336
+ })
337
+
338
+ // Use SonicBoom for async buffered writes (same as pino)
339
+ import SonicBoom from 'sonic-boom'
340
+ const log = createFiro({
341
+ mode: 'prod',
342
+ prodTransportConfig: { dest: new SonicBoom({ fd: 1 }) }
343
+ })
344
+ ```
345
+
292
346
  ## Color palette
293
347
 
294
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.
@@ -297,14 +351,14 @@ Most loggers give you monochrome walls of text. firo gives you **30 handpicked c
297
351
 
298
352
  ### How it works
299
353
 
300
- 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.
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.
301
355
 
302
- But the real fun starts when you reach for `FIRO_COLORS` — a named palette of 30 colors with full IDE autocomplete:
356
+ You can also pin a specific color using `FIRO_COLORS` — a named palette with full IDE autocomplete:
303
357
 
304
358
  ```ts
305
- import { createLogger, FIRO_COLORS } from '@fend/firo'
359
+ import { createFiro, FIRO_COLORS } from '@fend/firo'
306
360
 
307
- const log = createLogger()
361
+ const log = createFiro()
308
362
 
309
363
  log.addContext('region', { value: 'west', color: FIRO_COLORS.coral })
310
364
  log.addContext('service', { value: 'auth', color: FIRO_COLORS.skyBlue })
@@ -322,18 +376,12 @@ log.addContext('trace', { value: 'abc', color: '38;5;214' }) // 256-colo
322
376
  log.addContext('span', { value: 'xyz', color: '38;2;255;105;180' }) // truecolor pink
323
377
  ```
324
378
 
325
- ### Use all 30 colors for auto-hash
379
+ ### Restrict to safe colors
326
380
 
327
- 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:
381
+ If your terminal doesn't support 256 colors, you can restrict auto-hash to 10 basic terminal-safe colors:
328
382
 
329
383
  ```ts
330
- const log = createLogger({ useAllColors: true })
331
-
332
- // Now every context key auto-gets one of 30 distinct colors
333
- log.addContext('service', 'api')
334
- log.addContext('region', 'west')
335
- log.addContext('pod', 'web-3')
336
- // Each badge is a different, beautiful color — no configuration needed
384
+ const log = createFiro({ useAllColors: false })
337
385
  ```
338
386
 
339
387
  ## Why not pino?
@@ -354,6 +402,35 @@ The problem with pino is development. Its default output is raw JSON — one gia
354
402
 
355
403
  In prod it emits clean NDJSON, same as pino. Your log aggregator won't know the difference.
356
404
 
405
+ ## Performance
406
+
407
+ Prod mode (NDJSON) with `timestamp: 'epoch'`, Apple M1, Node.js 25. Average time per 100K log lines (lower is better):
408
+
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 |
417
+
418
+ For comparison, here's [pino's own benchmark](https://github.com/pinojs/pino/blob/main/docs/benchmarks.md) (100K `"hello world"` logs):
419
+
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 |
429
+
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.
431
+
432
+ Run it yourself: `pnpm bench`
433
+
357
434
  ## API reference
358
435
 
359
436
  ### Logger methods
@@ -369,8 +446,9 @@ In prod it emits clean NDJSON, same as pino. Your log aggregator won't know the
369
446
  | `addContext(item)` | Add a context entry (object form) |
370
447
  | `removeFromContext(key)` | Remove a context entry by key |
371
448
  | `getContext()` | Return the current context array |
449
+ | `hasInContext(key)` | Check if a context key exists |
372
450
 
373
- ### `createLogger(config?)`
451
+ ### `createFiro(config?)`
374
452
 
375
453
  | Option | Type | Default | Description |
376
454
  |---|---|---|---|
@@ -380,7 +458,8 @@ In prod it emits clean NDJSON, same as pino. Your log aggregator won't know the
380
458
  | `minLevelInProd` | `LogLevel` | — | Overrides `minLevel` in prod mode |
381
459
  | `transport` | `TransportFn` | — | Custom transport, overrides `mode` |
382
460
  | `devTransportConfig` | `DevTransportConfig` | — | Options for the built-in dev transport |
383
- | `useAllColors` | `boolean` | `false` | Use all 30 palette colors for auto-hash (instead of 10 safe) |
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) |
384
463
 
385
464
  ## License
386
465
 
package/dist/index.cjs CHANGED
@@ -31,13 +31,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  FIRO_COLORS: () => FIRO_COLORS,
34
+ FiroUtils: () => utils_exports,
34
35
  createDevTransport: () => createDevTransport,
35
- createJsonTransport: () => createJsonTransport,
36
- createLogger: () => createLogger
36
+ createFiro: () => createFiro,
37
+ createProdTransport: () => createProdTransport
37
38
  });
38
39
  module.exports = __toCommonJS(index_exports);
39
40
 
40
41
  // src/utils.ts
42
+ var utils_exports = {};
43
+ __export(utils_exports, {
44
+ FIRO_COLORS: () => FIRO_COLORS,
45
+ LOG_LEVELS: () => LOG_LEVELS,
46
+ colorize: () => colorize,
47
+ colorizeLevel: () => colorizeLevel,
48
+ getColorIndex: () => getColorIndex,
49
+ jsonReplacer: () => jsonReplacer,
50
+ safeStringify: () => safeStringify,
51
+ serializeError: () => serializeError,
52
+ wrapToError: () => wrapToError
53
+ });
54
+ var import_node_util = require("util");
41
55
  var LOG_LEVELS = {
42
56
  debug: 20,
43
57
  info: 30,
@@ -92,6 +106,29 @@ var colorize = (text, colorIndex, color) => {
92
106
  const code = color ?? (COLORS_LIST[colorIndex] || COLORS_LIST[colorIndex % SAFE_COLORS_COUNT]);
93
107
  return `\x1B[${code}m${text}\x1B[0m`;
94
108
  };
109
+ var jsonReplacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
110
+ var safeStringify = (obj) => {
111
+ try {
112
+ return JSON.stringify(obj, jsonReplacer);
113
+ } catch {
114
+ return (0, import_node_util.inspect)(obj);
115
+ }
116
+ };
117
+ var wrapToError = (obj) => {
118
+ if (obj instanceof Error) return obj;
119
+ return new Error(
120
+ typeof obj === "object" && obj !== null ? safeStringify(obj) : String(obj)
121
+ );
122
+ };
123
+ var serializeError = (_err) => {
124
+ const err = wrapToError(_err);
125
+ return {
126
+ message: err.message,
127
+ stack: err.stack,
128
+ name: err.name,
129
+ ...err
130
+ };
131
+ };
95
132
  var colorizeLevel = (level, text) => {
96
133
  if (level === "info") return text;
97
134
  switch (level) {
@@ -109,8 +146,8 @@ var colorizeLevel = (level, text) => {
109
146
  }
110
147
  };
111
148
 
112
- // src/transports.ts
113
- var import_node_util = require("util");
149
+ // src/transport_dev.ts
150
+ var import_node_util2 = require("util");
114
151
  var import_node_process = __toESM(require("process"), 1);
115
152
  var createDevTransport = (config = {}) => {
116
153
  const locale = config.locale ?? void 0;
@@ -138,9 +175,9 @@ var createDevTransport = (config = {}) => {
138
175
  let dataStr = "";
139
176
  if (data !== void 0) {
140
177
  const inspectOptions = opts?.pretty ? { compact: false, colors: true, depth: null } : { compact: true, breakLength: Infinity, colors: true, depth: null };
141
- dataStr = (0, import_node_util.inspect)(data, inspectOptions);
178
+ dataStr = (0, import_node_util2.inspect)(data, inspectOptions);
142
179
  }
143
- const msgStr = typeof msg === "object" && msg !== null ? (0, import_node_util.inspect)(msg, { colors: true, compact: true, breakLength: Infinity }) : String(msg);
180
+ const msgStr = typeof msg === "object" && msg !== null ? (0, import_node_util2.inspect)(msg, { colors: true, compact: true, breakLength: Infinity }) : String(msg);
144
181
  const parts = [
145
182
  `[${timestamp}]`,
146
183
  // Normal (not dimmed)
@@ -154,36 +191,17 @@ var createDevTransport = (config = {}) => {
154
191
  };
155
192
  return transport;
156
193
  };
157
- var jsonReplacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
158
- var safeStringify = (obj) => {
159
- try {
160
- return JSON.stringify(obj, jsonReplacer);
161
- } catch {
162
- return (0, import_node_util.inspect)(obj);
163
- }
164
- };
165
- var wrapToError = (obj) => {
166
- if (obj instanceof Error) return obj;
167
- return new Error(
168
- typeof obj === "object" && obj !== null ? safeStringify(obj) : String(obj)
169
- );
170
- };
171
- var serializeError = (_err) => {
172
- const err = wrapToError(_err);
173
- return {
174
- message: err.message,
175
- stack: err.stack,
176
- name: err.name,
177
- ...err
178
- };
179
- };
180
- var buildRecord = (level, context, msg, data) => {
194
+
195
+ // src/transport_prod.ts
196
+ var import_node_util3 = require("util");
197
+ var import_node_process2 = __toESM(require("process"), 1);
198
+ var buildRecord = (level, context, msg, getTimestamp, data) => {
181
199
  const contextObj = context.reduce((acc, item) => {
182
200
  acc[item.key] = item.value;
183
201
  return acc;
184
202
  }, {});
185
203
  const logRecord = {
186
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
204
+ timestamp: getTimestamp(),
187
205
  level,
188
206
  ...contextObj
189
207
  };
@@ -207,14 +225,16 @@ var buildRecord = (level, context, msg, data) => {
207
225
  }
208
226
  return logRecord;
209
227
  };
210
- var createJsonTransport = () => {
228
+ var createProdTransport = (config = {}) => {
229
+ const getTimestamp = config.timestamp === "epoch" ? () => Date.now() : () => (/* @__PURE__ */ new Date()).toISOString();
230
+ const dest = config.dest ?? import_node_process2.default.stdout;
211
231
  return (level, context, msg, data) => {
212
- const record = buildRecord(level, context, msg, data);
232
+ const record = buildRecord(level, context, msg, getTimestamp, data);
213
233
  let line;
214
234
  try {
215
235
  line = JSON.stringify(record, jsonReplacer) + "\n";
216
236
  } catch {
217
- if (record.data) record.data = (0, import_node_util.inspect)(record.data);
237
+ if (record.data) record.data = (0, import_node_util3.inspect)(record.data);
218
238
  try {
219
239
  line = JSON.stringify(record, jsonReplacer) + "\n";
220
240
  } catch {
@@ -226,28 +246,25 @@ var createJsonTransport = () => {
226
246
  }) + "\n";
227
247
  }
228
248
  }
229
- import_node_process.default.stdout.write(line);
249
+ dest.write(line);
230
250
  };
231
251
  };
232
252
 
233
253
  // src/index.ts
234
- var fillContextItem = (item, useAllColors = false) => {
235
- return {
254
+ var createFiro = (config = {}, parentContext = []) => {
255
+ const useAllColors = config.useAllColors ?? true;
256
+ const fill = (item) => ({
236
257
  ...item,
237
258
  colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key, useAllColors),
238
259
  color: item.color,
239
260
  omitKey: item.omitKey ?? false
240
- };
241
- };
242
- var createLoggerInternal = (config, parentContext) => {
243
- const useAllColors = config.useAllColors ?? false;
244
- const fill = (item) => fillContextItem(item, useAllColors);
261
+ });
245
262
  const appendContextWithInvokeContext = (context2, invokeContext) => {
246
263
  if (!invokeContext || invokeContext.length === 0) return context2;
247
264
  return [...context2, ...invokeContext.map(fill)];
248
265
  };
249
266
  const context = [...parentContext.map(fill)];
250
- const transport = config.transport ?? (config.mode === "prod" ? createJsonTransport() : createDevTransport(config.devTransportConfig));
267
+ const transport = config.transport ?? (config.mode === "prod" ? createProdTransport(config.prodTransportConfig) : createDevTransport(config.devTransportConfig));
251
268
  const minLevelName = config.mode === "prod" ? config.minLevelInProd ?? config.minLevel : config.minLevelInDev ?? config.minLevel;
252
269
  const minLevel = LOG_LEVELS[minLevelName ?? "debug"];
253
270
  const getContext = () => context;
@@ -278,7 +295,7 @@ var createLoggerInternal = (config, parentContext) => {
278
295
  }
279
296
  return { key, value, colorIndex: getColorIndex(key, useAllColors) };
280
297
  });
281
- return createLoggerInternal({ transport, minLevel: minLevelName, useAllColors }, [...context, ...newItems]);
298
+ return createFiro({ transport, minLevel: minLevelName, useAllColors }, [...context, ...newItems]);
282
299
  };
283
300
  const debug = (msg, data, opts) => {
284
301
  if (minLevel > LOG_LEVELS.debug) return;
@@ -311,13 +328,11 @@ var createLoggerInternal = (config, parentContext) => {
311
328
  removeFromContext: removeKeyFromContext
312
329
  });
313
330
  };
314
- var createLogger = (config = {}) => {
315
- return createLoggerInternal(config, []);
316
- };
317
331
  // Annotate the CommonJS export names for ESM import in node:
318
332
  0 && (module.exports = {
319
333
  FIRO_COLORS,
334
+ FiroUtils,
320
335
  createDevTransport,
321
- createJsonTransport,
322
- createLogger
336
+ createFiro,
337
+ createProdTransport
323
338
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,11 @@
1
1
  /** Available log severity levels. */
2
2
  type LogLevel = 'debug' | 'info' | 'warn' | 'error';
3
+ declare const LOG_LEVELS: {
4
+ readonly debug: 20;
5
+ readonly info: 30;
6
+ readonly warn: 40;
7
+ readonly error: 50;
8
+ };
3
9
  /** Primitive types allowed as context values. */
4
10
  type ContextValue = string | number | boolean | null | undefined;
5
11
  /** Options to customize how a context item is rendered. */
@@ -68,6 +74,34 @@ declare const FIRO_COLORS: {
68
74
  readonly tangerine: "38;5;208";
69
75
  readonly periwinkle: "38;5;147";
70
76
  };
77
+ declare const getColorIndex: (str: string, useAllColors?: boolean) => number;
78
+ declare const colorize: (text: string, colorIndex: number, color?: string) => string;
79
+ declare const jsonReplacer: (_key: string, value: unknown) => unknown;
80
+ declare const safeStringify: (obj: unknown) => string;
81
+ declare const wrapToError: (obj: unknown) => Error;
82
+ declare const serializeError: (_err: unknown) => any;
83
+ declare const colorizeLevel: (level: LogLevel, text: string) => string;
84
+
85
+ type utils_ContextExtension = ContextExtension;
86
+ type utils_ContextItem = ContextItem;
87
+ type utils_ContextItemWithOptions = ContextItemWithOptions;
88
+ type utils_ContextOptions = ContextOptions;
89
+ type utils_ContextValue = ContextValue;
90
+ declare const utils_FIRO_COLORS: typeof FIRO_COLORS;
91
+ declare const utils_LOG_LEVELS: typeof LOG_LEVELS;
92
+ type utils_LogLevel = LogLevel;
93
+ type utils_LogOptions = LogOptions;
94
+ type utils_TransportFn = TransportFn;
95
+ declare const utils_colorize: typeof colorize;
96
+ declare const utils_colorizeLevel: typeof colorizeLevel;
97
+ declare const utils_getColorIndex: typeof getColorIndex;
98
+ declare const utils_jsonReplacer: typeof jsonReplacer;
99
+ declare const utils_safeStringify: typeof safeStringify;
100
+ declare const utils_serializeError: typeof serializeError;
101
+ declare const utils_wrapToError: typeof wrapToError;
102
+ 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 };
104
+ }
71
105
 
72
106
  /**
73
107
  * Configuration options for the development transport.
@@ -86,13 +120,23 @@ type DevTransportConfig = {
86
120
  * @returns A `TransportFn` that writes to the console.
87
121
  */
88
122
  declare const createDevTransport: (config?: DevTransportConfig) => TransportFn;
123
+
124
+ type TimestampFormat = 'iso' | 'epoch';
125
+ type ProdTransportConfig = {
126
+ /** Timestamp format: 'iso' (default) for ISO 8601 string, 'epoch' for ms since Unix epoch. */
127
+ timestamp?: TimestampFormat;
128
+ /** Output destination. Any object with a `.write(string)` method. Defaults to `process.stdout`. */
129
+ dest?: {
130
+ write(s: string): unknown;
131
+ };
132
+ };
89
133
  /**
90
134
  * Creates a built-in transport optimized for production.
91
135
  * Emits strictly structured NDJSON (Newline Delimited JSON) to stdout.
92
136
  *
93
137
  * @returns A `TransportFn` that writes JSON to standard output.
94
138
  */
95
- declare const createJsonTransport: () => TransportFn;
139
+ declare const createProdTransport: (config?: ProdTransportConfig) => TransportFn;
96
140
 
97
141
  /**
98
142
  * Configuration options for creating a logger instance.
@@ -110,11 +154,13 @@ type LoggerConfig = {
110
154
  transport?: TransportFn;
111
155
  /** Options for fine-tuning the built-in development transport (e.g. timestamp format). */
112
156
  devTransportConfig?: DevTransportConfig;
113
- /** Use the full extended color palette (30 colors including 256-color) for auto-assigned context badges. Defaults to false (safe 10-color palette). */
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. */
114
160
  useAllColors?: boolean;
115
161
  };
116
162
  /**
117
- * The logger instance returned by `createLogger`.
163
+ * The logger instance returned by `createFiro`.
118
164
  * It is a callable object: calling `log(msg)` is shorthand for `log.info(msg)`.
119
165
  */
120
166
  interface Firo {
@@ -153,6 +199,6 @@ interface Firo {
153
199
  * @param config Optional configuration for log levels, mode, and transports.
154
200
  * @returns A fully configured `Firo` instance.
155
201
  */
156
- declare const createLogger: (config?: LoggerConfig) => Firo;
202
+ declare const createFiro: (config?: LoggerConfig, parentContext?: ContextItem[]) => Firo;
157
203
 
158
- export { type ContextExtension, type ContextItem, type ContextItemWithOptions, type ContextOptions, type ContextValue, type DevTransportConfig, FIRO_COLORS, type Firo, type LogLevel, type LogOptions, type LoggerConfig, type TransportFn, createDevTransport, createJsonTransport, createLogger };
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 };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  /** Available log severity levels. */
2
2
  type LogLevel = 'debug' | 'info' | 'warn' | 'error';
3
+ declare const LOG_LEVELS: {
4
+ readonly debug: 20;
5
+ readonly info: 30;
6
+ readonly warn: 40;
7
+ readonly error: 50;
8
+ };
3
9
  /** Primitive types allowed as context values. */
4
10
  type ContextValue = string | number | boolean | null | undefined;
5
11
  /** Options to customize how a context item is rendered. */
@@ -68,6 +74,34 @@ declare const FIRO_COLORS: {
68
74
  readonly tangerine: "38;5;208";
69
75
  readonly periwinkle: "38;5;147";
70
76
  };
77
+ declare const getColorIndex: (str: string, useAllColors?: boolean) => number;
78
+ declare const colorize: (text: string, colorIndex: number, color?: string) => string;
79
+ declare const jsonReplacer: (_key: string, value: unknown) => unknown;
80
+ declare const safeStringify: (obj: unknown) => string;
81
+ declare const wrapToError: (obj: unknown) => Error;
82
+ declare const serializeError: (_err: unknown) => any;
83
+ declare const colorizeLevel: (level: LogLevel, text: string) => string;
84
+
85
+ type utils_ContextExtension = ContextExtension;
86
+ type utils_ContextItem = ContextItem;
87
+ type utils_ContextItemWithOptions = ContextItemWithOptions;
88
+ type utils_ContextOptions = ContextOptions;
89
+ type utils_ContextValue = ContextValue;
90
+ declare const utils_FIRO_COLORS: typeof FIRO_COLORS;
91
+ declare const utils_LOG_LEVELS: typeof LOG_LEVELS;
92
+ type utils_LogLevel = LogLevel;
93
+ type utils_LogOptions = LogOptions;
94
+ type utils_TransportFn = TransportFn;
95
+ declare const utils_colorize: typeof colorize;
96
+ declare const utils_colorizeLevel: typeof colorizeLevel;
97
+ declare const utils_getColorIndex: typeof getColorIndex;
98
+ declare const utils_jsonReplacer: typeof jsonReplacer;
99
+ declare const utils_safeStringify: typeof safeStringify;
100
+ declare const utils_serializeError: typeof serializeError;
101
+ declare const utils_wrapToError: typeof wrapToError;
102
+ 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 };
104
+ }
71
105
 
72
106
  /**
73
107
  * Configuration options for the development transport.
@@ -86,13 +120,23 @@ type DevTransportConfig = {
86
120
  * @returns A `TransportFn` that writes to the console.
87
121
  */
88
122
  declare const createDevTransport: (config?: DevTransportConfig) => TransportFn;
123
+
124
+ type TimestampFormat = 'iso' | 'epoch';
125
+ type ProdTransportConfig = {
126
+ /** Timestamp format: 'iso' (default) for ISO 8601 string, 'epoch' for ms since Unix epoch. */
127
+ timestamp?: TimestampFormat;
128
+ /** Output destination. Any object with a `.write(string)` method. Defaults to `process.stdout`. */
129
+ dest?: {
130
+ write(s: string): unknown;
131
+ };
132
+ };
89
133
  /**
90
134
  * Creates a built-in transport optimized for production.
91
135
  * Emits strictly structured NDJSON (Newline Delimited JSON) to stdout.
92
136
  *
93
137
  * @returns A `TransportFn` that writes JSON to standard output.
94
138
  */
95
- declare const createJsonTransport: () => TransportFn;
139
+ declare const createProdTransport: (config?: ProdTransportConfig) => TransportFn;
96
140
 
97
141
  /**
98
142
  * Configuration options for creating a logger instance.
@@ -110,11 +154,13 @@ type LoggerConfig = {
110
154
  transport?: TransportFn;
111
155
  /** Options for fine-tuning the built-in development transport (e.g. timestamp format). */
112
156
  devTransportConfig?: DevTransportConfig;
113
- /** Use the full extended color palette (30 colors including 256-color) for auto-assigned context badges. Defaults to false (safe 10-color palette). */
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. */
114
160
  useAllColors?: boolean;
115
161
  };
116
162
  /**
117
- * The logger instance returned by `createLogger`.
163
+ * The logger instance returned by `createFiro`.
118
164
  * It is a callable object: calling `log(msg)` is shorthand for `log.info(msg)`.
119
165
  */
120
166
  interface Firo {
@@ -153,6 +199,6 @@ interface Firo {
153
199
  * @param config Optional configuration for log levels, mode, and transports.
154
200
  * @returns A fully configured `Firo` instance.
155
201
  */
156
- declare const createLogger: (config?: LoggerConfig) => Firo;
202
+ declare const createFiro: (config?: LoggerConfig, parentContext?: ContextItem[]) => Firo;
157
203
 
158
- export { type ContextExtension, type ContextItem, type ContextItemWithOptions, type ContextOptions, type ContextValue, type DevTransportConfig, FIRO_COLORS, type Firo, type LogLevel, type LogOptions, type LoggerConfig, type TransportFn, createDevTransport, createJsonTransport, createLogger };
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 };
package/dist/index.js CHANGED
@@ -1,4 +1,23 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
1
7
  // src/utils.ts
8
+ var utils_exports = {};
9
+ __export(utils_exports, {
10
+ FIRO_COLORS: () => FIRO_COLORS,
11
+ LOG_LEVELS: () => LOG_LEVELS,
12
+ colorize: () => colorize,
13
+ colorizeLevel: () => colorizeLevel,
14
+ getColorIndex: () => getColorIndex,
15
+ jsonReplacer: () => jsonReplacer,
16
+ safeStringify: () => safeStringify,
17
+ serializeError: () => serializeError,
18
+ wrapToError: () => wrapToError
19
+ });
20
+ import { inspect } from "util";
2
21
  var LOG_LEVELS = {
3
22
  debug: 20,
4
23
  info: 30,
@@ -53,6 +72,29 @@ var colorize = (text, colorIndex, color) => {
53
72
  const code = color ?? (COLORS_LIST[colorIndex] || COLORS_LIST[colorIndex % SAFE_COLORS_COUNT]);
54
73
  return `\x1B[${code}m${text}\x1B[0m`;
55
74
  };
75
+ var jsonReplacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
76
+ var safeStringify = (obj) => {
77
+ try {
78
+ return JSON.stringify(obj, jsonReplacer);
79
+ } catch {
80
+ return inspect(obj);
81
+ }
82
+ };
83
+ var wrapToError = (obj) => {
84
+ if (obj instanceof Error) return obj;
85
+ return new Error(
86
+ typeof obj === "object" && obj !== null ? safeStringify(obj) : String(obj)
87
+ );
88
+ };
89
+ var serializeError = (_err) => {
90
+ const err = wrapToError(_err);
91
+ return {
92
+ message: err.message,
93
+ stack: err.stack,
94
+ name: err.name,
95
+ ...err
96
+ };
97
+ };
56
98
  var colorizeLevel = (level, text) => {
57
99
  if (level === "info") return text;
58
100
  switch (level) {
@@ -70,8 +112,8 @@ var colorizeLevel = (level, text) => {
70
112
  }
71
113
  };
72
114
 
73
- // src/transports.ts
74
- import { inspect } from "util";
115
+ // src/transport_dev.ts
116
+ import { inspect as inspect2 } from "util";
75
117
  import process from "process";
76
118
  var createDevTransport = (config = {}) => {
77
119
  const locale = config.locale ?? void 0;
@@ -99,9 +141,9 @@ var createDevTransport = (config = {}) => {
99
141
  let dataStr = "";
100
142
  if (data !== void 0) {
101
143
  const inspectOptions = opts?.pretty ? { compact: false, colors: true, depth: null } : { compact: true, breakLength: Infinity, colors: true, depth: null };
102
- dataStr = inspect(data, inspectOptions);
144
+ dataStr = inspect2(data, inspectOptions);
103
145
  }
104
- const msgStr = typeof msg === "object" && msg !== null ? inspect(msg, { colors: true, compact: true, breakLength: Infinity }) : String(msg);
146
+ const msgStr = typeof msg === "object" && msg !== null ? inspect2(msg, { colors: true, compact: true, breakLength: Infinity }) : String(msg);
105
147
  const parts = [
106
148
  `[${timestamp}]`,
107
149
  // Normal (not dimmed)
@@ -115,36 +157,17 @@ var createDevTransport = (config = {}) => {
115
157
  };
116
158
  return transport;
117
159
  };
118
- var jsonReplacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
119
- var safeStringify = (obj) => {
120
- try {
121
- return JSON.stringify(obj, jsonReplacer);
122
- } catch {
123
- return inspect(obj);
124
- }
125
- };
126
- var wrapToError = (obj) => {
127
- if (obj instanceof Error) return obj;
128
- return new Error(
129
- typeof obj === "object" && obj !== null ? safeStringify(obj) : String(obj)
130
- );
131
- };
132
- var serializeError = (_err) => {
133
- const err = wrapToError(_err);
134
- return {
135
- message: err.message,
136
- stack: err.stack,
137
- name: err.name,
138
- ...err
139
- };
140
- };
141
- var buildRecord = (level, context, msg, data) => {
160
+
161
+ // src/transport_prod.ts
162
+ import { inspect as inspect3 } from "util";
163
+ import process2 from "process";
164
+ var buildRecord = (level, context, msg, getTimestamp, data) => {
142
165
  const contextObj = context.reduce((acc, item) => {
143
166
  acc[item.key] = item.value;
144
167
  return acc;
145
168
  }, {});
146
169
  const logRecord = {
147
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
170
+ timestamp: getTimestamp(),
148
171
  level,
149
172
  ...contextObj
150
173
  };
@@ -168,14 +191,16 @@ var buildRecord = (level, context, msg, data) => {
168
191
  }
169
192
  return logRecord;
170
193
  };
171
- var createJsonTransport = () => {
194
+ var createProdTransport = (config = {}) => {
195
+ const getTimestamp = config.timestamp === "epoch" ? () => Date.now() : () => (/* @__PURE__ */ new Date()).toISOString();
196
+ const dest = config.dest ?? process2.stdout;
172
197
  return (level, context, msg, data) => {
173
- const record = buildRecord(level, context, msg, data);
198
+ const record = buildRecord(level, context, msg, getTimestamp, data);
174
199
  let line;
175
200
  try {
176
201
  line = JSON.stringify(record, jsonReplacer) + "\n";
177
202
  } catch {
178
- if (record.data) record.data = inspect(record.data);
203
+ if (record.data) record.data = inspect3(record.data);
179
204
  try {
180
205
  line = JSON.stringify(record, jsonReplacer) + "\n";
181
206
  } catch {
@@ -187,28 +212,25 @@ var createJsonTransport = () => {
187
212
  }) + "\n";
188
213
  }
189
214
  }
190
- process.stdout.write(line);
215
+ dest.write(line);
191
216
  };
192
217
  };
193
218
 
194
219
  // src/index.ts
195
- var fillContextItem = (item, useAllColors = false) => {
196
- return {
220
+ var createFiro = (config = {}, parentContext = []) => {
221
+ const useAllColors = config.useAllColors ?? true;
222
+ const fill = (item) => ({
197
223
  ...item,
198
224
  colorIndex: typeof item.colorIndex === "number" ? item.colorIndex : getColorIndex(item.key, useAllColors),
199
225
  color: item.color,
200
226
  omitKey: item.omitKey ?? false
201
- };
202
- };
203
- var createLoggerInternal = (config, parentContext) => {
204
- const useAllColors = config.useAllColors ?? false;
205
- const fill = (item) => fillContextItem(item, useAllColors);
227
+ });
206
228
  const appendContextWithInvokeContext = (context2, invokeContext) => {
207
229
  if (!invokeContext || invokeContext.length === 0) return context2;
208
230
  return [...context2, ...invokeContext.map(fill)];
209
231
  };
210
232
  const context = [...parentContext.map(fill)];
211
- const transport = config.transport ?? (config.mode === "prod" ? createJsonTransport() : createDevTransport(config.devTransportConfig));
233
+ const transport = config.transport ?? (config.mode === "prod" ? createProdTransport(config.prodTransportConfig) : createDevTransport(config.devTransportConfig));
212
234
  const minLevelName = config.mode === "prod" ? config.minLevelInProd ?? config.minLevel : config.minLevelInDev ?? config.minLevel;
213
235
  const minLevel = LOG_LEVELS[minLevelName ?? "debug"];
214
236
  const getContext = () => context;
@@ -239,7 +261,7 @@ var createLoggerInternal = (config, parentContext) => {
239
261
  }
240
262
  return { key, value, colorIndex: getColorIndex(key, useAllColors) };
241
263
  });
242
- return createLoggerInternal({ transport, minLevel: minLevelName, useAllColors }, [...context, ...newItems]);
264
+ return createFiro({ transport, minLevel: minLevelName, useAllColors }, [...context, ...newItems]);
243
265
  };
244
266
  const debug = (msg, data, opts) => {
245
267
  if (minLevel > LOG_LEVELS.debug) return;
@@ -272,12 +294,10 @@ var createLoggerInternal = (config, parentContext) => {
272
294
  removeFromContext: removeKeyFromContext
273
295
  });
274
296
  };
275
- var createLogger = (config = {}) => {
276
- return createLoggerInternal(config, []);
277
- };
278
297
  export {
279
298
  FIRO_COLORS,
299
+ utils_exports as FiroUtils,
280
300
  createDevTransport,
281
- createJsonTransport,
282
- createLogger
301
+ createFiro,
302
+ createProdTransport
283
303
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fend/firo",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Elegant logger for Node.js, Bun and Deno with brilliant DX.",
5
5
  "keywords": [
6
6
  "firo",
@@ -51,6 +51,7 @@
51
51
  "test": "node --import tsx --test test/*.test.ts",
52
52
  "check": "tsc --noEmit && node --import tsx --test test/*.test.ts",
53
53
  "typecheck": "tsc --noEmit",
54
- "demo": "tsx demo.ts"
54
+ "demo": "tsx demo.ts",
55
+ "bench": "tsx benchmark/prod.ts > /dev/null"
55
56
  }
56
57
  }