@logtape/logtape 1.1.4 → 1.1.6

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.
@@ -1,532 +0,0 @@
1
- import { suite } from "@alinea/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
- trace: null,
271
- debug: "blue",
272
- info: "cyan",
273
- warning: "yellow",
274
- error: "red",
275
- fatal: "magenta",
276
- },
277
- })(info),
278
- "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
279
- "\x1b[1m\x1b[36mINF\x1b[0m " +
280
- "\x1b[2mmy-app·junk:\x1b[0m " +
281
- "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
282
- );
283
- assertEquals(
284
- getAnsiColorFormatter({
285
- levelColors: {
286
- trace: null,
287
- debug: "blue",
288
- info: null,
289
- warning: "yellow",
290
- error: "red",
291
- fatal: "magenta",
292
- },
293
- levelStyle: null,
294
- })(info),
295
- "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m INF " +
296
- "\x1b[2mmy-app·junk:\x1b[0m " +
297
- "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
298
- );
299
-
300
- assertEquals(
301
- getAnsiColorFormatter({ categoryStyle: "bold" })(info),
302
- "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
303
- "\x1b[1m\x1b[32mINF\x1b[0m " +
304
- "\x1b[1mmy-app·junk:\x1b[0m " +
305
- "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
306
- );
307
- assertEquals(
308
- getAnsiColorFormatter({ categoryStyle: null })(info),
309
- "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
310
- "\x1b[1m\x1b[32mINF\x1b[0m " +
311
- "my-app·junk: " +
312
- "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
313
- );
314
-
315
- assertEquals(
316
- getAnsiColorFormatter({ categoryColor: "cyan" })(info),
317
- "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
318
- "\x1b[1m\x1b[32mINF\x1b[0m " +
319
- "\x1b[2m\x1b[36mmy-app·junk:\x1b[0m " +
320
- "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
321
- );
322
-
323
- let recordedValues: FormattedValues | null = null;
324
- assertEquals(
325
- getAnsiColorFormatter({
326
- format(values) {
327
- recordedValues = values;
328
- const { timestamp, level, category, message } = values;
329
- return `${level} <${category}> ${message} ${timestamp}`;
330
- },
331
- })(info),
332
- "\x1b[1m\x1b[32mINF\x1b[0m " +
333
- "<\x1b[2mmy-app·junk\x1b[0m> " +
334
- "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m! " +
335
- "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m\n",
336
- );
337
- assertEquals(
338
- recordedValues,
339
- {
340
- timestamp: "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m",
341
- level: "\x1b[1m\x1b[32mINF\x1b[0m",
342
- category: "\x1b[2mmy-app·junk\x1b[0m",
343
- message: "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!",
344
- record: info,
345
- },
346
- );
347
- });
348
-
349
- test("ansiColorFormatter()", () => {
350
- assertEquals(
351
- ansiColorFormatter(info),
352
- "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
353
- "\x1b[1m\x1b[32mINF\x1b[0m " +
354
- "\x1b[2mmy-app·junk:\x1b[0m " +
355
- "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
356
- );
357
- assertEquals(
358
- ansiColorFormatter(fatal),
359
- "\x1b[2m2023-11-14 22:13:20.000 +00\x1b[0m " +
360
- "\x1b[1m\x1b[35mFTL\x1b[0m " +
361
- "\x1b[2mmy-app·junk:\x1b[0m " +
362
- "Hello, \x1b[33m123\x1b[39m & \x1b[33m456\x1b[39m!\n",
363
- );
364
- });
365
-
366
- test("defaultConsoleFormatter()", () => {
367
- assertEquals(
368
- defaultConsoleFormatter(info),
369
- [
370
- "%c22:13:20.000 %cINF%c %cmy-app·junk %cHello, %o & %o!",
371
- "color: gray;",
372
- "background-color: white; color: black;",
373
- "background-color: default;",
374
- "color: gray;",
375
- "color: default;",
376
- 123,
377
- 456,
378
- ],
379
- );
380
- });
381
-
382
- test("getJsonLinesFormatter()", () => {
383
- const logRecord: LogRecord = {
384
- level: "info",
385
- category: ["my-app", "junk"],
386
- message: ["Hello, ", 123, " & ", 456, "!"],
387
- rawMessage: "Hello, {a} & {b}!",
388
- timestamp: 1700000000000,
389
- properties: { userId: "12345", requestId: "abc-def" },
390
- };
391
-
392
- const warningRecord: LogRecord = {
393
- level: "warning",
394
- category: ["auth"],
395
- message: ["Login failed for ", "user@example.com"],
396
- // @ts-ignore: Mimicking a raw message with a template string
397
- rawMessage: ["Login failed for ", ""],
398
- timestamp: 1700000000000,
399
- properties: { attempt: 3 },
400
- };
401
-
402
- { // default options
403
- const formatter = getJsonLinesFormatter();
404
- const result = JSON.parse(formatter(logRecord));
405
-
406
- assertEquals(result["@timestamp"], "2023-11-14T22:13:20.000Z");
407
- assertEquals(result.level, "INFO");
408
- assertEquals(result.message, "Hello, 123 & 456!");
409
- assertEquals(result.logger, "my-app.junk");
410
- assertEquals(result.properties, { userId: "12345", requestId: "abc-def" });
411
- }
412
-
413
- { // warning level converts to WARN
414
- const formatter = getJsonLinesFormatter();
415
- const result = JSON.parse(formatter(warningRecord));
416
- assertEquals(result.level, "WARN");
417
- }
418
-
419
- { // categorySeparator string option
420
- const formatter = getJsonLinesFormatter({ categorySeparator: "/" });
421
- const result = JSON.parse(formatter(logRecord));
422
- assertEquals(result.logger, "my-app/junk");
423
- }
424
-
425
- { // categorySeparator function option
426
- const formatter = getJsonLinesFormatter({
427
- categorySeparator: (category) => category.join("::").toUpperCase(),
428
- });
429
- const result = JSON.parse(formatter(logRecord));
430
- assertEquals(result.logger, "MY-APP::JUNK");
431
- }
432
-
433
- { // categorySeparator function returning array
434
- const formatter = getJsonLinesFormatter({
435
- categorySeparator: (category) => category,
436
- });
437
- const result = JSON.parse(formatter(logRecord));
438
- assertEquals(result.logger, ["my-app", "junk"]);
439
- }
440
-
441
- { // message template option
442
- const formatter = getJsonLinesFormatter({ message: "template" });
443
- const result = JSON.parse(formatter(logRecord));
444
- assertEquals(result.message, "Hello, {a} & {b}!");
445
-
446
- const result2 = JSON.parse(formatter(warningRecord));
447
- assertEquals(result2.message, "Login failed for {}");
448
- }
449
-
450
- { // message template with string rawMessage
451
- const stringRawRecord: LogRecord = {
452
- ...logRecord,
453
- rawMessage: "Simple string message",
454
- };
455
- const formatter = getJsonLinesFormatter({ message: "template" });
456
- const result = JSON.parse(formatter(stringRawRecord));
457
- assertEquals(result.message, "Simple string message");
458
- }
459
-
460
- { // message rendered option (default)
461
- const formatter = getJsonLinesFormatter({ message: "rendered" });
462
- const result = JSON.parse(formatter(logRecord));
463
- assertEquals(result.message, "Hello, 123 & 456!");
464
- }
465
-
466
- { // properties flatten option
467
- const formatter = getJsonLinesFormatter({ properties: "flatten" });
468
- const result = JSON.parse(formatter(logRecord));
469
- assertEquals(result.userId, "12345");
470
- assertEquals(result.requestId, "abc-def");
471
- assertEquals(result.properties, undefined);
472
- }
473
-
474
- { // properties prepend option
475
- const formatter = getJsonLinesFormatter({ properties: "prepend:ctx_" });
476
- const result = JSON.parse(formatter(logRecord));
477
- assertEquals(result.ctx_userId, "12345");
478
- assertEquals(result.ctx_requestId, "abc-def");
479
- assertEquals(result.properties, undefined);
480
- }
481
-
482
- { // properties nest option
483
- const formatter = getJsonLinesFormatter({ properties: "nest:context" });
484
- const result = JSON.parse(formatter(logRecord));
485
- assertEquals(result.context, { userId: "12345", requestId: "abc-def" });
486
- assertEquals(result.properties, undefined);
487
- }
488
-
489
- { // properties nest option (default)
490
- const formatter = getJsonLinesFormatter();
491
- const result = JSON.parse(formatter(logRecord));
492
- assertEquals(result.properties, { userId: "12345", requestId: "abc-def" });
493
- }
494
-
495
- { // invalid properties option - empty prepend prefix
496
- assertThrows(
497
- () => getJsonLinesFormatter({ properties: "prepend:" }),
498
- TypeError,
499
- 'Invalid properties option: "prepend:". It must be of the form "prepend:<prefix>" where <prefix> is a non-empty string.',
500
- );
501
- }
502
-
503
- { // invalid properties option - invalid format
504
- assertThrows(
505
- () =>
506
- getJsonLinesFormatter({
507
- // @ts-ignore: Intentionally invalid type for testing
508
- properties: "invalid:option",
509
- }),
510
- TypeError,
511
- 'Invalid properties option: "invalid:option". It must be "flatten", "prepend:<prefix>", or "nest:<key>".',
512
- );
513
- }
514
-
515
- { // combined options
516
- const formatter = getJsonLinesFormatter({
517
- categorySeparator: "::",
518
- message: "template",
519
- properties: "prepend:prop_",
520
- });
521
- const result = JSON.parse(formatter(logRecord));
522
-
523
- assertEquals(result, {
524
- "@timestamp": "2023-11-14T22:13:20.000Z",
525
- level: "INFO",
526
- message: "Hello, {a} & {b}!",
527
- logger: "my-app::junk",
528
- prop_userId: "12345",
529
- prop_requestId: "abc-def",
530
- });
531
- }
532
- });