Skip to content

Commit 8f9e50c

Browse files
committed
Capture console logs in Hono middleware
1 parent 75edf88 commit 8f9e50c

File tree

3 files changed

+100
-79
lines changed

3 files changed

+100
-79
lines changed

src/hono/middleware.ts

Lines changed: 91 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { Context, Hono } from "hono";
22
import { MiddlewareHandler } from "hono/types";
33
import { performance } from "perf_hooks";
44

5+
import { AsyncLocalStorage } from "async_hooks";
56
import { ApitallyClient } from "../common/client.js";
7+
import { patchConsole } from "../common/consoleCapture.js";
68
import { consumerFromStringOrObject } from "../common/consumerRegistry.js";
79
import { parseContentLength } from "../common/headers.js";
8-
import { convertHeaders } from "../common/requestLogger.js";
10+
import { convertHeaders, LogRecord } from "../common/requestLogger.js";
911
import {
1012
getResponseBody,
1113
getResponseJson,
@@ -37,103 +39,113 @@ export function useApitally(app: Hono, config: ApitallyConfig) {
3739
}
3840

3941
function getMiddleware(client: ApitallyClient): MiddlewareHandler {
42+
const logsContext = new AsyncLocalStorage<LogRecord[]>();
43+
44+
if (client.requestLogger.config.captureLogs) {
45+
patchConsole(logsContext);
46+
}
47+
4048
return async (c, next) => {
4149
if (!client.isEnabled() || c.req.method.toUpperCase() === "OPTIONS") {
4250
await next();
4351
return;
4452
}
4553

46-
const timestamp = Date.now() / 1000;
47-
const startTime = performance.now();
48-
49-
await next();
54+
await logsContext.run([], async () => {
55+
const timestamp = Date.now() / 1000;
56+
const startTime = performance.now();
5057

51-
let response;
52-
const responseTime = performance.now() - startTime;
53-
const [responseSize, newResponse] = await measureResponseSize(c.res);
54-
const requestSize = parseContentLength(c.req.header("content-length"));
55-
56-
const consumer = getConsumer(c);
57-
client.consumerRegistry.addOrUpdateConsumer(consumer);
58-
59-
client.requestCounter.addRequest({
60-
consumer: consumer?.identifier,
61-
method: c.req.method,
62-
path: c.req.routePath,
63-
statusCode: c.res.status,
64-
responseTime,
65-
requestSize,
66-
responseSize,
67-
});
58+
await next();
6859

69-
response = newResponse;
60+
let response;
61+
const responseTime = performance.now() - startTime;
62+
const [responseSize, newResponse] = await measureResponseSize(c.res);
63+
const requestSize = parseContentLength(c.req.header("content-length"));
7064

71-
if (c.res.status === 400) {
72-
const [responseJson, newResponse] = await getResponseJson(response);
73-
const validationErrors = extractZodErrors(responseJson);
74-
validationErrors.forEach((error) => {
75-
client.validationErrorCounter.addValidationError({
76-
consumer: consumer?.identifier,
77-
method: c.req.method,
78-
path: c.req.routePath,
79-
...error,
80-
});
81-
});
82-
response = newResponse;
83-
}
65+
const consumer = getConsumer(c);
66+
client.consumerRegistry.addOrUpdateConsumer(consumer);
8467

85-
if (c.error) {
86-
client.serverErrorCounter.addServerError({
68+
client.requestCounter.addRequest({
8769
consumer: consumer?.identifier,
8870
method: c.req.method,
8971
path: c.req.routePath,
90-
type: c.error.name,
91-
msg: c.error.message,
92-
traceback: c.error.stack || "",
72+
statusCode: c.res.status,
73+
responseTime,
74+
requestSize,
75+
responseSize,
9376
});
94-
}
9577

96-
if (client.requestLogger.enabled) {
97-
let requestBody;
98-
let responseBody;
99-
let newResponse = response;
100-
const requestContentType = c.req.header("content-type");
101-
const responseContentType = c.res.headers.get("content-type");
102-
if (
103-
client.requestLogger.config.logRequestBody &&
104-
client.requestLogger.isSupportedContentType(requestContentType)
105-
) {
106-
requestBody = Buffer.from(await c.req.arrayBuffer());
107-
}
108-
if (
109-
client.requestLogger.config.logResponseBody &&
110-
client.requestLogger.isSupportedContentType(responseContentType)
111-
) {
112-
[responseBody, newResponse] = await getResponseBody(response);
78+
response = newResponse;
79+
80+
if (c.res.status === 400) {
81+
const [responseJson, newResponse] = await getResponseJson(response);
82+
const validationErrors = extractZodErrors(responseJson);
83+
validationErrors.forEach((error) => {
84+
client.validationErrorCounter.addValidationError({
85+
consumer: consumer?.identifier,
86+
method: c.req.method,
87+
path: c.req.routePath,
88+
...error,
89+
});
90+
});
11391
response = newResponse;
11492
}
115-
client.requestLogger.logRequest(
116-
{
117-
timestamp,
93+
94+
if (c.error) {
95+
client.serverErrorCounter.addServerError({
96+
consumer: consumer?.identifier,
11897
method: c.req.method,
11998
path: c.req.routePath,
120-
url: c.req.url,
121-
headers: convertHeaders(c.req.header()),
122-
size: Number(requestSize),
123-
consumer: consumer?.identifier,
124-
body: requestBody,
125-
},
126-
{
127-
statusCode: c.res.status,
128-
responseTime: responseTime / 1000,
129-
headers: convertHeaders(c.res.headers),
130-
size: responseSize,
131-
body: responseBody,
132-
},
133-
c.error,
134-
);
135-
}
136-
c.res = response;
99+
type: c.error.name,
100+
msg: c.error.message,
101+
traceback: c.error.stack || "",
102+
});
103+
}
104+
105+
if (client.requestLogger.enabled) {
106+
let requestBody;
107+
let responseBody;
108+
let newResponse = response;
109+
const requestContentType = c.req.header("content-type");
110+
const responseContentType = c.res.headers.get("content-type");
111+
if (
112+
client.requestLogger.config.logRequestBody &&
113+
client.requestLogger.isSupportedContentType(requestContentType)
114+
) {
115+
requestBody = Buffer.from(await c.req.arrayBuffer());
116+
}
117+
if (
118+
client.requestLogger.config.logResponseBody &&
119+
client.requestLogger.isSupportedContentType(responseContentType)
120+
) {
121+
[responseBody, newResponse] = await getResponseBody(response);
122+
response = newResponse;
123+
}
124+
const logs = logsContext.getStore();
125+
client.requestLogger.logRequest(
126+
{
127+
timestamp,
128+
method: c.req.method,
129+
path: c.req.routePath,
130+
url: c.req.url,
131+
headers: convertHeaders(c.req.header()),
132+
size: Number(requestSize),
133+
consumer: consumer?.identifier,
134+
body: requestBody,
135+
},
136+
{
137+
statusCode: c.res.status,
138+
responseTime: responseTime / 1000,
139+
headers: convertHeaders(c.res.headers),
140+
size: responseSize,
141+
body: responseBody,
142+
},
143+
c.error,
144+
logs,
145+
);
146+
}
147+
c.res = response;
148+
});
137149
};
138150
}
139151

tests/hono/app.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ describe("Middleware for Hono", () => {
113113
]);
114114
expect(call[1].body).toBeInstanceOf(Buffer);
115115
expect(call[1].body!.toString()).toMatch(/^Hello John!/);
116+
expect(call[3]).toBeDefined();
117+
expect(call[3]).toHaveLength(2);
118+
expect(call[3]![0].level).toBe("log");
119+
expect(call[3]![0].message).toBe("Test 1");
120+
expect(call[3]![1].level).toBe("warn");
121+
expect(call[3]![1].message).toBe("Test 2");
116122
spy.mockReset();
117123

118124
const body = JSON.stringify({ name: "John", age: 20 });

tests/hono/app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const getApp = async () => {
1919
logRequestBody: true,
2020
logResponseHeaders: true,
2121
logResponseBody: true,
22+
captureLogs: true,
2223
},
2324
});
2425

@@ -33,6 +34,8 @@ export const getApp = async () => {
3334
),
3435
(c) => {
3536
setConsumer(c, "test");
37+
console.log("Test 1");
38+
console.warn("Test 2");
3639
return c.text(
3740
`Hello ${c.req.query("name")}! You are ${c.req.query("age")} years old!`,
3841
);

0 commit comments

Comments
 (0)