@logtape/logtape 0.11.0 → 0.12.0-dev.181

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.
Files changed (171) hide show
  1. package/config.test.ts +591 -0
  2. package/config.ts +421 -0
  3. package/context.test.ts +187 -0
  4. package/context.ts +55 -0
  5. package/deno.json +36 -0
  6. package/dist/_virtual/rolldown_runtime.cjs +30 -0
  7. package/dist/config.cjs +247 -0
  8. package/dist/config.d.cts +189 -0
  9. package/dist/config.d.cts.map +1 -0
  10. package/dist/config.d.ts +189 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +241 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/context.cjs +30 -0
  15. package/dist/context.d.cts +39 -0
  16. package/dist/context.d.cts.map +1 -0
  17. package/dist/context.d.ts +39 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +31 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/filter.cjs +32 -0
  22. package/dist/filter.d.cts +37 -0
  23. package/dist/filter.d.cts.map +1 -0
  24. package/{types → dist}/filter.d.ts +12 -6
  25. package/dist/filter.d.ts.map +1 -0
  26. package/dist/filter.js +31 -0
  27. package/dist/filter.js.map +1 -0
  28. package/dist/formatter.cjs +281 -0
  29. package/dist/formatter.d.cts +338 -0
  30. package/dist/formatter.d.cts.map +1 -0
  31. package/dist/formatter.d.ts +338 -0
  32. package/dist/formatter.d.ts.map +1 -0
  33. package/dist/formatter.js +275 -0
  34. package/dist/formatter.js.map +1 -0
  35. package/dist/level.cjs +64 -0
  36. package/dist/level.d.cts +34 -0
  37. package/dist/level.d.cts.map +1 -0
  38. package/{types → dist}/level.d.ts +7 -5
  39. package/dist/level.d.ts.map +1 -0
  40. package/dist/level.js +62 -0
  41. package/dist/level.js.map +1 -0
  42. package/dist/logger.cjs +351 -0
  43. package/dist/logger.d.cts +501 -0
  44. package/dist/logger.d.cts.map +1 -0
  45. package/dist/logger.d.ts +501 -0
  46. package/dist/logger.d.ts.map +1 -0
  47. package/dist/logger.js +351 -0
  48. package/dist/logger.js.map +1 -0
  49. package/dist/mod.cjs +33 -0
  50. package/dist/mod.d.cts +9 -0
  51. package/dist/mod.d.ts +9 -0
  52. package/dist/mod.js +9 -0
  53. package/dist/record.d.cts +50 -0
  54. package/dist/record.d.cts.map +1 -0
  55. package/dist/record.d.ts +50 -0
  56. package/dist/record.d.ts.map +1 -0
  57. package/dist/sink.cjs +95 -0
  58. package/dist/sink.d.cts +112 -0
  59. package/dist/sink.d.cts.map +1 -0
  60. package/{types → dist}/sink.d.ts +49 -45
  61. package/dist/sink.d.ts.map +1 -0
  62. package/dist/sink.js +94 -0
  63. package/dist/sink.js.map +1 -0
  64. package/dist/util.cjs +9 -0
  65. package/dist/util.d.cts +12 -0
  66. package/dist/util.d.cts.map +1 -0
  67. package/dist/util.d.ts +12 -0
  68. package/dist/util.d.ts.map +1 -0
  69. package/dist/util.deno.cjs +16 -0
  70. package/dist/util.deno.d.cts +12 -0
  71. package/dist/util.deno.d.cts.map +1 -0
  72. package/dist/util.deno.d.ts +12 -0
  73. package/dist/util.deno.d.ts.map +1 -0
  74. package/dist/util.deno.js +16 -0
  75. package/dist/util.deno.js.map +1 -0
  76. package/dist/util.js +9 -0
  77. package/dist/util.js.map +1 -0
  78. package/dist/util.node.cjs +10 -0
  79. package/dist/util.node.d.cts +12 -0
  80. package/dist/util.node.d.cts.map +1 -0
  81. package/dist/util.node.d.ts +12 -0
  82. package/dist/util.node.d.ts.map +1 -0
  83. package/dist/util.node.js +10 -0
  84. package/dist/util.node.js.map +1 -0
  85. package/filter.test.ts +70 -0
  86. package/filter.ts +57 -0
  87. package/fixtures.ts +30 -0
  88. package/formatter.test.ts +530 -0
  89. package/formatter.ts +724 -0
  90. package/level.test.ts +47 -0
  91. package/level.ts +67 -0
  92. package/logger.test.ts +823 -0
  93. package/logger.ts +1124 -0
  94. package/mod.ts +54 -0
  95. package/package.json +35 -23
  96. package/record.ts +49 -0
  97. package/sink.test.ts +219 -0
  98. package/sink.ts +167 -0
  99. package/tsdown.config.ts +24 -0
  100. package/util.deno.ts +19 -0
  101. package/util.node.ts +12 -0
  102. package/util.ts +11 -0
  103. package/esm/_dnt.shims.js +0 -57
  104. package/esm/config.js +0 -297
  105. package/esm/context.js +0 -23
  106. package/esm/filter.js +0 -42
  107. package/esm/formatter.js +0 -370
  108. package/esm/level.js +0 -59
  109. package/esm/logger.js +0 -517
  110. package/esm/mod.js +0 -8
  111. package/esm/nodeUtil.cjs +0 -20
  112. package/esm/nodeUtil.js +0 -2
  113. package/esm/package.json +0 -3
  114. package/esm/record.js +0 -1
  115. package/esm/sink.js +0 -96
  116. package/script/_dnt.shims.js +0 -60
  117. package/script/config.js +0 -331
  118. package/script/context.js +0 -26
  119. package/script/filter.js +0 -46
  120. package/script/formatter.js +0 -380
  121. package/script/level.js +0 -64
  122. package/script/logger.js +0 -548
  123. package/script/mod.js +0 -36
  124. package/script/nodeUtil.js +0 -20
  125. package/script/package.json +0 -3
  126. package/script/record.js +0 -2
  127. package/script/sink.js +0 -101
  128. package/types/_dnt.shims.d.ts +0 -2
  129. package/types/_dnt.shims.d.ts.map +0 -1
  130. package/types/_dnt.test_shims.d.ts.map +0 -1
  131. package/types/config.d.ts +0 -183
  132. package/types/config.d.ts.map +0 -1
  133. package/types/config.test.d.ts.map +0 -1
  134. package/types/context.d.ts +0 -35
  135. package/types/context.d.ts.map +0 -1
  136. package/types/context.test.d.ts.map +0 -1
  137. package/types/deps/jsr.io/@std/assert/0.222.1/_constants.d.ts.map +0 -1
  138. package/types/deps/jsr.io/@std/assert/0.222.1/_diff.d.ts.map +0 -1
  139. package/types/deps/jsr.io/@std/assert/0.222.1/_format.d.ts.map +0 -1
  140. package/types/deps/jsr.io/@std/assert/0.222.1/assert.d.ts.map +0 -1
  141. package/types/deps/jsr.io/@std/assert/0.222.1/assert_equals.d.ts.map +0 -1
  142. package/types/deps/jsr.io/@std/assert/0.222.1/assert_false.d.ts.map +0 -1
  143. package/types/deps/jsr.io/@std/assert/0.222.1/assert_greater_or_equal.d.ts.map +0 -1
  144. package/types/deps/jsr.io/@std/assert/0.222.1/assert_is_error.d.ts.map +0 -1
  145. package/types/deps/jsr.io/@std/assert/0.222.1/assert_less_or_equal.d.ts.map +0 -1
  146. package/types/deps/jsr.io/@std/assert/0.222.1/assert_rejects.d.ts.map +0 -1
  147. package/types/deps/jsr.io/@std/assert/0.222.1/assert_strict_equals.d.ts.map +0 -1
  148. package/types/deps/jsr.io/@std/assert/0.222.1/assert_throws.d.ts.map +0 -1
  149. package/types/deps/jsr.io/@std/assert/0.222.1/assertion_error.d.ts.map +0 -1
  150. package/types/deps/jsr.io/@std/assert/0.222.1/equal.d.ts.map +0 -1
  151. package/types/deps/jsr.io/@std/async/0.222.1/delay.d.ts.map +0 -1
  152. package/types/deps/jsr.io/@std/fmt/0.222.1/colors.d.ts.map +0 -1
  153. package/types/filter.d.ts.map +0 -1
  154. package/types/filter.test.d.ts.map +0 -1
  155. package/types/fixtures.d.ts.map +0 -1
  156. package/types/formatter.d.ts +0 -332
  157. package/types/formatter.d.ts.map +0 -1
  158. package/types/formatter.test.d.ts.map +0 -1
  159. package/types/level.d.ts.map +0 -1
  160. package/types/level.test.d.ts.map +0 -1
  161. package/types/logger.d.ts +0 -573
  162. package/types/logger.d.ts.map +0 -1
  163. package/types/logger.test.d.ts.map +0 -1
  164. package/types/mod.d.ts +0 -9
  165. package/types/mod.d.ts.map +0 -1
  166. package/types/nodeUtil.d.ts +0 -12
  167. package/types/nodeUtil.d.ts.map +0 -1
  168. package/types/record.d.ts +0 -44
  169. package/types/record.d.ts.map +0 -1
  170. package/types/sink.d.ts.map +0 -1
  171. package/types/sink.test.d.ts.map +0 -1
