@logtape/hono 1.3.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,670 @@
1
+ import { suite } from "@alinea/suite";
2
+ import { assert, assertEquals, assertExists } from "@std/assert";
3
+ import { configure, type LogRecord, reset } from "@logtape/logtape";
4
+ import { Hono } from "hono";
5
+ import { honoLogger } from "./mod.ts";
6
+
7
+ const test = suite(import.meta);
8
+
9
+ // Test fixture: Collect log records, filtering out internal LogTape meta logs
10
+ function createTestSink(): {
11
+ sink: (record: LogRecord) => void;
12
+ logs: LogRecord[];
13
+ } {
14
+ const logs: LogRecord[] = [];
15
+ return {
16
+ sink: (record: LogRecord) => {
17
+ if (record.category[0] !== "logtape") {
18
+ logs.push(record);
19
+ }
20
+ },
21
+ logs,
22
+ };
23
+ }
24
+
25
+ // Setup helper
26
+ async function setupLogtape(): Promise<{
27
+ logs: LogRecord[];
28
+ cleanup: () => Promise<void>;
29
+ }> {
30
+ const { sink, logs } = createTestSink();
31
+ await configure({
32
+ sinks: { test: sink },
33
+ loggers: [{ category: [], sinks: ["test"] }],
34
+ });
35
+ return { logs, cleanup: () => reset() };
36
+ }
37
+
38
+ // ============================================
39
+ // Basic Middleware Creation Tests
40
+ // ============================================
41
+
42
+ test("honoLogger(): creates a middleware function", async () => {
43
+ const { cleanup } = await setupLogtape();
44
+ try {
45
+ const middleware = honoLogger();
46
+ assertEquals(typeof middleware, "function");
47
+ } finally {
48
+ await cleanup();
49
+ }
50
+ });
51
+
52
+ test("honoLogger(): logs request after response", async () => {
53
+ const { logs, cleanup } = await setupLogtape();
54
+ try {
55
+ const app = new Hono();
56
+ app.use(honoLogger());
57
+ app.get("/test", (c) => c.text("Hello"));
58
+
59
+ const res = await app.request("/test");
60
+ assertEquals(res.status, 200);
61
+ assertEquals(logs.length, 1);
62
+ } finally {
63
+ await cleanup();
64
+ }
65
+ });
66
+
67
+ // ============================================
68
+ // Category Configuration Tests
69
+ // ============================================
70
+
71
+ test("honoLogger(): uses default category ['hono']", async () => {
72
+ const { logs, cleanup } = await setupLogtape();
73
+ try {
74
+ const app = new Hono();
75
+ app.use(honoLogger());
76
+ app.get("/test", (c) => c.text("Hello"));
77
+
78
+ await app.request("/test");
79
+
80
+ assertEquals(logs.length, 1);
81
+ assertEquals(logs[0].category, ["hono"]);
82
+ } finally {
83
+ await cleanup();
84
+ }
85
+ });
86
+
87
+ test("honoLogger(): uses custom category array", async () => {
88
+ const { logs, cleanup } = await setupLogtape();
89
+ try {
90
+ const app = new Hono();
91
+ app.use(honoLogger({ category: ["myapp", "http"] }));
92
+ app.get("/test", (c) => c.text("Hello"));
93
+
94
+ await app.request("/test");
95
+
96
+ assertEquals(logs.length, 1);
97
+ assertEquals(logs[0].category, ["myapp", "http"]);
98
+ } finally {
99
+ await cleanup();
100
+ }
101
+ });
102
+
103
+ test("honoLogger(): accepts string category", async () => {
104
+ const { logs, cleanup } = await setupLogtape();
105
+ try {
106
+ const app = new Hono();
107
+ app.use(honoLogger({ category: "myapp" }));
108
+ app.get("/test", (c) => c.text("Hello"));
109
+
110
+ await app.request("/test");
111
+
112
+ assertEquals(logs.length, 1);
113
+ assertEquals(logs[0].category, ["myapp"]);
114
+ } finally {
115
+ await cleanup();
116
+ }
117
+ });
118
+
119
+ // ============================================
120
+ // Log Level Tests
121
+ // ============================================
122
+
123
+ test("honoLogger(): uses default log level 'info'", async () => {
124
+ const { logs, cleanup } = await setupLogtape();
125
+ try {
126
+ const app = new Hono();
127
+ app.use(honoLogger());
128
+ app.get("/test", (c) => c.text("Hello"));
129
+
130
+ await app.request("/test");
131
+
132
+ assertEquals(logs.length, 1);
133
+ assertEquals(logs[0].level, "info");
134
+ } finally {
135
+ await cleanup();
136
+ }
137
+ });
138
+
139
+ test("honoLogger(): uses custom log level 'debug'", async () => {
140
+ const { logs, cleanup } = await setupLogtape();
141
+ try {
142
+ const app = new Hono();
143
+ app.use(honoLogger({ level: "debug" }));
144
+ app.get("/test", (c) => c.text("Hello"));
145
+
146
+ await app.request("/test");
147
+
148
+ assertEquals(logs.length, 1);
149
+ assertEquals(logs[0].level, "debug");
150
+ } finally {
151
+ await cleanup();
152
+ }
153
+ });
154
+
155
+ test("honoLogger(): uses custom log level 'warning'", async () => {
156
+ const { logs, cleanup } = await setupLogtape();
157
+ try {
158
+ const app = new Hono();
159
+ app.use(honoLogger({ level: "warning" }));
160
+ app.get("/test", (c) => c.text("Hello"));
161
+
162
+ await app.request("/test");
163
+
164
+ assertEquals(logs.length, 1);
165
+ assertEquals(logs[0].level, "warning");
166
+ } finally {
167
+ await cleanup();
168
+ }
169
+ });
170
+
171
+ // ============================================
172
+ // Format Tests - Combined (default)
173
+ // ============================================
174
+
175
+ test("honoLogger(): combined format logs structured properties", async () => {
176
+ const { logs, cleanup } = await setupLogtape();
177
+ try {
178
+ const app = new Hono();
179
+ app.use(honoLogger({ format: "combined" }));
180
+ app.get("/test", (c) => c.text("Hello"));
181
+
182
+ await app.request("/test", {
183
+ headers: {
184
+ "User-Agent": "test-agent/1.0",
185
+ "Referer": "http://example.com",
186
+ },
187
+ });
188
+
189
+ assertEquals(logs.length, 1);
190
+ const props = logs[0].properties;
191
+ assertEquals(props.method, "GET");
192
+ assert((props.url as string).includes("/test"));
193
+ assertEquals(props.path, "/test");
194
+ assertEquals(props.status, 200);
195
+ assertExists(props.responseTime);
196
+ assertEquals(props.userAgent, "test-agent/1.0");
197
+ assertEquals(props.referrer, "http://example.com");
198
+ } finally {
199
+ await cleanup();
200
+ }
201
+ });
202
+
203
+ // ============================================
204
+ // Format Tests - Common
205
+ // ============================================
206
+
207
+ test("honoLogger(): common format excludes referrer and userAgent", async () => {
208
+ const { logs, cleanup } = await setupLogtape();
209
+ try {
210
+ const app = new Hono();
211
+ app.use(honoLogger({ format: "common" }));
212
+ app.get("/test", (c) => c.text("Hello"));
213
+
214
+ await app.request("/test", {
215
+ headers: {
216
+ "User-Agent": "test-agent/1.0",
217
+ "Referer": "http://example.com",
218
+ },
219
+ });
220
+
221
+ assertEquals(logs.length, 1);
222
+ const props = logs[0].properties;
223
+ assertEquals(props.method, "GET");
224
+ assertEquals(props.path, "/test");
225
+ assertEquals(props.status, 200);
226
+ assertEquals(props.referrer, undefined);
227
+ assertEquals(props.userAgent, undefined);
228
+ } finally {
229
+ await cleanup();
230
+ }
231
+ });
232
+
233
+ // ============================================
234
+ // Format Tests - Dev
235
+ // ============================================
236
+
237
+ test("honoLogger(): dev format returns string message", async () => {
238
+ const { logs, cleanup } = await setupLogtape();
239
+ try {
240
+ const app = new Hono();
241
+ app.use(honoLogger({ format: "dev" }));
242
+ app.post("/api/users", (c) => {
243
+ c.status(201);
244
+ return c.text("Created");
245
+ });
246
+
247
+ await app.request("/api/users", { method: "POST" });
248
+
249
+ assertEquals(logs.length, 1);
250
+ const msg = logs[0].rawMessage;
251
+ assert(msg.includes("POST"));
252
+ assert(msg.includes("/api/users"));
253
+ assert(msg.includes("201"));
254
+ assert(msg.includes("ms"));
255
+ } finally {
256
+ await cleanup();
257
+ }
258
+ });
259
+
260
+ // ============================================
261
+ // Format Tests - Short
262
+ // ============================================
263
+
264
+ test("honoLogger(): short format includes url", async () => {
265
+ const { logs, cleanup } = await setupLogtape();
266
+ try {
267
+ const app = new Hono();
268
+ app.use(honoLogger({ format: "short" }));
269
+ app.get("/test", (c) => c.text("Hello"));
270
+
271
+ await app.request("/test");
272
+
273
+ assertEquals(logs.length, 1);
274
+ const msg = logs[0].rawMessage;
275
+ assert(msg.includes("GET"));
276
+ assert(msg.includes("/test"));
277
+ assert(msg.includes("200"));
278
+ assert(msg.includes("ms"));
279
+ } finally {
280
+ await cleanup();
281
+ }
282
+ });
283
+
284
+ // ============================================
285
+ // Format Tests - Tiny
286
+ // ============================================
287
+
288
+ test("honoLogger(): tiny format is minimal", async () => {
289
+ const { logs, cleanup } = await setupLogtape();
290
+ try {
291
+ const app = new Hono();
292
+ app.use(honoLogger({ format: "tiny" }));
293
+ app.get("/test", (c) => c.text("Hello"));
294
+
295
+ const res = await app.request("/test");
296
+ assertEquals(res.status, 200);
297
+
298
+ assertEquals(logs.length, 1);
299
+ const msg = logs[0].rawMessage;
300
+ assert(msg.includes("GET"));
301
+ assert(msg.includes("/test"));
302
+ assert(msg.includes("200"));
303
+ assert(msg.includes("ms"));
304
+ } finally {
305
+ await cleanup();
306
+ }
307
+ });
308
+
309
+ // ============================================
310
+ // Custom Format Function Tests
311
+ // ============================================
312
+
313
+ test("honoLogger(): custom format returning string", async () => {
314
+ const { logs, cleanup } = await setupLogtape();
315
+ try {
316
+ const app = new Hono();
317
+ app.use(honoLogger({
318
+ format: (c, _responseTime) => `Custom: ${c.req.method} ${c.res.status}`,
319
+ }));
320
+ app.delete("/test", (c) => {
321
+ c.status(204);
322
+ return c.body(null);
323
+ });
324
+
325
+ await app.request("/test", { method: "DELETE" });
326
+
327
+ assertEquals(logs.length, 1);
328
+ assertEquals(logs[0].rawMessage, "Custom: DELETE 204");
329
+ } finally {
330
+ await cleanup();
331
+ }
332
+ });
333
+
334
+ test("honoLogger(): custom format returning object", async () => {
335
+ const { logs, cleanup } = await setupLogtape();
336
+ try {
337
+ const app = new Hono();
338
+ app.use(honoLogger({
339
+ format: (c, responseTime) => ({
340
+ customMethod: c.req.method,
341
+ customStatus: c.res.status,
342
+ customDuration: responseTime,
343
+ }),
344
+ }));
345
+ app.patch("/test", (c) => {
346
+ c.status(202);
347
+ return c.text("Accepted");
348
+ });
349
+
350
+ await app.request("/test", { method: "PATCH" });
351
+
352
+ assertEquals(logs.length, 1);
353
+ assertEquals(logs[0].properties.customMethod, "PATCH");
354
+ assertEquals(logs[0].properties.customStatus, 202);
355
+ assertExists(logs[0].properties.customDuration);
356
+ } finally {
357
+ await cleanup();
358
+ }
359
+ });
360
+
361
+ // ============================================
362
+ // Skip Function Tests
363
+ // ============================================
364
+
365
+ test("honoLogger(): skip function prevents logging when returns true", async () => {
366
+ const { logs, cleanup } = await setupLogtape();
367
+ try {
368
+ const app = new Hono();
369
+ app.use(honoLogger({
370
+ skip: () => true,
371
+ }));
372
+ app.get("/test", (c) => c.text("Hello"));
373
+
374
+ await app.request("/test");
375
+
376
+ assertEquals(logs.length, 0);
377
+ } finally {
378
+ await cleanup();
379
+ }
380
+ });
381
+
382
+ test("honoLogger(): skip function allows logging when returns false", async () => {
383
+ const { logs, cleanup } = await setupLogtape();
384
+ try {
385
+ const app = new Hono();
386
+ app.use(honoLogger({
387
+ skip: () => false,
388
+ }));
389
+ app.get("/test", (c) => c.text("Hello"));
390
+
391
+ await app.request("/test");
392
+
393
+ assertEquals(logs.length, 1);
394
+ } finally {
395
+ await cleanup();
396
+ }
397
+ });
398
+
399
+ test("honoLogger(): skip function receives context", async () => {
400
+ const { logs, cleanup } = await setupLogtape();
401
+ try {
402
+ const app = new Hono();
403
+ app.use(honoLogger({
404
+ skip: (c) => c.req.path === "/health",
405
+ }));
406
+ app.get("/test", (c) => c.text("Hello"));
407
+ app.get("/health", (c) => c.text("OK"));
408
+
409
+ // Health endpoint should be skipped
410
+ await app.request("/health");
411
+ assertEquals(logs.length, 0);
412
+
413
+ // Other endpoints should be logged
414
+ await app.request("/test");
415
+ assertEquals(logs.length, 1);
416
+ } finally {
417
+ await cleanup();
418
+ }
419
+ });
420
+
421
+ // ============================================
422
+ // logRequest (Immediate) Mode Tests
423
+ // ============================================
424
+
425
+ test("honoLogger(): logRequest mode logs at request start", async () => {
426
+ const { logs, cleanup } = await setupLogtape();
427
+ try {
428
+ const app = new Hono();
429
+ app.use(honoLogger({ logRequest: true }));
430
+ app.get("/test", (c) => c.text("Hello"));
431
+
432
+ await app.request("/test");
433
+
434
+ assertEquals(logs.length, 1);
435
+ assertEquals(logs[0].properties.responseTime, 0); // Zero because it's immediate
436
+ } finally {
437
+ await cleanup();
438
+ }
439
+ });
440
+
441
+ test("honoLogger(): non-logRequest mode logs after response", async () => {
442
+ const { logs, cleanup } = await setupLogtape();
443
+ try {
444
+ const app = new Hono();
445
+ app.use(honoLogger({ logRequest: false }));
446
+ app.get("/test", async (c) => {
447
+ // Small delay to ensure non-zero response time
448
+ await new Promise((resolve) => setTimeout(resolve, 5));
449
+ return c.text("Hello");
450
+ });
451
+
452
+ await app.request("/test");
453
+
454
+ assertEquals(logs.length, 1);
455
+ assert((logs[0].properties.responseTime as number) >= 0);
456
+ } finally {
457
+ await cleanup();
458
+ }
459
+ });
460
+
461
+ // ============================================
462
+ // Request Property Tests
463
+ // ============================================
464
+
465
+ test("honoLogger(): logs correct method", async () => {
466
+ const { logs, cleanup } = await setupLogtape();
467
+ try {
468
+ const app = new Hono();
469
+ app.use(honoLogger());
470
+ app.get("/test", (c) => c.text("Hello"));
471
+ app.post("/test", (c) => c.text("Hello"));
472
+ app.put("/test", (c) => c.text("Hello"));
473
+ app.delete("/test", (c) => c.text("Hello"));
474
+
475
+ const methods = ["GET", "POST", "PUT", "DELETE"];
476
+ for (const method of methods) {
477
+ await app.request("/test", { method });
478
+ }
479
+
480
+ assertEquals(logs.length, methods.length);
481
+ for (let i = 0; i < methods.length; i++) {
482
+ assertEquals(logs[i].properties.method, methods[i]);
483
+ }
484
+ } finally {
485
+ await cleanup();
486
+ }
487
+ });
488
+
489
+ test("honoLogger(): logs path correctly", async () => {
490
+ const { logs, cleanup } = await setupLogtape();
491
+ try {
492
+ const app = new Hono();
493
+ app.use(honoLogger());
494
+ app.get("/api/v1/users", (c) => c.text("Hello"));
495
+
496
+ await app.request("/api/v1/users");
497
+
498
+ assertEquals(logs.length, 1);
499
+ assertEquals(logs[0].properties.path, "/api/v1/users");
500
+ } finally {
501
+ await cleanup();
502
+ }
503
+ });
504
+
505
+ test("honoLogger(): logs status code", async () => {
506
+ const { logs, cleanup } = await setupLogtape();
507
+ try {
508
+ const app = new Hono();
509
+ app.use(honoLogger());
510
+
511
+ app.get("/200", (c) => c.text("OK"));
512
+ app.get("/201", (c) => {
513
+ c.status(201);
514
+ return c.text("Created");
515
+ });
516
+ app.get("/400", (c) => {
517
+ c.status(400);
518
+ return c.text("Bad Request");
519
+ });
520
+ app.get("/500", (c) => {
521
+ c.status(500);
522
+ return c.text("Error");
523
+ });
524
+
525
+ const paths = ["/200", "/201", "/400", "/500"];
526
+ const expectedStatuses = [200, 201, 400, 500];
527
+
528
+ for (const path of paths) {
529
+ await app.request(path);
530
+ }
531
+
532
+ assertEquals(logs.length, paths.length);
533
+ for (let i = 0; i < paths.length; i++) {
534
+ assertEquals(logs[i].properties.status, expectedStatuses[i]);
535
+ }
536
+ } finally {
537
+ await cleanup();
538
+ }
539
+ });
540
+
541
+ test("honoLogger(): logs response time as number", async () => {
542
+ const { logs, cleanup } = await setupLogtape();
543
+ try {
544
+ const app = new Hono();
545
+ app.use(honoLogger());
546
+ app.get("/test", (c) => c.text("Hello"));
547
+
548
+ await app.request("/test");
549
+
550
+ assertEquals(logs.length, 1);
551
+ assertEquals(typeof logs[0].properties.responseTime, "number");
552
+ assert((logs[0].properties.responseTime as number) >= 0);
553
+ } finally {
554
+ await cleanup();
555
+ }
556
+ });
557
+
558
+ test("honoLogger(): logs user agent", async () => {
559
+ const { logs, cleanup } = await setupLogtape();
560
+ try {
561
+ const app = new Hono();
562
+ app.use(honoLogger());
563
+ app.get("/test", (c) => c.text("Hello"));
564
+
565
+ await app.request("/test", {
566
+ headers: { "User-Agent": "TestClient/1.0" },
567
+ });
568
+
569
+ assertEquals(logs.length, 1);
570
+ assertEquals(logs[0].properties.userAgent, "TestClient/1.0");
571
+ } finally {
572
+ await cleanup();
573
+ }
574
+ });
575
+
576
+ test("honoLogger(): logs referrer", async () => {
577
+ const { logs, cleanup } = await setupLogtape();
578
+ try {
579
+ const app = new Hono();
580
+ app.use(honoLogger());
581
+ app.get("/test", (c) => c.text("Hello"));
582
+
583
+ await app.request("/test", {
584
+ headers: { "Referer": "https://example.com/page" },
585
+ });
586
+
587
+ assertEquals(logs.length, 1);
588
+ assertEquals(logs[0].properties.referrer, "https://example.com/page");
589
+ } finally {
590
+ await cleanup();
591
+ }
592
+ });
593
+
594
+ // ============================================
595
+ // Multiple Requests Tests
596
+ // ============================================
597
+
598
+ test("honoLogger(): handles multiple sequential requests", async () => {
599
+ const { logs, cleanup } = await setupLogtape();
600
+ try {
601
+ const app = new Hono();
602
+ app.use(honoLogger());
603
+ app.get("/path/:id", (c) => c.text(`Path: ${c.req.param("id")}`));
604
+
605
+ for (let i = 0; i < 5; i++) {
606
+ await app.request(`/path/${i}`);
607
+ }
608
+
609
+ assertEquals(logs.length, 5);
610
+ for (let i = 0; i < 5; i++) {
611
+ assertEquals(logs[i].properties.path, `/path/${i}`);
612
+ }
613
+ } finally {
614
+ await cleanup();
615
+ }
616
+ });
617
+
618
+ // ============================================
619
+ // Edge Cases
620
+ // ============================================
621
+
622
+ test("honoLogger(): handles missing user-agent", async () => {
623
+ const { logs, cleanup } = await setupLogtape();
624
+ try {
625
+ const app = new Hono();
626
+ app.use(honoLogger());
627
+ app.get("/test", (c) => c.text("Hello"));
628
+
629
+ await app.request("/test");
630
+
631
+ assertEquals(logs.length, 1);
632
+ assertEquals(logs[0].properties.userAgent, undefined);
633
+ } finally {
634
+ await cleanup();
635
+ }
636
+ });
637
+
638
+ test("honoLogger(): handles missing referrer", async () => {
639
+ const { logs, cleanup } = await setupLogtape();
640
+ try {
641
+ const app = new Hono();
642
+ app.use(honoLogger());
643
+ app.get("/test", (c) => c.text("Hello"));
644
+
645
+ await app.request("/test");
646
+
647
+ assertEquals(logs.length, 1);
648
+ assertEquals(logs[0].properties.referrer, undefined);
649
+ } finally {
650
+ await cleanup();
651
+ }
652
+ });
653
+
654
+ test("honoLogger(): handles query parameters in url", async () => {
655
+ const { logs, cleanup } = await setupLogtape();
656
+ try {
657
+ const app = new Hono();
658
+ app.use(honoLogger());
659
+ app.get("/search", (c) => c.text(`Query: ${c.req.query("q")}`));
660
+
661
+ await app.request("/search?q=test&limit=10");
662
+
663
+ assertEquals(logs.length, 1);
664
+ assertEquals(logs[0].properties.path, "/search");
665
+ assert((logs[0].properties.url as string).includes("q=test"));
666
+ assert((logs[0].properties.url as string).includes("limit=10"));
667
+ } finally {
668
+ await cleanup();
669
+ }
670
+ });