@logtape/logtape 2.1.0-dev.564 → 2.1.0-dev.600

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.
@@ -23,7 +23,7 @@ const levelAbbreviations = {
23
23
  * If `colors` is `true`, the output will be ANSI-colored.
24
24
  * @returns The string representation of the value.
25
25
  */
26
- const inspect = typeof document !== "undefined" || typeof navigator !== "undefined" && navigator.product === "ReactNative" ? (v) => JSON.stringify(v) : "Deno" in globalThis && "inspect" in globalThis.Deno && typeof globalThis.Deno.inspect === "function" ? (v, opts) => globalThis.Deno.inspect(v, {
26
+ const platformInspect = typeof document !== "undefined" || typeof navigator !== "undefined" && navigator.product === "ReactNative" ? (v) => JSON.stringify(v) : "Deno" in globalThis && "inspect" in globalThis.Deno && typeof globalThis.Deno.inspect === "function" ? (v, opts) => globalThis.Deno.inspect(v, {
27
27
  strAbbreviateSize: Infinity,
28
28
  iterableLimit: Infinity,
29
29
  ...opts
@@ -32,6 +32,20 @@ const inspect = typeof document !== "undefined" || typeof navigator !== "undefin
32
32
  maxStringLength: Infinity,
33
33
  ...opts
34
34
  }) : (v) => JSON.stringify(v);
35
+ const inspect = (value, options) => String(platformInspect(value, options));
36
+ const utf8Encoder = new TextEncoder();
37
+ function renderMessageParts(msgParts, valueRenderer) {
38
+ const msgLen = msgParts.length;
39
+ if (msgLen === 1) return msgParts[0];
40
+ if (msgLen <= 6) {
41
+ let message = "";
42
+ for (let i = 0; i < msgLen; i++) message += i % 2 === 0 ? msgParts[i] : valueRenderer(msgParts[i]);
43
+ return message;
44
+ }
45
+ const parts = new Array(msgLen);
46
+ for (let i = 0; i < msgLen; i++) parts[i] = i % 2 === 0 ? msgParts[i] : valueRenderer(msgParts[i]);
47
+ return parts.join("");
48
+ }
35
49
  function padZero(num) {
36
50
  return num < 10 ? `0${num}` : `${num}`;
37
51
  }
@@ -272,18 +286,7 @@ function getTextFormatter(options = {}) {
272
286
  const lineEnding = getLineEndingValue(options.lineEnding);
273
287
  const formatter = options.format ?? (({ timestamp, level, category, message }) => `${timestamp ? `${timestamp} ` : ""}[${level}] ${category}: ${message}`);
274
288
  return (record) => {
275
- const msgParts = record.message;
276
- const msgLen = msgParts.length;
277
- let message;
278
- if (msgLen === 1) message = msgParts[0];
279
- else if (msgLen <= 6) {
280
- message = "";
281
- for (let i = 0; i < msgLen; i++) message += i % 2 === 0 ? msgParts[i] : valueRenderer(msgParts[i]);
282
- } else {
283
- const parts = new Array(msgLen);
284
- for (let i = 0; i < msgLen; i++) parts[i] = i % 2 === 0 ? msgParts[i] : valueRenderer(msgParts[i]);
285
- message = parts.join("");
286
- }
289
+ const message = renderMessageParts(record.message, valueRenderer);
287
290
  const timestamp = timestampRenderer(record.timestamp);
288
291
  const level = levelRenderer(record.level);
289
292
  const category = typeof categorySeparator === "function" ? categorySeparator(record.category) : record.category.join(categorySeparator);
@@ -492,6 +495,146 @@ function getJsonLinesFormatter(options = {}) {
492
495
  * @since 0.11.0
493
496
  */
494
497
  const jsonLinesFormatter = getJsonLinesFormatter();
498
+ function renderStructuredMessage(record, template) {
499
+ if (template) {
500
+ if (typeof record.rawMessage === "string") return record.rawMessage;
501
+ return record.rawMessage.join("{}");
502
+ }
503
+ return renderMessageParts(record.message, stringifyLogfmtValue);
504
+ }
505
+ function filterLogfmtKey(key) {
506
+ if (key === "") return null;
507
+ let needsEscape = false;
508
+ for (const char of key) {
509
+ const code = char.codePointAt(0);
510
+ if (shouldEscapeLogfmtKeyChar(char, code)) {
511
+ needsEscape = true;
512
+ break;
513
+ }
514
+ }
515
+ if (!needsEscape) return key;
516
+ let result = "";
517
+ for (const char of key) {
518
+ const code = char.codePointAt(0);
519
+ if (shouldEscapeLogfmtKeyChar(char, code)) result += encodeLogfmtKeyChar(char);
520
+ else result += char;
521
+ }
522
+ return result;
523
+ }
524
+ function shouldEscapeLogfmtKeyChar(char, code) {
525
+ return code <= 32 || code === 127 || code === 65533 || char === "=" || char === "\"" || char === "%";
526
+ }
527
+ function encodeLogfmtKeyChar(char) {
528
+ let result = "";
529
+ for (const byte of utf8Encoder.encode(char)) result += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
530
+ return result;
531
+ }
532
+ function stringifyLogfmtValue(value) {
533
+ if (typeof value === "string") return value;
534
+ if (value === null) return "null";
535
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "undefined" || typeof value === "symbol" || typeof value === "function") return String(value);
536
+ try {
537
+ const json = JSON.stringify(value, jsonReplacer);
538
+ if (typeof json === "string") return unwrapJsonStringLiteral(json);
539
+ } catch {}
540
+ return inspect(value, { colors: false });
541
+ }
542
+ function unwrapJsonStringLiteral(json) {
543
+ if (json.startsWith("\"") && json.endsWith("\"")) return JSON.parse(json);
544
+ return json;
545
+ }
546
+ function quoteLogfmtValue(value, isString) {
547
+ let needsQuote = value === "" || isString && shouldQuoteStringLiteral(value);
548
+ for (const char of value) {
549
+ const code = char.codePointAt(0);
550
+ if (shouldQuoteLogfmtValueChar(char, code)) {
551
+ needsQuote = true;
552
+ break;
553
+ }
554
+ }
555
+ if (!needsQuote) return value;
556
+ let quoted = "";
557
+ for (const char of value) {
558
+ const code = char.codePointAt(0);
559
+ quoted += escapeLogfmtValueChar(char, code);
560
+ }
561
+ return `"${quoted}"`;
562
+ }
563
+ function shouldQuoteStringLiteral(value) {
564
+ return value === "null" || value === "undefined" || value === "true" || value === "false";
565
+ }
566
+ function shouldQuoteLogfmtValueChar(char, code) {
567
+ return code <= 32 || code === 127 || code === 65533 || char === "=" || char === "\"" || char === "\\";
568
+ }
569
+ function escapeLogfmtValueChar(char, code) {
570
+ switch (char) {
571
+ case " ": return "\\t";
572
+ case "\n": return "\\n";
573
+ case "\r": return "\\r";
574
+ case "\"": return "\\\"";
575
+ case "\\": return "\\\\";
576
+ default: return code <= 31 || code === 127 ? `\\u${code.toString(16).padStart(4, "0")}` : char;
577
+ }
578
+ }
579
+ function formatLogfmtValue(value) {
580
+ const stringified = stringifyLogfmtValue(value);
581
+ return quoteLogfmtValue(stringified, typeof value === "string");
582
+ }
583
+ function pushLogfmtPair(pairs, key, value) {
584
+ const filteredKey = filterLogfmtKey(key);
585
+ if (filteredKey == null) return;
586
+ pairs.push(`${filteredKey}=${formatLogfmtValue(value)}`);
587
+ }
588
+ /**
589
+ * Get a [logfmt] formatter with the specified options. The log records
590
+ * will be rendered as space-delimited key-value pairs, one record per line.
591
+ * It looks like this:
592
+ *
593
+ * ```text
594
+ * time=2023-11-14T22:13:20.000Z level=info logger=my.logger msg="Hello, world!" key=value
595
+ * ```
596
+ *
597
+ * [logfmt]: https://brandur.org/logfmt
598
+ * @param options The options for the logfmt formatter.
599
+ * @returns The logfmt formatter.
600
+ * @since 2.1.0
601
+ */
602
+ function getLogfmtFormatter(options = {}) {
603
+ const prependPrefix = "prepend:";
604
+ const lineEnding = getLineEndingValue(options.lineEnding);
605
+ const timestampRenderer = createTimestampFormatter("rfc3339", resolveTimeZone(options.timeZone));
606
+ const isTemplateMessage = options.message === "template";
607
+ const propertiesOption = options.properties ?? "flatten";
608
+ let joinCategory;
609
+ if (typeof options.categorySeparator === "function") joinCategory = options.categorySeparator;
610
+ else {
611
+ const separator = options.categorySeparator ?? ".";
612
+ joinCategory = (category) => category.join(separator);
613
+ }
614
+ let propertyPrefix = "";
615
+ if (propertiesOption === "flatten") propertyPrefix = "";
616
+ else if (propertiesOption.startsWith(prependPrefix)) {
617
+ propertyPrefix = propertiesOption.substring(prependPrefix.length);
618
+ if (propertyPrefix === "") throw new TypeError("Invalid properties option: " + JSON.stringify(propertiesOption) + ". It must be of the form \"prepend:<prefix>\" where <prefix> is a non-empty string.");
619
+ } else throw new TypeError(`Invalid properties option: ${JSON.stringify(propertiesOption)}. It must be "flatten" or "prepend:<prefix>".`);
620
+ return (record) => {
621
+ const pairs = [];
622
+ pushLogfmtPair(pairs, "time", timestampRenderer(record.timestamp));
623
+ pushLogfmtPair(pairs, "level", record.level);
624
+ pushLogfmtPair(pairs, "logger", joinCategory(record.category));
625
+ pushLogfmtPair(pairs, "msg", renderStructuredMessage(record, isTemplateMessage));
626
+ for (const key in record.properties) if (Object.prototype.hasOwnProperty.call(record.properties, key)) pushLogfmtPair(pairs, `${propertyPrefix}${key}`, record.properties[key]);
627
+ return `${pairs.join(" ")}${lineEnding}`;
628
+ };
629
+ }
630
+ /**
631
+ * The default [logfmt] formatter. This formatter formats log records as
632
+ * space-delimited key-value pairs, one record per line.
633
+ *
634
+ * [logfmt]: https://brandur.org/logfmt
635
+ * @since 2.1.0
636
+ */
637
+ const logfmtFormatter = getLogfmtFormatter();
495
638
  /**
496
639
  * The styles for the log level in the console.
497
640
  */
@@ -537,5 +680,7 @@ exports.defaultConsoleFormatter = defaultConsoleFormatter;
537
680
  exports.defaultTextFormatter = defaultTextFormatter;
538
681
  exports.getAnsiColorFormatter = getAnsiColorFormatter;
539
682
  exports.getJsonLinesFormatter = getJsonLinesFormatter;
683
+ exports.getLogfmtFormatter = getLogfmtFormatter;
540
684
  exports.getTextFormatter = getTextFormatter;
541
- exports.jsonLinesFormatter = jsonLinesFormatter;
685
+ exports.jsonLinesFormatter = jsonLinesFormatter;
686
+ exports.logfmtFormatter = logfmtFormatter;
@@ -372,6 +372,84 @@ declare function getJsonLinesFormatter(options?: JsonLinesFormatterOptions): Tex
372
372
  * @since 0.11.0
373
373
  */
374
374
  declare const jsonLinesFormatter: TextFormatter;
375
+ /**
376
+ * Options for the {@link getLogfmtFormatter} function.
377
+ * @since 2.1.0
378
+ */
379
+ interface LogfmtFormatterOptions {
380
+ /**
381
+ * The separator between category names. For example, if the separator is
382
+ * `"."`, the category `["a", "b", "c"]` will be formatted as `"a.b.c"`.
383
+ * If this is a function, it will be called with the category array and
384
+ * should return a string, which will be used for rendering the category.
385
+ *
386
+ * @default `"."`
387
+ */
388
+ readonly categorySeparator?: string | ((category: readonly string[]) => string);
389
+ /**
390
+ * The message format. This can be one of the following:
391
+ *
392
+ * - `"template"`: The raw message template is used as the message.
393
+ * - `"rendered"`: The message is rendered with the values.
394
+ *
395
+ * @default `"rendered"`
396
+ */
397
+ readonly message?: "template" | "rendered";
398
+ /**
399
+ * The properties format. This can be one of the following:
400
+ *
401
+ * - `"flatten"`: The properties are flattened into logfmt key-value pairs.
402
+ * - `"prepend:<prefix>"`: The properties are prepended with the given prefix
403
+ * (e.g., `"prepend:ctx_"` will prepend `ctx_` to each property key).
404
+ *
405
+ * @default `"flatten"`
406
+ */
407
+ readonly properties?: "flatten" | `prepend:${string}`;
408
+ /**
409
+ * The timezone used for timestamp rendering.
410
+ *
411
+ * - `undefined` (default): UTC
412
+ * - `null`: System local timezone
413
+ * - IANA timezone name such as `"America/Bogota"` or `"Asia/Seoul"`
414
+ * - Fixed UTC offset string such as `"+09:00"` or `"-05:00"`
415
+ *
416
+ * @since 2.1.0
417
+ */
418
+ readonly timeZone?: string | null;
419
+ /**
420
+ * Line ending style for formatted output.
421
+ *
422
+ * - `"lf"`: Unix-style line endings (`\n`)
423
+ * - `"crlf"`: Windows-style line endings (`\r\n`)
424
+ *
425
+ * @default "lf"
426
+ * @since 2.1.0
427
+ */
428
+ readonly lineEnding?: "lf" | "crlf";
429
+ }
430
+ /**
431
+ * Get a [logfmt] formatter with the specified options. The log records
432
+ * will be rendered as space-delimited key-value pairs, one record per line.
433
+ * It looks like this:
434
+ *
435
+ * ```text
436
+ * time=2023-11-14T22:13:20.000Z level=info logger=my.logger msg="Hello, world!" key=value
437
+ * ```
438
+ *
439
+ * [logfmt]: https://brandur.org/logfmt
440
+ * @param options The options for the logfmt formatter.
441
+ * @returns The logfmt formatter.
442
+ * @since 2.1.0
443
+ */
444
+ declare function getLogfmtFormatter(options?: LogfmtFormatterOptions): TextFormatter;
445
+ /**
446
+ * The default [logfmt] formatter. This formatter formats log records as
447
+ * space-delimited key-value pairs, one record per line.
448
+ *
449
+ * [logfmt]: https://brandur.org/logfmt
450
+ * @since 2.1.0
451
+ */
452
+ declare const logfmtFormatter: TextFormatter;
375
453
  /**
376
454
  * A console formatter is a function that accepts a log record and returns
377
455
  * an array of arguments to pass to {@link console.log}.
@@ -391,5 +469,5 @@ type ConsoleFormatter = (record: LogRecord) => readonly unknown[];
391
469
  declare function defaultConsoleFormatter(record: LogRecord): readonly unknown[];
392
470
  //# sourceMappingURL=formatter.d.ts.map
393
471
  //#endregion
394
- export { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, ConsoleFormatter, FormattedValues, JsonLinesFormatterOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, defaultConsoleFormatter, defaultTextFormatter, getAnsiColorFormatter, getJsonLinesFormatter, getTextFormatter, jsonLinesFormatter };
472
+ export { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, ConsoleFormatter, FormattedValues, JsonLinesFormatterOptions, LogfmtFormatterOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, defaultConsoleFormatter, defaultTextFormatter, getAnsiColorFormatter, getJsonLinesFormatter, getLogfmtFormatter, getTextFormatter, jsonLinesFormatter, logfmtFormatter };
395
473
  //# sourceMappingURL=formatter.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"formatter.d.cts","names":[],"sources":["../src/formatter.ts"],"sourcesContent":[],"mappings":";;;;;;;AAWA;AA+DA;AA+BA;;;AA6IoB,KA3OR,aAAA,GA2OQ,CAAA,MAAA,EA3OiB,SA2OjB,EAAA,GAAA,MAAA;AAAe;AA+TnC;;;AAEG,UA7ec,eAAA,CA6ed;EAAa;AAiHhB;AAQA;EAyBY,SAAA,EAAA,MAAS,GAAA,IAAA;EA4BJ;;;EA0CW,KAKT,EAAA,MAAA;EAAS;;;EAiBc,QAA1B,EAAA,MAAA;EAAM;;;EAhEiD,OAAA,EAAA,MAAA;EAqFvD;;;EACyB,MACtC,EA1tBO,SA0tBP;AAAa;AAgEhB;AAMA;AAiEA;;AACW,UA31BM,oBAAA,CA21BN;EAA8B;AACzB;AA8JhB;AAUA;AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mEA/8Be;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAmEK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+TJ,gBAAA,WACL,uBACR;;;;;;;;;;;cAiHU,sBAAsB;;;;;KAQvB,SAAA;;;;;KAyBA,SAAA;;;;;UA4BK,yBAAA,SAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBA0ChC;;;;mBAKA;;;;eAKJ;;;;;;;;;;;gBAYC,OAAO,UAAU;;;;kBAKf;;;;kBAKA;;;;;;;;;;iBAWF,qBAAA,WACL,4BACR;;;;;;;;;;cAgEU,oBAAoB;;;;;UAMhB,yBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiED,qBAAA,WACL,4BACR;;;;;;;;;;;;;;;;;cA8JU,oBAAoB;;;;;;;;;KAUrB,gBAAA,YAA4B;;;;;;;;iBAqBxB,uBAAA,SAAgC"}
1
+ {"version":3,"file":"formatter.d.cts","names":[],"sources":["../src/formatter.ts"],"sourcesContent":[],"mappings":";;;;;;;AAWA;AAsGA;AA+BA;;;AA6IoB,KAlRR,aAAA,GAkRQ,CAAA,MAAA,EAlRiB,SAkRjB,EAAA,GAAA,MAAA;AAAe;AA+TnC;;;AAEG,UA7ec,eAAA,CA6ed;EAAa;AA0FhB;AAQA;EAyBY,SAAA,EAAA,MAAS,GAAA,IAAA;EA4BJ;;;EA0CW,KAKT,EAAA,MAAA;EAAS;;;EAiBc,QAA1B,EAAA,MAAA;EAAM;;;EAhEiD,OAAA,EAAA,MAAA;EAqFvD;;;EACyB,MACtC,EAnsBO,SAmsBP;AAAa;AAgEhB;AAMA;AAiEA;;AACW,UAp0BM,oBAAA,CAo0BN;EAA8B;AACzB;AA8JhB;AAMA;AAoNA;;;;AAEgB;AA6EhB;AAUA;AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mEAjuCe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAmEK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+TJ,gBAAA,WACL,uBACR;;;;;;;;;;;cA0FU,sBAAsB;;;;;KAQvB,SAAA;;;;;KAyBA,SAAA;;;;;UA4BK,yBAAA,SAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBA0ChC;;;;mBAKA;;;;eAKJ;;;;;;;;;;;gBAYC,OAAO,UAAU;;;;kBAKf;;;;kBAKA;;;;;;;;;;iBAWF,qBAAA,WACL,4BACR;;;;;;;;;;cAgEU,oBAAoB;;;;;UAMhB,yBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiED,qBAAA,WACL,4BACR;;;;;;;;;;;;;;;;;cA8JU,oBAAoB;;;;;UAMhB,sBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoND,kBAAA,WACL,yBACR;;;;;;;;cA6EU,iBAAiB;;;;;;;;;KAUlB,gBAAA,YAA4B;;;;;;;;iBAqBxB,uBAAA,SAAgC"}
@@ -372,6 +372,84 @@ declare function getJsonLinesFormatter(options?: JsonLinesFormatterOptions): Tex
372
372
  * @since 0.11.0
373
373
  */
374
374
  declare const jsonLinesFormatter: TextFormatter;
375
+ /**
376
+ * Options for the {@link getLogfmtFormatter} function.
377
+ * @since 2.1.0
378
+ */
379
+ interface LogfmtFormatterOptions {
380
+ /**
381
+ * The separator between category names. For example, if the separator is
382
+ * `"."`, the category `["a", "b", "c"]` will be formatted as `"a.b.c"`.
383
+ * If this is a function, it will be called with the category array and
384
+ * should return a string, which will be used for rendering the category.
385
+ *
386
+ * @default `"."`
387
+ */
388
+ readonly categorySeparator?: string | ((category: readonly string[]) => string);
389
+ /**
390
+ * The message format. This can be one of the following:
391
+ *
392
+ * - `"template"`: The raw message template is used as the message.
393
+ * - `"rendered"`: The message is rendered with the values.
394
+ *
395
+ * @default `"rendered"`
396
+ */
397
+ readonly message?: "template" | "rendered";
398
+ /**
399
+ * The properties format. This can be one of the following:
400
+ *
401
+ * - `"flatten"`: The properties are flattened into logfmt key-value pairs.
402
+ * - `"prepend:<prefix>"`: The properties are prepended with the given prefix
403
+ * (e.g., `"prepend:ctx_"` will prepend `ctx_` to each property key).
404
+ *
405
+ * @default `"flatten"`
406
+ */
407
+ readonly properties?: "flatten" | `prepend:${string}`;
408
+ /**
409
+ * The timezone used for timestamp rendering.
410
+ *
411
+ * - `undefined` (default): UTC
412
+ * - `null`: System local timezone
413
+ * - IANA timezone name such as `"America/Bogota"` or `"Asia/Seoul"`
414
+ * - Fixed UTC offset string such as `"+09:00"` or `"-05:00"`
415
+ *
416
+ * @since 2.1.0
417
+ */
418
+ readonly timeZone?: string | null;
419
+ /**
420
+ * Line ending style for formatted output.
421
+ *
422
+ * - `"lf"`: Unix-style line endings (`\n`)
423
+ * - `"crlf"`: Windows-style line endings (`\r\n`)
424
+ *
425
+ * @default "lf"
426
+ * @since 2.1.0
427
+ */
428
+ readonly lineEnding?: "lf" | "crlf";
429
+ }
430
+ /**
431
+ * Get a [logfmt] formatter with the specified options. The log records
432
+ * will be rendered as space-delimited key-value pairs, one record per line.
433
+ * It looks like this:
434
+ *
435
+ * ```text
436
+ * time=2023-11-14T22:13:20.000Z level=info logger=my.logger msg="Hello, world!" key=value
437
+ * ```
438
+ *
439
+ * [logfmt]: https://brandur.org/logfmt
440
+ * @param options The options for the logfmt formatter.
441
+ * @returns The logfmt formatter.
442
+ * @since 2.1.0
443
+ */
444
+ declare function getLogfmtFormatter(options?: LogfmtFormatterOptions): TextFormatter;
445
+ /**
446
+ * The default [logfmt] formatter. This formatter formats log records as
447
+ * space-delimited key-value pairs, one record per line.
448
+ *
449
+ * [logfmt]: https://brandur.org/logfmt
450
+ * @since 2.1.0
451
+ */
452
+ declare const logfmtFormatter: TextFormatter;
375
453
  /**
376
454
  * A console formatter is a function that accepts a log record and returns
377
455
  * an array of arguments to pass to {@link console.log}.
@@ -391,5 +469,5 @@ type ConsoleFormatter = (record: LogRecord) => readonly unknown[];
391
469
  declare function defaultConsoleFormatter(record: LogRecord): readonly unknown[];
392
470
  //# sourceMappingURL=formatter.d.ts.map
393
471
  //#endregion
394
- export { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, ConsoleFormatter, FormattedValues, JsonLinesFormatterOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, defaultConsoleFormatter, defaultTextFormatter, getAnsiColorFormatter, getJsonLinesFormatter, getTextFormatter, jsonLinesFormatter };
472
+ export { AnsiColor, AnsiColorFormatterOptions, AnsiStyle, ConsoleFormatter, FormattedValues, JsonLinesFormatterOptions, LogfmtFormatterOptions, TextFormatter, TextFormatterOptions, ansiColorFormatter, defaultConsoleFormatter, defaultTextFormatter, getAnsiColorFormatter, getJsonLinesFormatter, getLogfmtFormatter, getTextFormatter, jsonLinesFormatter, logfmtFormatter };
395
473
  //# sourceMappingURL=formatter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"formatter.d.ts","names":[],"sources":["../src/formatter.ts"],"sourcesContent":[],"mappings":";;;;;;;AAWA;AA+DA;AA+BA;;;AA6IoB,KA3OR,aAAA,GA2OQ,CAAA,MAAA,EA3OiB,SA2OjB,EAAA,GAAA,MAAA;AAAe;AA+TnC;;;AAEG,UA7ec,eAAA,CA6ed;EAAa;AAiHhB;AAQA;EAyBY,SAAA,EAAA,MAAS,GAAA,IAAA;EA4BJ;;;EA0CW,KAKT,EAAA,MAAA;EAAS;;;EAiBc,QAA1B,EAAA,MAAA;EAAM;;;EAhEiD,OAAA,EAAA,MAAA;EAqFvD;;;EACyB,MACtC,EA1tBO,SA0tBP;AAAa;AAgEhB;AAMA;AAiEA;;AACW,UA31BM,oBAAA,CA21BN;EAA8B;AACzB;AA8JhB;AAUA;AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mEA/8Be;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAmEK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+TJ,gBAAA,WACL,uBACR;;;;;;;;;;;cAiHU,sBAAsB;;;;;KAQvB,SAAA;;;;;KAyBA,SAAA;;;;;UA4BK,yBAAA,SAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBA0ChC;;;;mBAKA;;;;eAKJ;;;;;;;;;;;gBAYC,OAAO,UAAU;;;;kBAKf;;;;kBAKA;;;;;;;;;;iBAWF,qBAAA,WACL,4BACR;;;;;;;;;;cAgEU,oBAAoB;;;;;UAMhB,yBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiED,qBAAA,WACL,4BACR;;;;;;;;;;;;;;;;;cA8JU,oBAAoB;;;;;;;;;KAUrB,gBAAA,YAA4B;;;;;;;;iBAqBxB,uBAAA,SAAgC"}
1
+ {"version":3,"file":"formatter.d.ts","names":[],"sources":["../src/formatter.ts"],"sourcesContent":[],"mappings":";;;;;;;AAWA;AAsGA;AA+BA;;;AA6IoB,KAlRR,aAAA,GAkRQ,CAAA,MAAA,EAlRiB,SAkRjB,EAAA,GAAA,MAAA;AAAe;AA+TnC;;;AAEG,UA7ec,eAAA,CA6ed;EAAa;AA0FhB;AAQA;EAyBY,SAAA,EAAA,MAAS,GAAA,IAAA;EA4BJ;;;EA0CW,KAKT,EAAA,MAAA;EAAS;;;EAiBc,QAA1B,EAAA,MAAA;EAAM;;;EAhEiD,OAAA,EAAA,MAAA;EAqFvD;;;EACyB,MACtC,EAnsBO,SAmsBP;AAAa;AAgEhB;AAMA;AAiEA;;AACW,UAp0BM,oBAAA,CAo0BN;EAA8B;AACzB;AA8JhB;AAMA;AAoNA;;;;AAEgB;AA6EhB;AAUA;AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mEAjuCe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAmEK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+TJ,gBAAA,WACL,uBACR;;;;;;;;;;;cA0FU,sBAAsB;;;;;KAQvB,SAAA;;;;;KAyBA,SAAA;;;;;UA4BK,yBAAA,SAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBA0ChC;;;;mBAKA;;;;eAKJ;;;;;;;;;;;gBAYC,OAAO,UAAU;;;;kBAKf;;;;kBAKA;;;;;;;;;;iBAWF,qBAAA,WACL,4BACR;;;;;;;;;;cAgEU,oBAAoB;;;;;UAMhB,yBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiED,qBAAA,WACL,4BACR;;;;;;;;;;;;;;;;;cA8JU,oBAAoB;;;;;UAMhB,sBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoND,kBAAA,WACL,yBACR;;;;;;;;cA6EU,iBAAiB;;;;;;;;;KAUlB,gBAAA,YAA4B;;;;;;;;iBAqBxB,uBAAA,SAAgC"}
package/dist/formatter.js CHANGED
@@ -22,7 +22,7 @@ const levelAbbreviations = {
22
22
  * If `colors` is `true`, the output will be ANSI-colored.
23
23
  * @returns The string representation of the value.
24
24
  */
25
- const inspect = typeof document !== "undefined" || typeof navigator !== "undefined" && navigator.product === "ReactNative" ? (v) => JSON.stringify(v) : "Deno" in globalThis && "inspect" in globalThis.Deno && typeof globalThis.Deno.inspect === "function" ? (v, opts) => globalThis.Deno.inspect(v, {
25
+ const platformInspect = typeof document !== "undefined" || typeof navigator !== "undefined" && navigator.product === "ReactNative" ? (v) => JSON.stringify(v) : "Deno" in globalThis && "inspect" in globalThis.Deno && typeof globalThis.Deno.inspect === "function" ? (v, opts) => globalThis.Deno.inspect(v, {
26
26
  strAbbreviateSize: Infinity,
27
27
  iterableLimit: Infinity,
28
28
  ...opts
@@ -31,6 +31,20 @@ const inspect = typeof document !== "undefined" || typeof navigator !== "undefin
31
31
  maxStringLength: Infinity,
32
32
  ...opts
33
33
  }) : (v) => JSON.stringify(v);
34
+ const inspect = (value, options) => String(platformInspect(value, options));
35
+ const utf8Encoder = new TextEncoder();
36
+ function renderMessageParts(msgParts, valueRenderer) {
37
+ const msgLen = msgParts.length;
38
+ if (msgLen === 1) return msgParts[0];
39
+ if (msgLen <= 6) {
40
+ let message = "";
41
+ for (let i = 0; i < msgLen; i++) message += i % 2 === 0 ? msgParts[i] : valueRenderer(msgParts[i]);
42
+ return message;
43
+ }
44
+ const parts = new Array(msgLen);
45
+ for (let i = 0; i < msgLen; i++) parts[i] = i % 2 === 0 ? msgParts[i] : valueRenderer(msgParts[i]);
46
+ return parts.join("");
47
+ }
34
48
  function padZero(num) {
35
49
  return num < 10 ? `0${num}` : `${num}`;
36
50
  }
@@ -271,18 +285,7 @@ function getTextFormatter(options = {}) {
271
285
  const lineEnding = getLineEndingValue(options.lineEnding);
272
286
  const formatter = options.format ?? (({ timestamp, level, category, message }) => `${timestamp ? `${timestamp} ` : ""}[${level}] ${category}: ${message}`);
273
287
  return (record) => {
274
- const msgParts = record.message;
275
- const msgLen = msgParts.length;
276
- let message;
277
- if (msgLen === 1) message = msgParts[0];
278
- else if (msgLen <= 6) {
279
- message = "";
280
- for (let i = 0; i < msgLen; i++) message += i % 2 === 0 ? msgParts[i] : valueRenderer(msgParts[i]);
281
- } else {
282
- const parts = new Array(msgLen);
283
- for (let i = 0; i < msgLen; i++) parts[i] = i % 2 === 0 ? msgParts[i] : valueRenderer(msgParts[i]);
284
- message = parts.join("");
285
- }
288
+ const message = renderMessageParts(record.message, valueRenderer);
286
289
  const timestamp = timestampRenderer(record.timestamp);
287
290
  const level = levelRenderer(record.level);
288
291
  const category = typeof categorySeparator === "function" ? categorySeparator(record.category) : record.category.join(categorySeparator);
@@ -491,6 +494,146 @@ function getJsonLinesFormatter(options = {}) {
491
494
  * @since 0.11.0
492
495
  */
493
496
  const jsonLinesFormatter = getJsonLinesFormatter();
497
+ function renderStructuredMessage(record, template) {
498
+ if (template) {
499
+ if (typeof record.rawMessage === "string") return record.rawMessage;
500
+ return record.rawMessage.join("{}");
501
+ }
502
+ return renderMessageParts(record.message, stringifyLogfmtValue);
503
+ }
504
+ function filterLogfmtKey(key) {
505
+ if (key === "") return null;
506
+ let needsEscape = false;
507
+ for (const char of key) {
508
+ const code = char.codePointAt(0);
509
+ if (shouldEscapeLogfmtKeyChar(char, code)) {
510
+ needsEscape = true;
511
+ break;
512
+ }
513
+ }
514
+ if (!needsEscape) return key;
515
+ let result = "";
516
+ for (const char of key) {
517
+ const code = char.codePointAt(0);
518
+ if (shouldEscapeLogfmtKeyChar(char, code)) result += encodeLogfmtKeyChar(char);
519
+ else result += char;
520
+ }
521
+ return result;
522
+ }
523
+ function shouldEscapeLogfmtKeyChar(char, code) {
524
+ return code <= 32 || code === 127 || code === 65533 || char === "=" || char === "\"" || char === "%";
525
+ }
526
+ function encodeLogfmtKeyChar(char) {
527
+ let result = "";
528
+ for (const byte of utf8Encoder.encode(char)) result += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
529
+ return result;
530
+ }
531
+ function stringifyLogfmtValue(value) {
532
+ if (typeof value === "string") return value;
533
+ if (value === null) return "null";
534
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "undefined" || typeof value === "symbol" || typeof value === "function") return String(value);
535
+ try {
536
+ const json = JSON.stringify(value, jsonReplacer);
537
+ if (typeof json === "string") return unwrapJsonStringLiteral(json);
538
+ } catch {}
539
+ return inspect(value, { colors: false });
540
+ }
541
+ function unwrapJsonStringLiteral(json) {
542
+ if (json.startsWith("\"") && json.endsWith("\"")) return JSON.parse(json);
543
+ return json;
544
+ }
545
+ function quoteLogfmtValue(value, isString) {
546
+ let needsQuote = value === "" || isString && shouldQuoteStringLiteral(value);
547
+ for (const char of value) {
548
+ const code = char.codePointAt(0);
549
+ if (shouldQuoteLogfmtValueChar(char, code)) {
550
+ needsQuote = true;
551
+ break;
552
+ }
553
+ }
554
+ if (!needsQuote) return value;
555
+ let quoted = "";
556
+ for (const char of value) {
557
+ const code = char.codePointAt(0);
558
+ quoted += escapeLogfmtValueChar(char, code);
559
+ }
560
+ return `"${quoted}"`;
561
+ }
562
+ function shouldQuoteStringLiteral(value) {
563
+ return value === "null" || value === "undefined" || value === "true" || value === "false";
564
+ }
565
+ function shouldQuoteLogfmtValueChar(char, code) {
566
+ return code <= 32 || code === 127 || code === 65533 || char === "=" || char === "\"" || char === "\\";
567
+ }
568
+ function escapeLogfmtValueChar(char, code) {
569
+ switch (char) {
570
+ case " ": return "\\t";
571
+ case "\n": return "\\n";
572
+ case "\r": return "\\r";
573
+ case "\"": return "\\\"";
574
+ case "\\": return "\\\\";
575
+ default: return code <= 31 || code === 127 ? `\\u${code.toString(16).padStart(4, "0")}` : char;
576
+ }
577
+ }
578
+ function formatLogfmtValue(value) {
579
+ const stringified = stringifyLogfmtValue(value);
580
+ return quoteLogfmtValue(stringified, typeof value === "string");
581
+ }
582
+ function pushLogfmtPair(pairs, key, value) {
583
+ const filteredKey = filterLogfmtKey(key);
584
+ if (filteredKey == null) return;
585
+ pairs.push(`${filteredKey}=${formatLogfmtValue(value)}`);
586
+ }
587
+ /**
588
+ * Get a [logfmt] formatter with the specified options. The log records
589
+ * will be rendered as space-delimited key-value pairs, one record per line.
590
+ * It looks like this:
591
+ *
592
+ * ```text
593
+ * time=2023-11-14T22:13:20.000Z level=info logger=my.logger msg="Hello, world!" key=value
594
+ * ```
595
+ *
596
+ * [logfmt]: https://brandur.org/logfmt
597
+ * @param options The options for the logfmt formatter.
598
+ * @returns The logfmt formatter.
599
+ * @since 2.1.0
600
+ */
601
+ function getLogfmtFormatter(options = {}) {
602
+ const prependPrefix = "prepend:";
603
+ const lineEnding = getLineEndingValue(options.lineEnding);
604
+ const timestampRenderer = createTimestampFormatter("rfc3339", resolveTimeZone(options.timeZone));
605
+ const isTemplateMessage = options.message === "template";
606
+ const propertiesOption = options.properties ?? "flatten";
607
+ let joinCategory;
608
+ if (typeof options.categorySeparator === "function") joinCategory = options.categorySeparator;
609
+ else {
610
+ const separator = options.categorySeparator ?? ".";
611
+ joinCategory = (category) => category.join(separator);
612
+ }
613
+ let propertyPrefix = "";
614
+ if (propertiesOption === "flatten") propertyPrefix = "";
615
+ else if (propertiesOption.startsWith(prependPrefix)) {
616
+ propertyPrefix = propertiesOption.substring(prependPrefix.length);
617
+ if (propertyPrefix === "") throw new TypeError("Invalid properties option: " + JSON.stringify(propertiesOption) + ". It must be of the form \"prepend:<prefix>\" where <prefix> is a non-empty string.");
618
+ } else throw new TypeError(`Invalid properties option: ${JSON.stringify(propertiesOption)}. It must be "flatten" or "prepend:<prefix>".`);
619
+ return (record) => {
620
+ const pairs = [];
621
+ pushLogfmtPair(pairs, "time", timestampRenderer(record.timestamp));
622
+ pushLogfmtPair(pairs, "level", record.level);
623
+ pushLogfmtPair(pairs, "logger", joinCategory(record.category));
624
+ pushLogfmtPair(pairs, "msg", renderStructuredMessage(record, isTemplateMessage));
625
+ for (const key in record.properties) if (Object.prototype.hasOwnProperty.call(record.properties, key)) pushLogfmtPair(pairs, `${propertyPrefix}${key}`, record.properties[key]);
626
+ return `${pairs.join(" ")}${lineEnding}`;
627
+ };
628
+ }
629
+ /**
630
+ * The default [logfmt] formatter. This formatter formats log records as
631
+ * space-delimited key-value pairs, one record per line.
632
+ *
633
+ * [logfmt]: https://brandur.org/logfmt
634
+ * @since 2.1.0
635
+ */
636
+ const logfmtFormatter = getLogfmtFormatter();
494
637
  /**
495
638
  * The styles for the log level in the console.
496
639
  */
@@ -531,5 +674,5 @@ function defaultConsoleFormatter(record) {
531
674
  }
532
675
 
533
676
  //#endregion
534
- export { ansiColorFormatter, defaultConsoleFormatter, defaultTextFormatter, getAnsiColorFormatter, getJsonLinesFormatter, getTextFormatter, jsonLinesFormatter };
677
+ export { ansiColorFormatter, defaultConsoleFormatter, defaultTextFormatter, getAnsiColorFormatter, getJsonLinesFormatter, getLogfmtFormatter, getTextFormatter, jsonLinesFormatter, logfmtFormatter };
535
678
  //# sourceMappingURL=formatter.js.map