package/filter.test.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { suite } from "@hongminhee/suite";
2
+ import { assert } from "@std/assert/assert";
3
+ import { assertFalse } from "@std/assert/false";
4
+ import { assertStrictEquals } from "@std/assert/strict-equals";
5
+ import { assertThrows } from "@std/assert/throws";
6
+ import { type Filter, getLevelFilter, toFilter } from "./filter.ts";
7
+ import { debug, error, fatal, info, warning } from "./fixtures.ts";
8
+ import type { LogLevel } from "./level.ts";
9
+
10
+ const test = suite(import.meta);
11
+
12
+ test("getLevelFilter()", () => {
13
+ const noneFilter = getLevelFilter(null);
14
+ assertFalse(noneFilter(fatal));
15
+ assertFalse(noneFilter(error));
16
+ assertFalse(noneFilter(warning));
17
+ assertFalse(noneFilter(info));
18
+ assertFalse(noneFilter(debug));
19
+
20
+ const fatalFilter = getLevelFilter("fatal");
21
+ assert(fatalFilter(fatal));
22
+ assertFalse(fatalFilter(error));
23
+ assertFalse(fatalFilter(warning));
24
+ assertFalse(fatalFilter(info));
25
+ assertFalse(fatalFilter(debug));
26
+
27
+ const errorFilter = getLevelFilter("error");
28
+ assert(errorFilter(fatal));
29
+ assert(errorFilter(error));
30
+ assertFalse(errorFilter(warning));
31
+ assertFalse(errorFilter(info));
32
+ assertFalse(errorFilter(debug));
33
+
34
+ const warningFilter = getLevelFilter("warning");
35
+ assert(warningFilter(fatal));
36
+ assert(warningFilter(error));
37
+ assert(warningFilter(warning));
38
+ assertFalse(warningFilter(info));
39
+ assertFalse(warningFilter(debug));
40
+
41
+ const infoFilter = getLevelFilter("info");
42
+ assert(infoFilter(fatal));
43
+ assert(infoFilter(error));
44
+ assert(infoFilter(warning));
45
+ assert(infoFilter(info));
46
+ assertFalse(infoFilter(debug));
47
+
48
+ const debugFilter = getLevelFilter("debug");
49
+ assert(debugFilter(fatal));
50
+ assert(debugFilter(error));
51
+ assert(debugFilter(warning));
52
+ assert(debugFilter(info));
53
+ assert(debugFilter(debug));
54
+
55
+ assertThrows(
56
+ () => getLevelFilter("invalid" as LogLevel),
57
+ TypeError,
58
+ "Invalid log level: invalid.",
59
+ );
60
+ });
61
+
62
+ test("toFilter()", () => {
63
+ const hasJunk: Filter = (record) => record.category.includes("junk");
64
+ assertStrictEquals(toFilter(hasJunk), hasJunk);
65
+
66
+ const infoFilter = toFilter("info");
67
+ assertFalse(infoFilter(debug));
68
+ assert(infoFilter(info));
69
+ assert(infoFilter(warning));
70
+ });
package/filter.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { LogLevel } from "./level.ts";
2
+ import type { LogRecord } from "./record.ts";
3
+
4
+ /**
5
+ * A filter is a function that accepts a log record and returns `true` if the
6
+ * record should be passed to the sink.
7
+ *
8
+ * @param record The log record to filter.
9
+ * @returns `true` if the record should be passed to the sink.
10
+ */
11
+ export type Filter = (record: LogRecord) => boolean;
12
+
13
+ /**
14
+ * A filter-like value is either a {@link Filter} or a {@link LogLevel}.
15
+ * `null` is also allowed to represent a filter that rejects all records.
16
+ */
17
+ export type FilterLike = Filter | LogLevel | null;
18
+
19
+ /**
20
+ * Converts a {@link FilterLike} value to an actual {@link Filter}.
21
+ *
22
+ * @param filter The filter-like value to convert.
23
+ * @returns The actual filter.
24
+ */
25
+ export function toFilter(filter: FilterLike): Filter {
26
+ if (typeof filter === "function") return filter;
27
+ return getLevelFilter(filter);
28
+ }
29
+
30
+ /**
31
+ * Returns a filter that accepts log records with the specified level.
32
+ *
33
+ * @param level The level to filter by. If `null`, the filter will reject all
34
+ * records.
35
+ * @returns The filter.
36
+ */
37
+ export function getLevelFilter(level: LogLevel | null): Filter {
38
+ if (level == null) return () => false;
39
+ if (level === "fatal") {
40
+ return (record: LogRecord) => record.level === "fatal";
41
+ } else if (level === "error") {
42
+ return (record: LogRecord) =>
43
+ record.level === "fatal" || record.level === "error";
44
+ } else if (level === "warning") {
45
+ return (record: LogRecord) =>
46
+ record.level === "fatal" ||
47
+ record.level === "error" ||
48
+ record.level === "warning";
49
+ } else if (level === "info") {
50
+ return (record: LogRecord) =>
51
+ record.level === "fatal" ||
52
+ record.level === "error" ||
53
+ record.level === "warning" ||
54
+ record.level === "info";
55
+ } else if (level === "debug") return () => true;
56
+ throw new TypeError(`Invalid log level: ${level}.`);
57
+ }
package/fixtures.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { LogRecord } from "./record.ts";
2
+
3
+ export const info: LogRecord = {
4
+ level: "info",
5
+ category: ["my-app", "junk"],
6
+ message: ["Hello, ", 123, " & ", 456, "!"],
7
+ rawMessage: "Hello, {a} & {b}!",
8
+ timestamp: 1700000000000,
9
+ properties: {},
10
+ };
11
+
12
+ export const debug: LogRecord = {
13
+ ...info,
14
+ level: "debug",
15
+ };
16
+
17
+ export const warning: LogRecord = {
18
+ ...info,
19
+ level: "warning",
20
+ };
21
+
22
+ export const error: LogRecord = {
23
+ ...info,
24
+ level: "error",
25
+ };
26
+
27
+ export const fatal: LogRecord = {
28
+ ...info,
29
+ level: "fatal",
30
+ };
@@ -0,0 +1,530 @@
1
+ import { suite } from "@hongminhee/suite";
2
+ import { assertEquals } from "@std/assert/equals";
3
+ import { assertThrows } from "@std/assert/throws";
4
+ import { fatal, info } from "./fixtures.ts";
5
+ import {
6
+ ansiColorFormatter,
7
+ defaultConsoleFormatter,
8
+ defaultTextFormatter,
9
+ type FormattedValues,
10
+ getAnsiColorFormatter,
11
+ getJsonLinesFormatter,
12
+ getTextFormatter,
13
+ } from "./formatter.ts";
14
+ import type { LogRecord } from "./record.ts";
15
+
16
+ const test = suite(import.meta);
17
+
18
+ test("getTextFormatter()", () => {
19
+ assertEquals(
20
+ getTextFormatter()(info),
21
+ "2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!\n",
22
+ );
23
+ assertEquals(
24
+ getTextFormatter({ timestamp: "date" })(info),
25
+ "2023-11-14 [INF] my-app·junk: Hello, 123 & 456!\n",
26
+ );
27
+ assertEquals(
28
+ getTextFormatter({ timestamp: "date-time" })(info),
29
+ "2023-11-14 22:13:20.000 [INF] my-app·junk: Hello, 123 & 456!\n",
30
+ );
31
+ assertEquals(
32
+ getTextFormatter({ timestamp: "date-time-timezone" })(info),
33
+ "2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!\n",
34
+ );
35
+ assertEquals(
36
+ getTextFormatter({ timestamp: "date-time-tz" })(info),
37
+ "2023-11-14 22:13:20.000 +00 [INF] my-app·junk: Hello, 123 & 456!\n",
38
+ );
39
+ assertEquals(
40
+ getTextFormatter({ timestamp: "none" })(info),
41
+ "[INF] my-app·junk: Hello, 123 & 456!\n",
42
+ );
43
+ assertEquals(
44
+ getTextFormatter({ timestamp: "disabled" })(info),
45
+ "[INF] my-app·junk: Hello, 123 & 456!\n",
46
+ );
47
+ assertEquals(
48
+ getTextFormatter({ timestamp: "rfc3339" })(info),
49
+ "2023-11-14T22:13:20.000Z [INF] my-app·junk: Hello, 123 & 456!\n",
50
+ );
51
+ assertEquals(
52
+ getTextFormatter({ timestamp: "time" })(info),
53
+ "22:13:20.000 [INF] my-app·junk: Hello, 123 & 456!\n",
54
+ );
55
+ assertEquals(
56
+ getTextFormatter({ timestamp: "time-timezone" })(info),
57
+ "22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!\n",
58
+ );
59
+ assertEquals(
60
+ getTextFormatter({ timestamp: "time-tz" })(info),
61
+ "22:13:20.000 +00 [INF] my-app·junk: Hello, 123 & 456!\n",
62
+ );
63
+ assertEquals(
64
+ getTextFormatter({
65
+ timestamp(ts) {
66
+ const t = new Date(ts);
67
+ return t.toUTCString();
68
+ },
69
+ })(info),
70
+ "Tue, 14 Nov 2023 22:13:20 GMT [INF] my-app·junk: Hello, 123 & 456!\n",
71
+ );
72
+
73
+ assertEquals(
74
+ getTextFormatter({ level: "ABBR" })(info),
75
+ "2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!\n",
76
+ );
77
+ assertEquals(
78
+ getTextFormatter({ level: "FULL" })(info),
79
+ "2023-11-14 22:13:20.000 +00:00 [INFO] my-app·junk: Hello, 123 & 456!\n",
80
+ );
81
+ assertEquals(
82
+ getTextFormatter({ level: "L" })(info),
83
+ "2023-11-14 22:13:20.000 +00:00 [I] my-app·junk: Hello, 123 & 456!\n",
84
+ );
85
+ assertEquals(
86
+ getTextFormatter({ level: "abbr" })(info),
87
+ "2023-11-14 22:13:20.000 +00:00 [inf] my-app·junk: Hello, 123 & 456!\n",
88
+ );
89
+ assertEquals(
90
+ getTextFormatter({ level: "full" })(info),
91
+ "2023-11-14 22:13:20.000 +00:00 [info] my-app·junk: Hello, 123 & 456!\n",
92
+ );
93
+ assertEquals(
94
+ getTextFormatter({ level: "l" })(info),
95
+ "2023-11-14 22:13:20.000 +00:00 [i] my-app·junk: Hello, 123 & 456!\n",
96
+ );
97
+ assertEquals(
98
+ getTextFormatter({
99
+ level(level) {
100
+ return level.at(-1) ?? "";
101
+ },
102
+ })(info),
103
+ "2023-11-14 22:13:20.000 +00:00 [o] my-app·junk: Hello, 123 & 456!\n",
104
+ );
105
+
106
+ assertEquals(
107
+ getTextFormatter({ category: "." })(info),
108
+ "2023-11-14 22:13:20.000 +00:00 [INF] my-app.junk: Hello, 123 & 456!\n",
109
+ );
110
+ assertEquals(
111
+ getTextFormatter({
112
+ category(category) {
113
+ return `<${category.join("/")}>`;
114
+ },
115
+ })(info),
116
+ "2023-11-14 22:13:20.000 +00:00 [INF] <my-app/junk>: Hello, 123 & 456!\n",
117
+ );
118
+
119
+ assertEquals(
120
+ getTextFormatter({
121
+ value(value) {
122
+ return typeof value;
123
+ },
124
+ })(info),
125
+ "2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, number & number!\n",
126
+ );
127
+
128
+ let recordedValues: FormattedValues | null = null;
129
+ assertEquals(
130
+ getTextFormatter({
131
+ format(values) {
132
+ recordedValues = values;
133
+ const { timestamp, level, category, message } = values;
134
+ return `${level} <${category}> ${message} ${timestamp}`;
135
+ },
136
+ })(info),
137
+ "INF <my-app·junk> Hello, 123 & 456! 2023-11-14 22:13:20.000 +00:00\n",
138
+ );
139
+ assertEquals(
140
+ recordedValues,
141
+ {
142
+ timestamp: "2023-11-14 22:13:20.000 +00:00",
143
+ level: "INF",
144
+ category: "my-app·junk",
145
+ message: "Hello, 123 & 456!",
146
+ record: info,
147
+ },
148
+ );
149
+
150
+ const longArray = new Array(150).fill(0);
151
+ const longStringAndArray: LogRecord = {
152
+ level: "info",
153
+ category: ["my-app", "junk"],
154
+ message: ["Hello, ", "a".repeat(15000), " & ", longArray, "!"],
155
+ rawMessage: "Hello, {a} & {b}!",
156
+ timestamp: 1700000000000,
157
+ properties: {},
158
+ };
159
+ let longArrayStr = "[\n";
160
+ for (let i = 0; i < Math.floor(longArray.length / 12); i++) {
161
+ longArrayStr += " 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n";
162
+ }
163
+ for (let i = 0; i < longArray.length % 12; i++) {
164
+ if (i < 1) longArrayStr += " 0";
165
+ else longArrayStr += ", 0";
166
+ if (i === longArray.length % 12 - 1) longArrayStr += "\n";
167
+ }
168
+ longArrayStr += "]";
169
+ // dnt-shim-ignore
170
+ if ("Deno" in globalThis) {
171
+ assertEquals(
172
+ getTextFormatter()(longStringAndArray),
173
+ `2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, "${
174
+ "a".repeat(15000)
175
+ }" & ${longArrayStr}!\n`,
176
+ );
177
+ } else {
178
+ assertEquals(
179
+ getTextFormatter()(longStringAndArray),
180
+ `2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, '${
181
+ "a".repeat(15000)
182
+ }' & ${longArrayStr}!\n`,
183
+ );
184
+ }
185
+ });
186
+
187
+ test("defaultTextFormatter()", () => {
188
+ assertEquals(
189
+ defaultTextFormatter(info),
190
+ "2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!\n",
191
+ );
192
+ assertEquals(
193
+ defaultTextFormatter(fatal),
194
+ "2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!\n",
195
+ );
196
+ });
197
+
198
+ test("getAnsiColorFormatter()", () => {
199
+ assertEquals(
200
+ getAnsiColorFormatter()(info),
201
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
202
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
203
+ "\x1b[2mmy-app·junk:\x1b[0m " +
204
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
205
+ );
206
+ assertEquals(
207
+ getAnsiColorFormatter({ timestampStyle: "bold" })(info),
208
+ "\x1b[1m2023-11-14 22:13:20.000 +00\x1b[0m " +
209
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
210
+ "\x1b[2mmy-app·junk:\x1b[0m " +
211
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
212
+ );
213
+ assertEquals(
214
+ getAnsiColorFormatter({ timestampStyle: null })(info),
215
+ "2023-11-14 22:13:20.000 +00 " +
216
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
217
+ "\x1b[2mmy-app·junk:\x1b[0m " +
218
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
219
+ );
220
+
221
+ assertEquals(
222
+ getAnsiColorFormatter({ timestampColor: "cyan" })(info),
223
+ "\x1b[2m\x1b[36m2023-11-14 22:13:20.000 +00\x1b[0m " +
224
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
225
+ "\x1b[2mmy-app·junk:\x1b[0m " +
226
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
227
+ );
228
+ assertEquals(
229
+ getAnsiColorFormatter({ timestampColor: null })(info),
230
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
231
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
232
+ "\x1b[2mmy-app·junk:\x1b[0m " +
233
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
234
+ );
235
+ assertEquals(
236
+ getAnsiColorFormatter({ timestampStyle: null, timestampColor: "cyan" })(
237
+ info,
238
+ ),
239
+ "\x1b[36m2023-11-14 22:13:20.000 +00\x1b[0m " +
240
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
241
+ "\x1b[2mmy-app·junk:\x1b[0m " +
242
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
243
+ );
244
+ assertEquals(
245
+ getAnsiColorFormatter({ timestampStyle: null, timestampColor: null })(info),
246
+ "2023-11-14 22:13:20.000 +00 " +
247
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
248
+ "\x1b[2mmy-app·junk:\x1b[0m " +
249
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
250
+ );
251
+
252
+ assertEquals(
253
+ getAnsiColorFormatter({ levelStyle: null })(info),
254
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
255
+ "\x1b[32mINF\x1b[0m " +
256
+ "\x1b[2mmy-app·junk:\x1b[0m " +
257
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
258
+ );
259
+ assertEquals(
260
+ getAnsiColorFormatter({ levelStyle: "dim" })(info),
261
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
262
+ "\x1b[2m\x1b[32mINF\x1b[0m " +
263
+ "\x1b[2mmy-app·junk:\x1b[0m " +
264
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
265
+ );
266
+
267
+ assertEquals(
268
+ getAnsiColorFormatter({
269
+ levelColors: {
270
+ debug: "blue",
271
+ info: "cyan",
272
+ warning: "yellow",
273
+ error: "red",
274
+ fatal: "magenta",
275
+ },
276
+ })(info),
277
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
278
+ "\x1b[1m\x1b[36mINF\x1b[0m " +
279
+ "\x1b[2mmy-app·junk:\x1b[0m " +
280
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
281
+ );
282
+ assertEquals(
283
+ getAnsiColorFormatter({
284
+ levelColors: {
285
+ debug: "blue",
286
+ info: null,
287
+ warning: "yellow",
288
+ error: "red",
289
+ fatal: "magenta",
290
+ },
291
+ levelStyle: null,
292
+ })(info),
293
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m INF " +
294
+ "\x1b[2mmy-app·junk:\x1b[0m " +
295
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
296
+ );
297
+
298
+ assertEquals(
299
+ getAnsiColorFormatter({ categoryStyle: "bold" })(info),
300
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
301
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
302
+ "\x1b[1mmy-app·junk:\x1b[0m " +
303
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
304
+ );
305
+ assertEquals(
306
+ getAnsiColorFormatter({ categoryStyle: null })(info),
307
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
308
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
309
+ "my-app·junk: " +
310
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
311
+ );
312
+
313
+ assertEquals(
314
+ getAnsiColorFormatter({ categoryColor: "cyan" })(info),
315
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
316
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
317
+ "\x1b[2m\x1b[36mmy-app·junk:\x1b[0m " +
318
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
319
+ );
320
+
321
+ let recordedValues: FormattedValues | null = null;
322
+ assertEquals(
323
+ getAnsiColorFormatter({
324
+ format(values) {
325
+ recordedValues = values;
326
+ const { timestamp, level, category, message } = values;
327
+ return `${level} <${category}> ${message} ${timestamp}`;
328
+ },
329
+ })(info),
330
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
331
+ "<\x1b[2mmy-app·junk\x1b[0m> " +
332
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m! " +
333
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m\n",
334
+ );
335
+ assertEquals(
336
+ recordedValues,
337
+ {
338
+ timestamp: "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m",
339
+ level: "\x1b[1m\x1b[32mINF\x1b[0m",
340
+ category: "\x1b[2mmy-app·junk\x1b[0m",
341
+ message: "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!",
342
+ record: info,
343
+ },
344
+ );
345
+ });
346
+
347
+ test("ansiColorFormatter()", () => {
348
+ assertEquals(
349
+ ansiColorFormatter(info),
350
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
351
+ "\x1b[1m\x1b[32mINF\x1b[0m " +
352
+ "\x1b[2mmy-app·junk:\x1b[0m " +
353
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
354
+ );
355
+ assertEquals(
356
+ ansiColorFormatter(fatal),
357
+ "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
358
+ "\x1b[1m\x1b[35mFTL\x1b[0m " +
359
+ "\x1b[2mmy-app·junk:\x1b[0m " +
360
+ "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
361
+ );
362
+ });
363
+
364
+ test("defaultConsoleFormatter()", () => {
365
+ assertEquals(
366
+ defaultConsoleFormatter(info),
367
+ [
368
+ "%c22:13:20.000 %cINF%c %cmy-app·junk %cHello, %o & %o!",
369
+ "color: gray;",
370
+ "background-color: white; color: black;",
371
+ "background-color: default;",
372
+ "color: gray;",
373
+ "color: default;",
374
+ 123,
375
+ 456,
376
+ ],
377
+ );
378
+ });
379
+
380
+ test("getJsonLinesFormatter()", () => {
381
+ const logRecord: LogRecord = {
382
+ level: "info",
383
+ category: ["my-app", "junk"],
384
+ message: ["Hello, ", 123, " & ", 456, "!"],
385
+ rawMessage: "Hello, {a} & {b}!",
386
+ timestamp: 1700000000000,
387
+ properties: { userId: "12345", requestId: "abc-def" },
388
+ };
389
+
390
+ const warningRecord: LogRecord = {
391
+ level: "warning",
392
+ category: ["auth"],
393
+ message: ["Login failed for ", "user@example.com"],
394
+ // @ts-ignore: Mimicking a raw message with a template string
395
+ rawMessage: ["Login failed for ", ""],
396
+ timestamp: 1700000000000,
397
+ properties: { attempt: 3 },
398
+ };
399
+
400
+ { // default options
401
+ const formatter = getJsonLinesFormatter();
402
+ const result = JSON.parse(formatter(logRecord));
403
+
404
+ assertEquals(result["@timestamp"], "2023-11-14T22:13:20.000Z");
405
+ assertEquals(result.level, "INFO");
406
+ assertEquals(result.message, "Hello, 123 & 456!");
407
+ assertEquals(result.logger, "my-app.junk");
408
+ assertEquals(result.properties, { userId: "12345", requestId: "abc-def" });
409
+ }
410
+
411
+ { // warning level converts to WARN
412
+ const formatter = getJsonLinesFormatter();
413
+ const result = JSON.parse(formatter(warningRecord));
414
+ assertEquals(result.level, "WARN");
415
+ }
416
+
417
+ { // categorySeparator string option
418
+ const formatter = getJsonLinesFormatter({ categorySeparator: "/" });
419
+ const result = JSON.parse(formatter(logRecord));
420
+ assertEquals(result.logger, "my-app/junk");
421
+ }
422
+
423
+ { // categorySeparator function option
424
+ const formatter = getJsonLinesFormatter({
425
+ categorySeparator: (category) => category.join("::").toUpperCase(),
426
+ });
427
+ const result = JSON.parse(formatter(logRecord));
428
+ assertEquals(result.logger, "MY-APP::JUNK");
429
+ }
430
+
431
+ { // categorySeparator function returning array
432
+ const formatter = getJsonLinesFormatter({
433
+ categorySeparator: (category) => category,
434
+ });
435
+ const result = JSON.parse(formatter(logRecord));
436
+ assertEquals(result.logger, ["my-app", "junk"]);
437
+ }
438
+
439
+ { // message template option
440
+ const formatter = getJsonLinesFormatter({ message: "template" });
441
+ const result = JSON.parse(formatter(logRecord));
442
+ assertEquals(result.message, "Hello, {a} & {b}!");
443
+
444
+ const result2 = JSON.parse(formatter(warningRecord));
445
+ assertEquals(result2.message, "Login failed for {}");
446
+ }
447
+
448
+ { // message template with string rawMessage
449
+ const stringRawRecord: LogRecord = {
450
+ ...logRecord,
451
+ rawMessage: "Simple string message",
452
+ };
453
+ const formatter = getJsonLinesFormatter({ message: "template" });
454
+ const result = JSON.parse(formatter(stringRawRecord));
455
+ assertEquals(result.message, "Simple string message");
456
+ }
457
+
458
+ { // message rendered option (default)
459
+ const formatter = getJsonLinesFormatter({ message: "rendered" });
460
+ const result = JSON.parse(formatter(logRecord));
461
+ assertEquals(result.message, "Hello, 123 & 456!");
462
+ }
463
+
464
+ { // properties flatten option
465
+ const formatter = getJsonLinesFormatter({ properties: "flatten" });
466
+ const result = JSON.parse(formatter(logRecord));
467
+ assertEquals(result.userId, "12345");
468
+ assertEquals(result.requestId, "abc-def");
469
+ assertEquals(result.properties, undefined);
470
+ }
471
+
472
+ { // properties prepend option
473
+ const formatter = getJsonLinesFormatter({ properties: "prepend:ctx_" });
474
+ const result = JSON.parse(formatter(logRecord));
475
+ assertEquals(result.ctx_userId, "12345");
476
+ assertEquals(result.ctx_requestId, "abc-def");
477
+ assertEquals(result.properties, undefined);
478
+ }
479
+
480
+ { // properties nest option
481
+ const formatter = getJsonLinesFormatter({ properties: "nest:context" });
482
+ const result = JSON.parse(formatter(logRecord));
483
+ assertEquals(result.context, { userId: "12345", requestId: "abc-def" });
484
+ assertEquals(result.properties, undefined);
485
+ }
486
+
487
+ { // properties nest option (default)
488
+ const formatter = getJsonLinesFormatter();
489
+ const result = JSON.parse(formatter(logRecord));
490
+ assertEquals(result.properties, { userId: "12345", requestId: "abc-def" });
491
+ }
492
+
493
+ { // invalid properties option - empty prepend prefix
494
+ assertThrows(
495
+ () => getJsonLinesFormatter({ properties: "prepend:" }),
496
+ TypeError,
497
+ 'Invalid properties option: "prepend:". It must be of the form "prepend:<prefix>" where <prefix> is a non-empty string.',
498
+ );
499
+ }
500
+
501
+ { // invalid properties option - invalid format
502
+ assertThrows(
503
+ () =>
504
+ getJsonLinesFormatter({
505
+ // @ts-ignore: Intentionally invalid type for testing
506
+ properties: "invalid:option",
507
+ }),
508
+ TypeError,
509
+ 'Invalid properties option: "invalid:option". It must be "flatten", "prepend:<prefix>", or "nest:<key>".',
510
+ );
511
+ }
512
+
513
+ { // combined options
514
+ const formatter = getJsonLinesFormatter({
515
+ categorySeparator: "::",
516
+ message: "template",
517
+ properties: "prepend:prop_",
518
+ });
519
+ const result = JSON.parse(formatter(logRecord));
520
+
521
+ assertEquals(result, {
522
+ "@timestamp": "2023-11-14T22:13:20.000Z",
523
+ level: "INFO",
524
+ message: "Hello, {a} & {b}!",
525
+ logger: "my-app::junk",
526
+ prop_userId: "12345",
527
+ prop_requestId: "abc-def",
528
+ });
529
+ }
530
+ });