apitally 0.19.0 → 0.21.0
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.
- package/README.md +98 -43
- package/dist/adonisjs/middleware.cjs +3 -0
- package/dist/adonisjs/middleware.cjs.map +1 -1
- package/dist/adonisjs/middleware.js +3 -0
- package/dist/adonisjs/middleware.js.map +1 -1
- package/dist/adonisjs/provider.cjs +55 -25
- package/dist/adonisjs/provider.cjs.map +1 -1
- package/dist/adonisjs/provider.js +55 -25
- package/dist/adonisjs/provider.js.map +1 -1
- package/dist/common/client.cjs +4 -3
- package/dist/common/client.cjs.map +1 -1
- package/dist/common/client.js +4 -3
- package/dist/common/client.js.map +1 -1
- package/dist/common/requestLogger.cjs +5 -1
- package/dist/common/requestLogger.cjs.map +1 -1
- package/dist/common/requestLogger.d.cts +1 -1
- package/dist/common/requestLogger.d.ts +1 -1
- package/dist/common/requestLogger.js +5 -1
- package/dist/common/requestLogger.js.map +1 -1
- package/dist/common/response.cjs +12 -6
- package/dist/common/response.cjs.map +1 -1
- package/dist/common/response.d.cts +3 -3
- package/dist/common/response.d.ts +3 -3
- package/dist/common/response.js +12 -6
- package/dist/common/response.js.map +1 -1
- package/dist/common/validationErrorCounter.cjs +2 -2
- package/dist/common/validationErrorCounter.cjs.map +1 -1
- package/dist/common/validationErrorCounter.js +2 -2
- package/dist/common/validationErrorCounter.js.map +1 -1
- package/dist/elysia/index.cjs +1586 -0
- package/dist/elysia/index.cjs.map +1 -0
- package/dist/elysia/index.d.cts +9 -0
- package/dist/elysia/index.d.ts +9 -0
- package/dist/elysia/index.js +1556 -0
- package/dist/elysia/index.js.map +1 -0
- package/dist/elysia/plugin.cjs +1580 -0
- package/dist/elysia/plugin.cjs.map +1 -0
- package/dist/elysia/plugin.d.cts +53 -0
- package/dist/elysia/plugin.d.ts +53 -0
- package/dist/elysia/plugin.js +1556 -0
- package/dist/elysia/plugin.js.map +1 -0
- package/dist/elysia/utils.cjs +98 -0
- package/dist/elysia/utils.cjs.map +1 -0
- package/dist/elysia/utils.d.cts +12 -0
- package/dist/elysia/utils.d.ts +12 -0
- package/dist/elysia/utils.js +78 -0
- package/dist/elysia/utils.js.map +1 -0
- package/dist/express/index.cjs +58 -25
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.js +58 -25
- package/dist/express/index.js.map +1 -1
- package/dist/express/middleware.cjs +58 -25
- package/dist/express/middleware.cjs.map +1 -1
- package/dist/express/middleware.js +58 -25
- package/dist/express/middleware.js.map +1 -1
- package/dist/fastify/index.cjs +58 -25
- package/dist/fastify/index.cjs.map +1 -1
- package/dist/fastify/index.js +58 -25
- package/dist/fastify/index.js.map +1 -1
- package/dist/fastify/plugin.cjs +58 -25
- package/dist/fastify/plugin.cjs.map +1 -1
- package/dist/fastify/plugin.js +58 -25
- package/dist/fastify/plugin.js.map +1 -1
- package/dist/h3/index.cjs +24 -11
- package/dist/h3/index.cjs.map +1 -1
- package/dist/h3/index.js +24 -11
- package/dist/h3/index.js.map +1 -1
- package/dist/h3/plugin.cjs +24 -11
- package/dist/h3/plugin.cjs.map +1 -1
- package/dist/h3/plugin.js +24 -11
- package/dist/h3/plugin.js.map +1 -1
- package/dist/hapi/index.cjs +6134 -0
- package/dist/hapi/index.cjs.map +1 -0
- package/dist/hapi/index.d.cts +9 -0
- package/dist/hapi/index.d.ts +9 -0
- package/dist/hapi/index.js +6124 -0
- package/dist/hapi/index.js.map +1 -0
- package/dist/hapi/plugin.cjs +6131 -0
- package/dist/hapi/plugin.cjs.map +1 -0
- package/dist/hapi/plugin.d.cts +36 -0
- package/dist/hapi/plugin.d.ts +36 -0
- package/dist/hapi/plugin.js +6124 -0
- package/dist/hapi/plugin.js.map +1 -0
- package/dist/hapi/utils.cjs +98 -0
- package/dist/hapi/utils.cjs.map +1 -0
- package/dist/hapi/utils.d.cts +14 -0
- package/dist/hapi/utils.d.ts +14 -0
- package/dist/hapi/utils.js +77 -0
- package/dist/hapi/utils.js.map +1 -0
- package/dist/hono/index.cjs +25 -12
- package/dist/hono/index.cjs.map +1 -1
- package/dist/hono/index.js +25 -12
- package/dist/hono/index.js.map +1 -1
- package/dist/hono/middleware.cjs +25 -12
- package/dist/hono/middleware.cjs.map +1 -1
- package/dist/hono/middleware.js +25 -12
- package/dist/hono/middleware.js.map +1 -1
- package/dist/koa/index.cjs +13 -6
- package/dist/koa/index.cjs.map +1 -1
- package/dist/koa/index.js +13 -6
- package/dist/koa/index.js.map +1 -1
- package/dist/koa/middleware.cjs +13 -6
- package/dist/koa/middleware.cjs.map +1 -1
- package/dist/koa/middleware.js +13 -6
- package/dist/koa/middleware.js.map +1 -1
- package/dist/loggers/hapi.cjs +54 -0
- package/dist/loggers/hapi.cjs.map +1 -0
- package/dist/loggers/hapi.d.cts +10 -0
- package/dist/loggers/hapi.d.ts +10 -0
- package/dist/loggers/hapi.js +31 -0
- package/dist/loggers/hapi.js.map +1 -0
- package/dist/loggers/index.cjs +81 -28
- package/dist/loggers/index.cjs.map +1 -1
- package/dist/loggers/index.d.cts +2 -0
- package/dist/loggers/index.d.ts +2 -0
- package/dist/loggers/index.js +80 -28
- package/dist/loggers/index.js.map +1 -1
- package/dist/loggers/pino.cjs +39 -13
- package/dist/loggers/pino.cjs.map +1 -1
- package/dist/loggers/pino.d.cts +1 -1
- package/dist/loggers/pino.d.ts +1 -1
- package/dist/loggers/pino.js +39 -13
- package/dist/loggers/pino.js.map +1 -1
- package/dist/nestjs/index.cjs +158 -118
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +156 -117
- package/dist/nestjs/index.js.map +1 -1
- package/package.json +42 -2
package/README.md
CHANGED
|
@@ -23,12 +23,14 @@ This SDK for Apitally currently supports the following Node.js web
|
|
|
23
23
|
frameworks:
|
|
24
24
|
|
|
25
25
|
- [Express](https://docs.apitally.io/frameworks/express)
|
|
26
|
-
- [NestJS](https://docs.apitally.io/frameworks/nestjs)
|
|
27
26
|
- [Fastify](https://docs.apitally.io/frameworks/fastify)
|
|
28
|
-
- [
|
|
27
|
+
- [NestJS](https://docs.apitally.io/frameworks/nestjs)
|
|
28
|
+
- [AdonisJS](https://docs.apitally.io/frameworks/adonisjs)
|
|
29
29
|
- [Hono](https://docs.apitally.io/frameworks/hono)
|
|
30
30
|
- [H3](https://docs.apitally.io/frameworks/h3) (v2)
|
|
31
|
-
- [
|
|
31
|
+
- [Elysia](https://docs.apitally.io/frameworks/elysia)
|
|
32
|
+
- [Koa](https://docs.apitally.io/frameworks/koa)
|
|
33
|
+
- [Hapi](https://docs.apitally.io/frameworks/hapi)
|
|
32
34
|
|
|
33
35
|
Learn more about Apitally on our 🌎 [website](https://apitally.io) or check out
|
|
34
36
|
the 📚 [documentation](https://docs.apitally.io).
|
|
@@ -89,25 +91,6 @@ useApitally(app, {
|
|
|
89
91
|
});
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
### NestJS
|
|
93
|
-
|
|
94
|
-
This is an example of how to use the Apitally middleware with a NestJS
|
|
95
|
-
application. For further instructions, see our
|
|
96
|
-
[setup guide for NestJS](https://docs.apitally.io/frameworks/nestjs).
|
|
97
|
-
|
|
98
|
-
```javascript
|
|
99
|
-
const { NestFactory } = require("@nestjs/core");
|
|
100
|
-
const { useApitally } = require("apitally/nestjs");
|
|
101
|
-
const { AppModule } = require("./app.module");
|
|
102
|
-
|
|
103
|
-
const app = await NestFactory.create(AppModule);
|
|
104
|
-
|
|
105
|
-
await useApitally(app, {
|
|
106
|
-
clientId: "your-client-id",
|
|
107
|
-
env: "dev", // or "prod" etc.
|
|
108
|
-
});
|
|
109
|
-
```
|
|
110
|
-
|
|
111
94
|
### Fastify
|
|
112
95
|
|
|
113
96
|
This is an example of how to register the Apitally plugin with a Fastify
|
|
@@ -142,24 +125,49 @@ fastify.register((instance, opts, done) => {
|
|
|
142
125
|
|
|
143
126
|
_Note:_ If your project uses ES modules you can use `await fastify.register(...)` and don't need to wrap your routes in a plugin. See the [Fastify V4 migration guide](https://fastify.dev/docs/latest/Guides/Migration-Guide-V4/#synchronous-route-definitions-2954) for more details.
|
|
144
127
|
|
|
145
|
-
###
|
|
128
|
+
### NestJS
|
|
146
129
|
|
|
147
|
-
This is an example of how to use the Apitally middleware with a
|
|
148
|
-
For further instructions, see our
|
|
149
|
-
[setup guide for
|
|
130
|
+
This is an example of how to use the Apitally middleware with a NestJS
|
|
131
|
+
application. For further instructions, see our
|
|
132
|
+
[setup guide for NestJS](https://docs.apitally.io/frameworks/nestjs).
|
|
150
133
|
|
|
151
134
|
```javascript
|
|
152
|
-
const
|
|
153
|
-
const { useApitally } = require("apitally/
|
|
135
|
+
const { NestFactory } = require("@nestjs/core");
|
|
136
|
+
const { useApitally } = require("apitally/nestjs");
|
|
137
|
+
const { AppModule } = require("./app.module");
|
|
154
138
|
|
|
155
|
-
|
|
139
|
+
async function bootstrap() {
|
|
140
|
+
const app = await NestFactory.create(AppModule);
|
|
156
141
|
|
|
157
|
-
useApitally(app, {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
142
|
+
await useApitally(app, {
|
|
143
|
+
clientId: "your-client-id",
|
|
144
|
+
env: "dev", // or "prod" etc.
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ...
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
bootstrap();
|
|
161
151
|
```
|
|
162
152
|
|
|
153
|
+
### AdonisJS
|
|
154
|
+
|
|
155
|
+
You can use the following Ace command to configure Apitally in your AdonisJS application:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
node ace configure apitally/adonisjs
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This command will automatically:
|
|
162
|
+
|
|
163
|
+
- Create a config file at `config/apitally.ts`
|
|
164
|
+
- Register the Apitally provider in `adonisrc.ts`
|
|
165
|
+
- Add the Apitally middleware to `start/kernel.ts`
|
|
166
|
+
- Add required environment variables to `.env` and `start/env.ts`
|
|
167
|
+
|
|
168
|
+
For further instructions, see our
|
|
169
|
+
[setup guide for AdonisJS](https://docs.apitally.io/frameworks/adonisjs).
|
|
170
|
+
|
|
163
171
|
### Hono
|
|
164
172
|
|
|
165
173
|
This is an example of how to use the Apitally middleware with a Hono application.
|
|
@@ -200,23 +208,70 @@ const app = new H3({
|
|
|
200
208
|
|
|
201
209
|
_Note:_ Apitally only works with H3 v2 and currently doesn't support nested apps.
|
|
202
210
|
|
|
203
|
-
###
|
|
211
|
+
### Elysia
|
|
204
212
|
|
|
205
|
-
|
|
213
|
+
This is an example of how to use the Apitally plugin with an Elysia application.
|
|
214
|
+
For further instructions, see our
|
|
215
|
+
[setup guide for Elysia](https://docs.apitally.io/frameworks/elysia).
|
|
206
216
|
|
|
207
|
-
```
|
|
208
|
-
|
|
217
|
+
```javascript
|
|
218
|
+
import { Elysia } from "elysia";
|
|
219
|
+
import { apitallyPlugin } from "apitally/elysia";
|
|
220
|
+
|
|
221
|
+
const app = new Elysia()
|
|
222
|
+
.use(
|
|
223
|
+
apitallyPlugin({
|
|
224
|
+
clientId: "your-client-id",
|
|
225
|
+
env: "dev", // or "prod" etc.
|
|
226
|
+
}),
|
|
227
|
+
)
|
|
228
|
+
.get("/", () => "hello");
|
|
209
229
|
```
|
|
210
230
|
|
|
211
|
-
|
|
231
|
+
### Koa
|
|
212
232
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
This is an example of how to use the Apitally middleware with a Koa application.
|
|
234
|
+
For further instructions, see our
|
|
235
|
+
[setup guide for Koa](https://docs.apitally.io/frameworks/koa).
|
|
236
|
+
|
|
237
|
+
```javascript
|
|
238
|
+
const Koa = require("koa");
|
|
239
|
+
const { useApitally } = require("apitally/koa");
|
|
240
|
+
|
|
241
|
+
const app = new Koa();
|
|
242
|
+
|
|
243
|
+
useApitally(app, {
|
|
244
|
+
clientId: "your-client-id",
|
|
245
|
+
env: "dev", // or "prod" etc.
|
|
246
|
+
});
|
|
247
|
+
```
|
|
217
248
|
|
|
249
|
+
### Hapi
|
|
250
|
+
|
|
251
|
+
This is an example of how to use the Apitally plugin with a Hapi application.
|
|
218
252
|
For further instructions, see our
|
|
219
|
-
[setup guide for
|
|
253
|
+
[setup guide for Hapi](https://docs.apitally.io/frameworks/hapi).
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
const Hapi = require("@hapi/hapi");
|
|
257
|
+
const { apitallyPlugin } = require("apitally/hapi");
|
|
258
|
+
|
|
259
|
+
const init = async () => {
|
|
260
|
+
const server = Hapi.server({
|
|
261
|
+
port: 3000,
|
|
262
|
+
host: "localhost",
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await server.register({
|
|
266
|
+
plugin: apitallyPlugin({
|
|
267
|
+
clientId: "your-client-id",
|
|
268
|
+
env: "dev", // or "prod" etc.
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
init();
|
|
274
|
+
```
|
|
220
275
|
|
|
221
276
|
## Getting help
|
|
222
277
|
|
|
@@ -104,6 +104,9 @@ var import_node_zlib = require("zlib");
|
|
|
104
104
|
var BODY_TOO_LARGE = import_node_buffer2.Buffer.from("<body too large>");
|
|
105
105
|
var BODY_MASKED = import_node_buffer2.Buffer.from("<masked>");
|
|
106
106
|
function convertHeaders(headers) {
|
|
107
|
+
if (!headers) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
107
110
|
if (headers instanceof Headers) {
|
|
108
111
|
return Array.from(headers.entries());
|
|
109
112
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/headers.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts","../../src/common/serverErrorCounter.ts","../../src/common/tempGzipFile.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { OutgoingHttpHeaders } from \"node:http\";\nimport { performance } from \"node:perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.js\";\nimport type { LogRecord } from \"../common/requestLogger.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\n\ndeclare module \"@adonisjs/core/http\" {\n interface HttpContext {\n apitallyConsumer?: ApitallyConsumer | string;\n apitallyError?: Error;\n }\n}\n\nexport default class ApitallyMiddleware {\n async handle(ctx: HttpContext, next: NextFn) {\n const client: ApitallyClient =\n await ctx.containerResolver.make(\"apitallyClient\");\n const logsContext: AsyncLocalStorage<LogRecord[]> =\n await ctx.containerResolver.make(\"apitallyLogsContext\");\n\n if (\n !client.isEnabled() ||\n ctx.request.method().toUpperCase() === \"OPTIONS\"\n ) {\n await next();\n return;\n }\n\n return logsContext.run([], async () => {\n const path = ctx.route?.pattern;\n const timestamp = Date.now() / 1000;\n const startTime = performance.now();\n\n await next();\n\n const responseTime = performance.now() - startTime;\n const requestSize = parseContentLength(\n ctx.request.header(\"content-length\"),\n );\n const requestContentType = ctx.request.header(\"content-type\")?.toString();\n let responseStatus = ctx.response.getStatus();\n let responseHeaders = ctx.response.getHeaders();\n let responseSize: number | undefined;\n let responseContentType: string | undefined;\n\n const consumer = ctx.apitallyConsumer\n ? consumerFromStringOrObject(ctx.apitallyConsumer)\n : null;\n client.consumerRegistry.addOrUpdateConsumer(consumer);\n\n const onWriteHead = (\n statusCode: number,\n headers: OutgoingHttpHeaders,\n ) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\n\n if (path) {\n client.requestCounter.addRequest({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n statusCode: responseStatus,\n responseTime,\n requestSize,\n responseSize,\n });\n\n if (\n responseStatus === 422 &&\n ctx.apitallyError &&\n \"code\" in ctx.apitallyError &&\n \"messages\" in ctx.apitallyError &&\n ctx.apitallyError.code === \"E_VALIDATION_ERROR\" &&\n Array.isArray(ctx.apitallyError.messages)\n ) {\n ctx.apitallyError.messages.forEach((message) => {\n client.validationErrorCounter.addValidationError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n loc: message.field,\n msg: message.message,\n type: message.rule,\n });\n });\n }\n\n if (responseStatus === 500 && ctx.apitallyError) {\n client.serverErrorCounter.addServerError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n type: ctx.apitallyError.name,\n msg: ctx.apitallyError.message,\n traceback: ctx.apitallyError.stack || \"\",\n });\n }\n }\n };\n\n // Capture the final status code and response headers just before they are sent\n const originalWriteHead = ctx.response.response.writeHead;\n ctx.response.response.writeHead = (...args: any) => {\n originalWriteHead.apply(ctx.response.response, args);\n onWriteHead(args[0], typeof args[1] === \"string\" ? args[2] : args[1]);\n return ctx.response.response;\n };\n\n if (client.requestLogger.enabled) {\n const logs = logsContext.getStore();\n\n const onEnd = (chunk: any) => {\n const requestBody =\n client.requestLogger.config.logRequestBody &&\n client.requestLogger.isSupportedContentType(requestContentType)\n ? ctx.request.raw()\n : undefined;\n const responseBody =\n client.requestLogger.config.logResponseBody &&\n client.requestLogger.isSupportedContentType(responseContentType)\n ? chunk\n : undefined;\n client.requestLogger.logRequest(\n {\n timestamp,\n method: ctx.request.method(),\n path,\n url: ctx.request.completeUrl(true),\n headers: convertHeaders(ctx.request.headers()),\n size: requestSize,\n consumer: consumer?.identifier,\n body: requestBody ? Buffer.from(requestBody) : undefined,\n },\n {\n statusCode: responseStatus,\n responseTime: responseTime / 1000,\n headers: convertHeaders(responseHeaders),\n size: responseSize,\n body: responseBody ? Buffer.from(responseBody) : undefined,\n },\n ctx.apitallyError,\n logs,\n );\n };\n\n // Capture the final response body just before it is sent\n const originalEnd = ctx.response.response.end;\n ctx.response.response.end = (...args: any) => {\n originalEnd.apply(ctx.response.response, args);\n onEnd(typeof args[0] !== \"function\" ? args[0] : undefined);\n return ctx.response.response;\n };\n }\n });\n }\n}\n","import { ApitallyConsumer } from \"./types.js\";\n\nexport const consumerFromStringOrObject = (\n consumer: ApitallyConsumer | string,\n) => {\n if (typeof consumer === \"string\") {\n consumer = String(consumer).trim().substring(0, 128);\n return consumer ? { identifier: consumer } : null;\n } else {\n consumer.identifier = String(consumer.identifier).trim().substring(0, 128);\n consumer.name = consumer.name?.trim().substring(0, 64);\n consumer.group = consumer.group?.trim().substring(0, 64);\n return consumer.identifier ? consumer : null;\n }\n};\n\nexport default class ConsumerRegistry {\n private consumers: Map<string, ApitallyConsumer>;\n private updated: Set<string>;\n\n constructor() {\n this.consumers = new Map();\n this.updated = new Set();\n }\n\n public addOrUpdateConsumer(consumer?: ApitallyConsumer | null) {\n if (!consumer || (!consumer.name && !consumer.group)) {\n return;\n }\n const existing = this.consumers.get(consumer.identifier);\n if (!existing) {\n this.consumers.set(consumer.identifier, consumer);\n this.updated.add(consumer.identifier);\n } else {\n if (consumer.name && consumer.name !== existing.name) {\n existing.name = consumer.name;\n this.updated.add(consumer.identifier);\n }\n if (consumer.group && consumer.group !== existing.group) {\n existing.group = consumer.group;\n this.updated.add(consumer.identifier);\n }\n }\n }\n\n public getAndResetUpdatedConsumers() {\n const data: Array<ApitallyConsumer> = [];\n this.updated.forEach((identifier) => {\n const consumer = this.consumers.get(identifier);\n if (consumer) {\n data.push(consumer);\n }\n });\n this.updated.clear();\n return data;\n }\n}\n","import { OutgoingHttpHeader } from \"node:http\";\n\nexport function parseContentLength(\n contentLength: OutgoingHttpHeader | undefined | null,\n): number | undefined {\n if (contentLength === undefined || contentLength === null) {\n return undefined;\n }\n if (typeof contentLength === \"number\") {\n return contentLength;\n }\n if (typeof contentLength === \"string\") {\n const parsed = parseInt(contentLength);\n return isNaN(parsed) ? undefined : parsed;\n }\n if (Array.isArray(contentLength)) {\n return parseContentLength(contentLength[0]);\n }\n return undefined;\n}\n\nexport function mergeHeaders(base: Headers, merge: Headers) {\n const mergedHeaders = new Headers(base);\n for (const [name, value] of merge)\n if (name === \"set-cookie\") mergedHeaders.append(name, value);\n else mergedHeaders.set(name, value);\n return mergedHeaders;\n}\n","import AsyncLock from \"async-lock\";\nimport { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { unlinkSync, writeFileSync } from \"node:fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"node:http\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst MAX_LOG_MSG_LENGTH = 2048;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\nconst MASK_BODY_FIELD_PATTERNS = [\n /password/i,\n /pwd/i,\n /token/i,\n /secret/i,\n /auth/i,\n /card[-_ ]?number/i,\n /ccv/i,\n /ssn/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type LogRecord = {\n timestamp: number;\n logger?: string;\n level: string;\n message: string;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n captureLogs: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskBodyFields: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n captureLogs: false,\n maskQueryParams: [],\n maskHeaders: [],\n maskBodyFields: [],\n excludePaths: [],\n};\n\ntype RequestLogItem = {\n uuid: string;\n request: Request;\n response: Response;\n exception?: {\n type: string;\n message: string;\n stacktrace: string;\n sentryEventId?: string;\n };\n logs?: LogRecord[];\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: RequestLogItem[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskBodyField(name: string) {\n const patterns = [\n ...this.config.maskBodyFields,\n ...MASK_BODY_FIELD_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n private hasJsonContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return contentType ? /\\bjson\\b/i.test(contentType) : null;\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n private maskBody(data: any): any {\n if (typeof data === \"object\" && data !== null && !Array.isArray(data)) {\n const result: any = {};\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === \"string\" && this.shouldMaskBodyField(key)) {\n result[key] = MASKED;\n } else {\n result[key] = this.maskBody(value);\n }\n }\n return result;\n }\n if (Array.isArray(data)) {\n return data.map((item) => this.maskBody(item));\n }\n return data;\n }\n\n private applyMasking(item: RequestLogItem) {\n // Apply user-provided maskRequestBodyCallback function\n if (\n this.config.maskRequestBodyCallback &&\n item.request.body &&\n item.request.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskRequestBodyCallback(item.request);\n item.request.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.request.body = undefined;\n }\n }\n\n // Apply user-provided maskResponseBodyCallback function\n if (\n this.config.maskResponseBodyCallback &&\n item.response.body &&\n item.response.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskResponseBodyCallback(\n item.request,\n item.response,\n );\n item.response.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.response.body = undefined;\n }\n }\n\n // Check request and response body sizes\n if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {\n item.request.body = BODY_TOO_LARGE;\n }\n if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {\n item.response.body = BODY_TOO_LARGE;\n }\n\n // Mask request and response body fields\n for (const key of [\"request\", \"response\"] as const) {\n const bodyData = item[key].body;\n if (\n !bodyData ||\n bodyData === BODY_TOO_LARGE ||\n bodyData === BODY_MASKED\n ) {\n continue;\n }\n\n const headers = item[key].headers;\n const hasJsonContent = this.hasJsonContentType(headers);\n if (hasJsonContent === null || hasJsonContent) {\n try {\n const parsedBody = JSON.parse(bodyData.toString());\n const maskedBody = this.maskBody(parsedBody);\n item[key].body = Buffer.from(JSON.stringify(maskedBody));\n } catch {\n // If parsing fails, leave body as is\n }\n }\n }\n\n // Mask request and response headers\n item.request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(item.request.headers)\n : [];\n item.response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(item.response.headers)\n : [];\n\n // Mask query params\n const url = new URL(item.request.url);\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n item.request.url = url.toString();\n\n return item;\n }\n\n logRequest(\n request: Request,\n response: Response,\n error?: Error,\n logs?: LogRecord[],\n ) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n }\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n }\n\n if (request.size !== undefined && request.size < 0) {\n request.size = undefined;\n }\n if (response.size !== undefined && response.size < 0) {\n response.size = undefined;\n }\n\n const item: RequestLogItem = {\n uuid: randomUUID(),\n request: request,\n response: response,\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : undefined,\n };\n\n if (logs && logs.length > 0) {\n item.logs = logs.map((log) => ({\n timestamp: log.timestamp,\n logger: log.logger,\n level: log.level,\n message: truncateLogMessage(log.message),\n }));\n }\n this.pendingWrites.push(item);\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n let item = this.pendingWrites.shift();\n if (item) {\n item = this.applyMasking(item);\n\n const finalItem = {\n uuid: item.uuid,\n request: skipEmptyValues(item.request),\n response: skipEmptyValues(item.response),\n exception: item.exception,\n logs: item.logs,\n };\n\n // Set up body serialization for JSON\n [finalItem.request.body, finalItem.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Override Buffer's default JSON serialization\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n\n await this.currentFile.writeLine(\n Buffer.from(JSON.stringify(finalItem)),\n );\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>,\n) {\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction truncateLogMessage(msg: string) {\n if (msg.length > MAX_LOG_MSG_LENGTH) {\n const suffix = \"... (truncated)\";\n return msg.slice(0, MAX_LOG_MSG_LENGTH - suffix.length) + suffix;\n }\n return msg;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n","import { createHash } from \"node:crypto\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport { ConsumerMethodPath, ServerError, ServerErrorsItem } from \"./types.js\";\n\nconst MAX_MSG_LENGTH = 2048;\nconst MAX_STACKTRACE_LENGTH = 65536;\n\nexport default class ServerErrorCounter {\n private errorCounts: Map<string, number>;\n private errorDetails: Map<string, ConsumerMethodPath & ServerError>;\n private sentryEventIds: Map<string, string>;\n\n constructor() {\n this.errorCounts = new Map();\n this.errorDetails = new Map();\n this.sentryEventIds = new Map();\n }\n\n public addServerError(serverError: ConsumerMethodPath & ServerError) {\n const key = this.getKey(serverError);\n if (!this.errorDetails.has(key)) {\n this.errorDetails.set(key, serverError);\n }\n this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);\n\n const sentryEventId = getSentryEventId();\n if (sentryEventId) {\n this.sentryEventIds.set(key, sentryEventId);\n }\n }\n\n public getAndResetServerErrors() {\n const data: Array<ServerErrorsItem> = [];\n this.errorCounts.forEach((count, key) => {\n const serverError = this.errorDetails.get(key);\n if (serverError) {\n data.push({\n consumer: serverError.consumer || null,\n method: serverError.method,\n path: serverError.path,\n type: serverError.type,\n msg: truncateExceptionMessage(serverError.msg),\n traceback: truncateExceptionStackTrace(serverError.traceback),\n sentry_event_id: this.sentryEventIds.get(key) || null,\n error_count: count,\n });\n }\n });\n this.errorCounts.clear();\n this.errorDetails.clear();\n this.sentryEventIds.clear();\n return data;\n }\n\n private getKey(serverError: ConsumerMethodPath & ServerError) {\n const hashInput = [\n serverError.consumer || \"\",\n serverError.method.toUpperCase(),\n serverError.path,\n serverError.type,\n serverError.msg.trim(),\n serverError.traceback.trim(),\n ].join(\"|\");\n return createHash(\"md5\").update(hashInput).digest(\"hex\");\n }\n}\n\nexport function truncateExceptionMessage(msg: string) {\n if (msg.length <= MAX_MSG_LENGTH) {\n return msg;\n }\n const suffix = \"... (truncated)\";\n const cutoff = MAX_MSG_LENGTH - suffix.length;\n return msg.substring(0, cutoff) + suffix;\n}\n\nexport function truncateExceptionStackTrace(stack: string) {\n const suffix = \"... (truncated) ...\";\n const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;\n const lines = stack.trim().split(\"\\n\");\n const truncatedLines: string[] = [];\n let length = 0;\n for (const line of lines) {\n if (length + line.length + 1 > cutoff) {\n truncatedLines.push(suffix);\n break;\n }\n truncatedLines.push(line);\n length += line.length + 1;\n }\n return truncatedLines.join(\"\\n\");\n}\n","import { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { createWriteStream, readFile, WriteStream } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, Gzip } from \"node:zlib\";\n\nexport default class TempGzipFile {\n public uuid: string;\n private filePath: string;\n private gzip: Gzip;\n private writeStream: WriteStream;\n private readyPromise: Promise<void>;\n private closedPromise: Promise<void>;\n\n constructor() {\n this.uuid = randomUUID();\n this.filePath = join(tmpdir(), `apitally-${this.uuid}.gz`);\n this.writeStream = createWriteStream(this.filePath);\n this.readyPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"ready\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.closedPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"close\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.gzip = createGzip();\n this.gzip.pipe(this.writeStream);\n }\n\n get size() {\n return this.writeStream.bytesWritten;\n }\n\n async writeLine(data: Buffer) {\n await this.readyPromise;\n return new Promise<void>((resolve, reject) => {\n this.gzip.write(Buffer.concat([data, Buffer.from(\"\\n\")]), (error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n async getContent() {\n return new Promise<Buffer>((resolve, reject) => {\n readFile(this.filePath, (error, data) => {\n if (error) {\n reject(error);\n } else {\n resolve(data);\n }\n });\n });\n }\n\n async close() {\n await new Promise<void>((resolve) => {\n this.gzip.end(() => {\n resolve();\n });\n });\n await this.closedPromise;\n }\n\n async delete() {\n await this.close();\n await unlink(this.filePath);\n }\n}\n"],"mappings":"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","names":["consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","import_node_buffer","import_node_crypto","import_node_fs","import_node_os","import_node_path","sentry","e","import_node_crypto","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","logsContext","isEnabled","request","method","toUpperCase","run","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","apitallyError","code","Array","isArray","messages","forEach","message","validationErrorCounter","addValidationError","loc","field","msg","type","rule","serverErrorCounter","addServerError","name","traceback","stack","originalWriteHead","writeHead","args","apply","requestLogger","enabled","logs","getStore","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
|
|
1
|
+
{"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/headers.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts","../../src/common/serverErrorCounter.ts","../../src/common/tempGzipFile.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { OutgoingHttpHeaders } from \"node:http\";\nimport { performance } from \"node:perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.js\";\nimport type { LogRecord } from \"../common/requestLogger.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\n\ndeclare module \"@adonisjs/core/http\" {\n interface HttpContext {\n apitallyConsumer?: ApitallyConsumer | string;\n apitallyError?: Error;\n }\n}\n\nexport default class ApitallyMiddleware {\n async handle(ctx: HttpContext, next: NextFn) {\n const client: ApitallyClient =\n await ctx.containerResolver.make(\"apitallyClient\");\n const logsContext: AsyncLocalStorage<LogRecord[]> =\n await ctx.containerResolver.make(\"apitallyLogsContext\");\n\n if (\n !client.isEnabled() ||\n ctx.request.method().toUpperCase() === \"OPTIONS\"\n ) {\n await next();\n return;\n }\n\n return logsContext.run([], async () => {\n const path = ctx.route?.pattern;\n const timestamp = Date.now() / 1000;\n const startTime = performance.now();\n\n await next();\n\n const responseTime = performance.now() - startTime;\n const requestSize = parseContentLength(\n ctx.request.header(\"content-length\"),\n );\n const requestContentType = ctx.request.header(\"content-type\")?.toString();\n let responseStatus = ctx.response.getStatus();\n let responseHeaders = ctx.response.getHeaders();\n let responseSize: number | undefined;\n let responseContentType: string | undefined;\n\n const consumer = ctx.apitallyConsumer\n ? consumerFromStringOrObject(ctx.apitallyConsumer)\n : null;\n client.consumerRegistry.addOrUpdateConsumer(consumer);\n\n const onWriteHead = (\n statusCode: number,\n headers: OutgoingHttpHeaders,\n ) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\n\n if (path) {\n client.requestCounter.addRequest({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n statusCode: responseStatus,\n responseTime,\n requestSize,\n responseSize,\n });\n\n if (\n responseStatus === 422 &&\n ctx.apitallyError &&\n \"code\" in ctx.apitallyError &&\n \"messages\" in ctx.apitallyError &&\n ctx.apitallyError.code === \"E_VALIDATION_ERROR\" &&\n Array.isArray(ctx.apitallyError.messages)\n ) {\n ctx.apitallyError.messages.forEach((message) => {\n client.validationErrorCounter.addValidationError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n loc: message.field,\n msg: message.message,\n type: message.rule,\n });\n });\n }\n\n if (responseStatus === 500 && ctx.apitallyError) {\n client.serverErrorCounter.addServerError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n type: ctx.apitallyError.name,\n msg: ctx.apitallyError.message,\n traceback: ctx.apitallyError.stack || \"\",\n });\n }\n }\n };\n\n // Capture the final status code and response headers just before they are sent\n const originalWriteHead = ctx.response.response.writeHead;\n ctx.response.response.writeHead = (...args: any) => {\n originalWriteHead.apply(ctx.response.response, args);\n onWriteHead(args[0], typeof args[1] === \"string\" ? args[2] : args[1]);\n return ctx.response.response;\n };\n\n if (client.requestLogger.enabled) {\n const logs = logsContext.getStore();\n\n const onEnd = (chunk: any) => {\n const requestBody =\n client.requestLogger.config.logRequestBody &&\n client.requestLogger.isSupportedContentType(requestContentType)\n ? ctx.request.raw()\n : undefined;\n const responseBody =\n client.requestLogger.config.logResponseBody &&\n client.requestLogger.isSupportedContentType(responseContentType)\n ? chunk\n : undefined;\n client.requestLogger.logRequest(\n {\n timestamp,\n method: ctx.request.method(),\n path,\n url: ctx.request.completeUrl(true),\n headers: convertHeaders(ctx.request.headers()),\n size: requestSize,\n consumer: consumer?.identifier,\n body: requestBody ? Buffer.from(requestBody) : undefined,\n },\n {\n statusCode: responseStatus,\n responseTime: responseTime / 1000,\n headers: convertHeaders(responseHeaders),\n size: responseSize,\n body: responseBody ? Buffer.from(responseBody) : undefined,\n },\n ctx.apitallyError,\n logs,\n );\n };\n\n // Capture the final response body just before it is sent\n const originalEnd = ctx.response.response.end;\n ctx.response.response.end = (...args: any) => {\n originalEnd.apply(ctx.response.response, args);\n onEnd(typeof args[0] !== \"function\" ? args[0] : undefined);\n return ctx.response.response;\n };\n }\n });\n }\n}\n","import { ApitallyConsumer } from \"./types.js\";\n\nexport const consumerFromStringOrObject = (\n consumer: ApitallyConsumer | string,\n) => {\n if (typeof consumer === \"string\") {\n consumer = String(consumer).trim().substring(0, 128);\n return consumer ? { identifier: consumer } : null;\n } else {\n consumer.identifier = String(consumer.identifier).trim().substring(0, 128);\n consumer.name = consumer.name?.trim().substring(0, 64);\n consumer.group = consumer.group?.trim().substring(0, 64);\n return consumer.identifier ? consumer : null;\n }\n};\n\nexport default class ConsumerRegistry {\n private consumers: Map<string, ApitallyConsumer>;\n private updated: Set<string>;\n\n constructor() {\n this.consumers = new Map();\n this.updated = new Set();\n }\n\n public addOrUpdateConsumer(consumer?: ApitallyConsumer | null) {\n if (!consumer || (!consumer.name && !consumer.group)) {\n return;\n }\n const existing = this.consumers.get(consumer.identifier);\n if (!existing) {\n this.consumers.set(consumer.identifier, consumer);\n this.updated.add(consumer.identifier);\n } else {\n if (consumer.name && consumer.name !== existing.name) {\n existing.name = consumer.name;\n this.updated.add(consumer.identifier);\n }\n if (consumer.group && consumer.group !== existing.group) {\n existing.group = consumer.group;\n this.updated.add(consumer.identifier);\n }\n }\n }\n\n public getAndResetUpdatedConsumers() {\n const data: Array<ApitallyConsumer> = [];\n this.updated.forEach((identifier) => {\n const consumer = this.consumers.get(identifier);\n if (consumer) {\n data.push(consumer);\n }\n });\n this.updated.clear();\n return data;\n }\n}\n","import { OutgoingHttpHeader } from \"node:http\";\n\nexport function parseContentLength(\n contentLength: OutgoingHttpHeader | undefined | null,\n): number | undefined {\n if (contentLength === undefined || contentLength === null) {\n return undefined;\n }\n if (typeof contentLength === \"number\") {\n return contentLength;\n }\n if (typeof contentLength === \"string\") {\n const parsed = parseInt(contentLength);\n return isNaN(parsed) ? undefined : parsed;\n }\n if (Array.isArray(contentLength)) {\n return parseContentLength(contentLength[0]);\n }\n return undefined;\n}\n\nexport function mergeHeaders(base: Headers, merge: Headers) {\n const mergedHeaders = new Headers(base);\n for (const [name, value] of merge)\n if (name === \"set-cookie\") mergedHeaders.append(name, value);\n else mergedHeaders.set(name, value);\n return mergedHeaders;\n}\n","import AsyncLock from \"async-lock\";\nimport { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { unlinkSync, writeFileSync } from \"node:fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"node:http\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst MAX_LOG_MSG_LENGTH = 2048;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n \"text/html\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\nconst MASK_BODY_FIELD_PATTERNS = [\n /password/i,\n /pwd/i,\n /token/i,\n /secret/i,\n /auth/i,\n /card[-_ ]?number/i,\n /ccv/i,\n /ssn/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type LogRecord = {\n timestamp: number;\n logger?: string;\n level: string;\n message: string;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n captureLogs: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskBodyFields: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n captureLogs: false,\n maskQueryParams: [],\n maskHeaders: [],\n maskBodyFields: [],\n excludePaths: [],\n};\n\ntype RequestLogItem = {\n uuid: string;\n request: Request;\n response: Response;\n exception?: {\n type: string;\n message: string;\n stacktrace: string;\n sentryEventId?: string;\n };\n logs?: LogRecord[];\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: RequestLogItem[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskBodyField(name: string) {\n const patterns = [\n ...this.config.maskBodyFields,\n ...MASK_BODY_FIELD_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n private hasJsonContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return contentType ? /\\bjson\\b/i.test(contentType) : null;\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n private maskBody(data: any): any {\n if (typeof data === \"object\" && data !== null && !Array.isArray(data)) {\n const result: any = {};\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === \"string\" && this.shouldMaskBodyField(key)) {\n result[key] = MASKED;\n } else {\n result[key] = this.maskBody(value);\n }\n }\n return result;\n }\n if (Array.isArray(data)) {\n return data.map((item) => this.maskBody(item));\n }\n return data;\n }\n\n private applyMasking(item: RequestLogItem) {\n // Apply user-provided maskRequestBodyCallback function\n if (\n this.config.maskRequestBodyCallback &&\n item.request.body &&\n item.request.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskRequestBodyCallback(item.request);\n item.request.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.request.body = undefined;\n }\n }\n\n // Apply user-provided maskResponseBodyCallback function\n if (\n this.config.maskResponseBodyCallback &&\n item.response.body &&\n item.response.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskResponseBodyCallback(\n item.request,\n item.response,\n );\n item.response.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.response.body = undefined;\n }\n }\n\n // Check request and response body sizes\n if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {\n item.request.body = BODY_TOO_LARGE;\n }\n if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {\n item.response.body = BODY_TOO_LARGE;\n }\n\n // Mask request and response body fields\n for (const key of [\"request\", \"response\"] as const) {\n const bodyData = item[key].body;\n if (\n !bodyData ||\n bodyData === BODY_TOO_LARGE ||\n bodyData === BODY_MASKED\n ) {\n continue;\n }\n\n const headers = item[key].headers;\n const hasJsonContent = this.hasJsonContentType(headers);\n if (hasJsonContent === null || hasJsonContent) {\n try {\n const parsedBody = JSON.parse(bodyData.toString());\n const maskedBody = this.maskBody(parsedBody);\n item[key].body = Buffer.from(JSON.stringify(maskedBody));\n } catch {\n // If parsing fails, leave body as is\n }\n }\n }\n\n // Mask request and response headers\n item.request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(item.request.headers)\n : [];\n item.response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(item.response.headers)\n : [];\n\n // Mask query params\n const url = new URL(item.request.url);\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n item.request.url = url.toString();\n\n return item;\n }\n\n logRequest(\n request: Request,\n response: Response,\n error?: Error,\n logs?: LogRecord[],\n ) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n }\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n }\n\n if (request.size !== undefined && request.size < 0) {\n request.size = undefined;\n }\n if (response.size !== undefined && response.size < 0) {\n response.size = undefined;\n }\n\n const item: RequestLogItem = {\n uuid: randomUUID(),\n request: request,\n response: response,\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : undefined,\n };\n\n if (logs && logs.length > 0) {\n item.logs = logs.map((log) => ({\n timestamp: log.timestamp,\n logger: log.logger,\n level: log.level,\n message: truncateLogMessage(log.message),\n }));\n }\n this.pendingWrites.push(item);\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n let item = this.pendingWrites.shift();\n if (item) {\n item = this.applyMasking(item);\n\n const finalItem = {\n uuid: item.uuid,\n request: skipEmptyValues(item.request),\n response: skipEmptyValues(item.response),\n exception: item.exception,\n logs: item.logs,\n };\n\n // Set up body serialization for JSON\n [finalItem.request.body, finalItem.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Override Buffer's default JSON serialization\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n\n await this.currentFile.writeLine(\n Buffer.from(JSON.stringify(finalItem)),\n );\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>\n | undefined,\n) {\n if (!headers) {\n return [];\n }\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction truncateLogMessage(msg: string) {\n if (msg.length > MAX_LOG_MSG_LENGTH) {\n const suffix = \"... (truncated)\";\n return msg.slice(0, MAX_LOG_MSG_LENGTH - suffix.length) + suffix;\n }\n return msg;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n","import { createHash } from \"node:crypto\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport { ConsumerMethodPath, ServerError, ServerErrorsItem } from \"./types.js\";\n\nconst MAX_MSG_LENGTH = 2048;\nconst MAX_STACKTRACE_LENGTH = 65536;\n\nexport default class ServerErrorCounter {\n private errorCounts: Map<string, number>;\n private errorDetails: Map<string, ConsumerMethodPath & ServerError>;\n private sentryEventIds: Map<string, string>;\n\n constructor() {\n this.errorCounts = new Map();\n this.errorDetails = new Map();\n this.sentryEventIds = new Map();\n }\n\n public addServerError(serverError: ConsumerMethodPath & ServerError) {\n const key = this.getKey(serverError);\n if (!this.errorDetails.has(key)) {\n this.errorDetails.set(key, serverError);\n }\n this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);\n\n const sentryEventId = getSentryEventId();\n if (sentryEventId) {\n this.sentryEventIds.set(key, sentryEventId);\n }\n }\n\n public getAndResetServerErrors() {\n const data: Array<ServerErrorsItem> = [];\n this.errorCounts.forEach((count, key) => {\n const serverError = this.errorDetails.get(key);\n if (serverError) {\n data.push({\n consumer: serverError.consumer || null,\n method: serverError.method,\n path: serverError.path,\n type: serverError.type,\n msg: truncateExceptionMessage(serverError.msg),\n traceback: truncateExceptionStackTrace(serverError.traceback),\n sentry_event_id: this.sentryEventIds.get(key) || null,\n error_count: count,\n });\n }\n });\n this.errorCounts.clear();\n this.errorDetails.clear();\n this.sentryEventIds.clear();\n return data;\n }\n\n private getKey(serverError: ConsumerMethodPath & ServerError) {\n const hashInput = [\n serverError.consumer || \"\",\n serverError.method.toUpperCase(),\n serverError.path,\n serverError.type,\n serverError.msg.trim(),\n serverError.traceback.trim(),\n ].join(\"|\");\n return createHash(\"md5\").update(hashInput).digest(\"hex\");\n }\n}\n\nexport function truncateExceptionMessage(msg: string) {\n if (msg.length <= MAX_MSG_LENGTH) {\n return msg;\n }\n const suffix = \"... (truncated)\";\n const cutoff = MAX_MSG_LENGTH - suffix.length;\n return msg.substring(0, cutoff) + suffix;\n}\n\nexport function truncateExceptionStackTrace(stack: string) {\n const suffix = \"... (truncated) ...\";\n const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;\n const lines = stack.trim().split(\"\\n\");\n const truncatedLines: string[] = [];\n let length = 0;\n for (const line of lines) {\n if (length + line.length + 1 > cutoff) {\n truncatedLines.push(suffix);\n break;\n }\n truncatedLines.push(line);\n length += line.length + 1;\n }\n return truncatedLines.join(\"\\n\");\n}\n","import { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { createWriteStream, readFile, WriteStream } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, Gzip } from \"node:zlib\";\n\nexport default class TempGzipFile {\n public uuid: string;\n private filePath: string;\n private gzip: Gzip;\n private writeStream: WriteStream;\n private readyPromise: Promise<void>;\n private closedPromise: Promise<void>;\n\n constructor() {\n this.uuid = randomUUID();\n this.filePath = join(tmpdir(), `apitally-${this.uuid}.gz`);\n this.writeStream = createWriteStream(this.filePath);\n this.readyPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"ready\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.closedPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"close\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.gzip = createGzip();\n this.gzip.pipe(this.writeStream);\n }\n\n get size() {\n return this.writeStream.bytesWritten;\n }\n\n async writeLine(data: Buffer) {\n await this.readyPromise;\n return new Promise<void>((resolve, reject) => {\n this.gzip.write(Buffer.concat([data, Buffer.from(\"\\n\")]), (error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n async getContent() {\n return new Promise<Buffer>((resolve, reject) => {\n readFile(this.filePath, (error, data) => {\n if (error) {\n reject(error);\n } else {\n resolve(data);\n }\n });\n });\n }\n\n async close() {\n await new Promise<void>((resolve) => {\n this.gzip.end(() => {\n resolve();\n });\n });\n await this.closedPromise;\n }\n\n async delete() {\n await this.close();\n await unlink(this.filePath);\n }\n}\n"],"mappings":"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","names":["consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","import_node_buffer","import_node_crypto","import_node_fs","import_node_os","import_node_path","sentry","e","import_node_crypto","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","logsContext","isEnabled","request","method","toUpperCase","run","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","apitallyError","code","Array","isArray","messages","forEach","message","validationErrorCounter","addValidationError","loc","field","msg","type","rule","serverErrorCounter","addServerError","name","traceback","stack","originalWriteHead","writeHead","args","apply","requestLogger","enabled","logs","getStore","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
|
|
@@ -72,6 +72,9 @@ import { createGzip } from "zlib";
|
|
|
72
72
|
var BODY_TOO_LARGE = Buffer3.from("<body too large>");
|
|
73
73
|
var BODY_MASKED = Buffer3.from("<masked>");
|
|
74
74
|
function convertHeaders(headers) {
|
|
75
|
+
if (!headers) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
75
78
|
if (headers instanceof Headers) {
|
|
76
79
|
return Array.from(headers.entries());
|
|
77
80
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/headers.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts","../../src/common/serverErrorCounter.ts","../../src/common/tempGzipFile.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { OutgoingHttpHeaders } from \"node:http\";\nimport { performance } from \"node:perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.js\";\nimport type { LogRecord } from \"../common/requestLogger.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\n\ndeclare module \"@adonisjs/core/http\" {\n interface HttpContext {\n apitallyConsumer?: ApitallyConsumer | string;\n apitallyError?: Error;\n }\n}\n\nexport default class ApitallyMiddleware {\n async handle(ctx: HttpContext, next: NextFn) {\n const client: ApitallyClient =\n await ctx.containerResolver.make(\"apitallyClient\");\n const logsContext: AsyncLocalStorage<LogRecord[]> =\n await ctx.containerResolver.make(\"apitallyLogsContext\");\n\n if (\n !client.isEnabled() ||\n ctx.request.method().toUpperCase() === \"OPTIONS\"\n ) {\n await next();\n return;\n }\n\n return logsContext.run([], async () => {\n const path = ctx.route?.pattern;\n const timestamp = Date.now() / 1000;\n const startTime = performance.now();\n\n await next();\n\n const responseTime = performance.now() - startTime;\n const requestSize = parseContentLength(\n ctx.request.header(\"content-length\"),\n );\n const requestContentType = ctx.request.header(\"content-type\")?.toString();\n let responseStatus = ctx.response.getStatus();\n let responseHeaders = ctx.response.getHeaders();\n let responseSize: number | undefined;\n let responseContentType: string | undefined;\n\n const consumer = ctx.apitallyConsumer\n ? consumerFromStringOrObject(ctx.apitallyConsumer)\n : null;\n client.consumerRegistry.addOrUpdateConsumer(consumer);\n\n const onWriteHead = (\n statusCode: number,\n headers: OutgoingHttpHeaders,\n ) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\n\n if (path) {\n client.requestCounter.addRequest({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n statusCode: responseStatus,\n responseTime,\n requestSize,\n responseSize,\n });\n\n if (\n responseStatus === 422 &&\n ctx.apitallyError &&\n \"code\" in ctx.apitallyError &&\n \"messages\" in ctx.apitallyError &&\n ctx.apitallyError.code === \"E_VALIDATION_ERROR\" &&\n Array.isArray(ctx.apitallyError.messages)\n ) {\n ctx.apitallyError.messages.forEach((message) => {\n client.validationErrorCounter.addValidationError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n loc: message.field,\n msg: message.message,\n type: message.rule,\n });\n });\n }\n\n if (responseStatus === 500 && ctx.apitallyError) {\n client.serverErrorCounter.addServerError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n type: ctx.apitallyError.name,\n msg: ctx.apitallyError.message,\n traceback: ctx.apitallyError.stack || \"\",\n });\n }\n }\n };\n\n // Capture the final status code and response headers just before they are sent\n const originalWriteHead = ctx.response.response.writeHead;\n ctx.response.response.writeHead = (...args: any) => {\n originalWriteHead.apply(ctx.response.response, args);\n onWriteHead(args[0], typeof args[1] === \"string\" ? args[2] : args[1]);\n return ctx.response.response;\n };\n\n if (client.requestLogger.enabled) {\n const logs = logsContext.getStore();\n\n const onEnd = (chunk: any) => {\n const requestBody =\n client.requestLogger.config.logRequestBody &&\n client.requestLogger.isSupportedContentType(requestContentType)\n ? ctx.request.raw()\n : undefined;\n const responseBody =\n client.requestLogger.config.logResponseBody &&\n client.requestLogger.isSupportedContentType(responseContentType)\n ? chunk\n : undefined;\n client.requestLogger.logRequest(\n {\n timestamp,\n method: ctx.request.method(),\n path,\n url: ctx.request.completeUrl(true),\n headers: convertHeaders(ctx.request.headers()),\n size: requestSize,\n consumer: consumer?.identifier,\n body: requestBody ? Buffer.from(requestBody) : undefined,\n },\n {\n statusCode: responseStatus,\n responseTime: responseTime / 1000,\n headers: convertHeaders(responseHeaders),\n size: responseSize,\n body: responseBody ? Buffer.from(responseBody) : undefined,\n },\n ctx.apitallyError,\n logs,\n );\n };\n\n // Capture the final response body just before it is sent\n const originalEnd = ctx.response.response.end;\n ctx.response.response.end = (...args: any) => {\n originalEnd.apply(ctx.response.response, args);\n onEnd(typeof args[0] !== \"function\" ? args[0] : undefined);\n return ctx.response.response;\n };\n }\n });\n }\n}\n","import { ApitallyConsumer } from \"./types.js\";\n\nexport const consumerFromStringOrObject = (\n consumer: ApitallyConsumer | string,\n) => {\n if (typeof consumer === \"string\") {\n consumer = String(consumer).trim().substring(0, 128);\n return consumer ? { identifier: consumer } : null;\n } else {\n consumer.identifier = String(consumer.identifier).trim().substring(0, 128);\n consumer.name = consumer.name?.trim().substring(0, 64);\n consumer.group = consumer.group?.trim().substring(0, 64);\n return consumer.identifier ? consumer : null;\n }\n};\n\nexport default class ConsumerRegistry {\n private consumers: Map<string, ApitallyConsumer>;\n private updated: Set<string>;\n\n constructor() {\n this.consumers = new Map();\n this.updated = new Set();\n }\n\n public addOrUpdateConsumer(consumer?: ApitallyConsumer | null) {\n if (!consumer || (!consumer.name && !consumer.group)) {\n return;\n }\n const existing = this.consumers.get(consumer.identifier);\n if (!existing) {\n this.consumers.set(consumer.identifier, consumer);\n this.updated.add(consumer.identifier);\n } else {\n if (consumer.name && consumer.name !== existing.name) {\n existing.name = consumer.name;\n this.updated.add(consumer.identifier);\n }\n if (consumer.group && consumer.group !== existing.group) {\n existing.group = consumer.group;\n this.updated.add(consumer.identifier);\n }\n }\n }\n\n public getAndResetUpdatedConsumers() {\n const data: Array<ApitallyConsumer> = [];\n this.updated.forEach((identifier) => {\n const consumer = this.consumers.get(identifier);\n if (consumer) {\n data.push(consumer);\n }\n });\n this.updated.clear();\n return data;\n }\n}\n","import { OutgoingHttpHeader } from \"node:http\";\n\nexport function parseContentLength(\n contentLength: OutgoingHttpHeader | undefined | null,\n): number | undefined {\n if (contentLength === undefined || contentLength === null) {\n return undefined;\n }\n if (typeof contentLength === \"number\") {\n return contentLength;\n }\n if (typeof contentLength === \"string\") {\n const parsed = parseInt(contentLength);\n return isNaN(parsed) ? undefined : parsed;\n }\n if (Array.isArray(contentLength)) {\n return parseContentLength(contentLength[0]);\n }\n return undefined;\n}\n\nexport function mergeHeaders(base: Headers, merge: Headers) {\n const mergedHeaders = new Headers(base);\n for (const [name, value] of merge)\n if (name === \"set-cookie\") mergedHeaders.append(name, value);\n else mergedHeaders.set(name, value);\n return mergedHeaders;\n}\n","import AsyncLock from \"async-lock\";\nimport { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { unlinkSync, writeFileSync } from \"node:fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"node:http\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst MAX_LOG_MSG_LENGTH = 2048;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\nconst MASK_BODY_FIELD_PATTERNS = [\n /password/i,\n /pwd/i,\n /token/i,\n /secret/i,\n /auth/i,\n /card[-_ ]?number/i,\n /ccv/i,\n /ssn/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type LogRecord = {\n timestamp: number;\n logger?: string;\n level: string;\n message: string;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n captureLogs: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskBodyFields: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n captureLogs: false,\n maskQueryParams: [],\n maskHeaders: [],\n maskBodyFields: [],\n excludePaths: [],\n};\n\ntype RequestLogItem = {\n uuid: string;\n request: Request;\n response: Response;\n exception?: {\n type: string;\n message: string;\n stacktrace: string;\n sentryEventId?: string;\n };\n logs?: LogRecord[];\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: RequestLogItem[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskBodyField(name: string) {\n const patterns = [\n ...this.config.maskBodyFields,\n ...MASK_BODY_FIELD_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n private hasJsonContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return contentType ? /\\bjson\\b/i.test(contentType) : null;\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n private maskBody(data: any): any {\n if (typeof data === \"object\" && data !== null && !Array.isArray(data)) {\n const result: any = {};\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === \"string\" && this.shouldMaskBodyField(key)) {\n result[key] = MASKED;\n } else {\n result[key] = this.maskBody(value);\n }\n }\n return result;\n }\n if (Array.isArray(data)) {\n return data.map((item) => this.maskBody(item));\n }\n return data;\n }\n\n private applyMasking(item: RequestLogItem) {\n // Apply user-provided maskRequestBodyCallback function\n if (\n this.config.maskRequestBodyCallback &&\n item.request.body &&\n item.request.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskRequestBodyCallback(item.request);\n item.request.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.request.body = undefined;\n }\n }\n\n // Apply user-provided maskResponseBodyCallback function\n if (\n this.config.maskResponseBodyCallback &&\n item.response.body &&\n item.response.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskResponseBodyCallback(\n item.request,\n item.response,\n );\n item.response.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.response.body = undefined;\n }\n }\n\n // Check request and response body sizes\n if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {\n item.request.body = BODY_TOO_LARGE;\n }\n if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {\n item.response.body = BODY_TOO_LARGE;\n }\n\n // Mask request and response body fields\n for (const key of [\"request\", \"response\"] as const) {\n const bodyData = item[key].body;\n if (\n !bodyData ||\n bodyData === BODY_TOO_LARGE ||\n bodyData === BODY_MASKED\n ) {\n continue;\n }\n\n const headers = item[key].headers;\n const hasJsonContent = this.hasJsonContentType(headers);\n if (hasJsonContent === null || hasJsonContent) {\n try {\n const parsedBody = JSON.parse(bodyData.toString());\n const maskedBody = this.maskBody(parsedBody);\n item[key].body = Buffer.from(JSON.stringify(maskedBody));\n } catch {\n // If parsing fails, leave body as is\n }\n }\n }\n\n // Mask request and response headers\n item.request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(item.request.headers)\n : [];\n item.response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(item.response.headers)\n : [];\n\n // Mask query params\n const url = new URL(item.request.url);\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n item.request.url = url.toString();\n\n return item;\n }\n\n logRequest(\n request: Request,\n response: Response,\n error?: Error,\n logs?: LogRecord[],\n ) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n }\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n }\n\n if (request.size !== undefined && request.size < 0) {\n request.size = undefined;\n }\n if (response.size !== undefined && response.size < 0) {\n response.size = undefined;\n }\n\n const item: RequestLogItem = {\n uuid: randomUUID(),\n request: request,\n response: response,\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : undefined,\n };\n\n if (logs && logs.length > 0) {\n item.logs = logs.map((log) => ({\n timestamp: log.timestamp,\n logger: log.logger,\n level: log.level,\n message: truncateLogMessage(log.message),\n }));\n }\n this.pendingWrites.push(item);\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n let item = this.pendingWrites.shift();\n if (item) {\n item = this.applyMasking(item);\n\n const finalItem = {\n uuid: item.uuid,\n request: skipEmptyValues(item.request),\n response: skipEmptyValues(item.response),\n exception: item.exception,\n logs: item.logs,\n };\n\n // Set up body serialization for JSON\n [finalItem.request.body, finalItem.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Override Buffer's default JSON serialization\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n\n await this.currentFile.writeLine(\n Buffer.from(JSON.stringify(finalItem)),\n );\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>,\n) {\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction truncateLogMessage(msg: string) {\n if (msg.length > MAX_LOG_MSG_LENGTH) {\n const suffix = \"... (truncated)\";\n return msg.slice(0, MAX_LOG_MSG_LENGTH - suffix.length) + suffix;\n }\n return msg;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n","import { createHash } from \"node:crypto\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport { ConsumerMethodPath, ServerError, ServerErrorsItem } from \"./types.js\";\n\nconst MAX_MSG_LENGTH = 2048;\nconst MAX_STACKTRACE_LENGTH = 65536;\n\nexport default class ServerErrorCounter {\n private errorCounts: Map<string, number>;\n private errorDetails: Map<string, ConsumerMethodPath & ServerError>;\n private sentryEventIds: Map<string, string>;\n\n constructor() {\n this.errorCounts = new Map();\n this.errorDetails = new Map();\n this.sentryEventIds = new Map();\n }\n\n public addServerError(serverError: ConsumerMethodPath & ServerError) {\n const key = this.getKey(serverError);\n if (!this.errorDetails.has(key)) {\n this.errorDetails.set(key, serverError);\n }\n this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);\n\n const sentryEventId = getSentryEventId();\n if (sentryEventId) {\n this.sentryEventIds.set(key, sentryEventId);\n }\n }\n\n public getAndResetServerErrors() {\n const data: Array<ServerErrorsItem> = [];\n this.errorCounts.forEach((count, key) => {\n const serverError = this.errorDetails.get(key);\n if (serverError) {\n data.push({\n consumer: serverError.consumer || null,\n method: serverError.method,\n path: serverError.path,\n type: serverError.type,\n msg: truncateExceptionMessage(serverError.msg),\n traceback: truncateExceptionStackTrace(serverError.traceback),\n sentry_event_id: this.sentryEventIds.get(key) || null,\n error_count: count,\n });\n }\n });\n this.errorCounts.clear();\n this.errorDetails.clear();\n this.sentryEventIds.clear();\n return data;\n }\n\n private getKey(serverError: ConsumerMethodPath & ServerError) {\n const hashInput = [\n serverError.consumer || \"\",\n serverError.method.toUpperCase(),\n serverError.path,\n serverError.type,\n serverError.msg.trim(),\n serverError.traceback.trim(),\n ].join(\"|\");\n return createHash(\"md5\").update(hashInput).digest(\"hex\");\n }\n}\n\nexport function truncateExceptionMessage(msg: string) {\n if (msg.length <= MAX_MSG_LENGTH) {\n return msg;\n }\n const suffix = \"... (truncated)\";\n const cutoff = MAX_MSG_LENGTH - suffix.length;\n return msg.substring(0, cutoff) + suffix;\n}\n\nexport function truncateExceptionStackTrace(stack: string) {\n const suffix = \"... (truncated) ...\";\n const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;\n const lines = stack.trim().split(\"\\n\");\n const truncatedLines: string[] = [];\n let length = 0;\n for (const line of lines) {\n if (length + line.length + 1 > cutoff) {\n truncatedLines.push(suffix);\n break;\n }\n truncatedLines.push(line);\n length += line.length + 1;\n }\n return truncatedLines.join(\"\\n\");\n}\n","import { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { createWriteStream, readFile, WriteStream } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, Gzip } from \"node:zlib\";\n\nexport default class TempGzipFile {\n public uuid: string;\n private filePath: string;\n private gzip: Gzip;\n private writeStream: WriteStream;\n private readyPromise: Promise<void>;\n private closedPromise: Promise<void>;\n\n constructor() {\n this.uuid = randomUUID();\n this.filePath = join(tmpdir(), `apitally-${this.uuid}.gz`);\n this.writeStream = createWriteStream(this.filePath);\n this.readyPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"ready\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.closedPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"close\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.gzip = createGzip();\n this.gzip.pipe(this.writeStream);\n }\n\n get size() {\n return this.writeStream.bytesWritten;\n }\n\n async writeLine(data: Buffer) {\n await this.readyPromise;\n return new Promise<void>((resolve, reject) => {\n this.gzip.write(Buffer.concat([data, Buffer.from(\"\\n\")]), (error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n async getContent() {\n return new Promise<Buffer>((resolve, reject) => {\n readFile(this.filePath, (error, data) => {\n if (error) {\n reject(error);\n } else {\n resolve(data);\n }\n });\n });\n }\n\n async close() {\n await new Promise<void>((resolve) => {\n this.gzip.end(() => {\n resolve();\n });\n });\n await this.closedPromise;\n }\n\n async delete() {\n await this.close();\n await unlink(this.filePath);\n }\n}\n"],"mappings":"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","names":["performance","consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","AsyncLock","Buffer","randomUUID","unlinkSync","writeFileSync","tmpdir","join","sentry","e","createHash","Buffer","randomUUID","createWriteStream","readFile","unlink","tmpdir","join","createGzip","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","logsContext","isEnabled","request","method","toUpperCase","run","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","apitallyError","code","Array","isArray","messages","forEach","message","validationErrorCounter","addValidationError","loc","field","msg","type","rule","serverErrorCounter","addServerError","name","traceback","stack","originalWriteHead","writeHead","args","apply","requestLogger","enabled","logs","getStore","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
|
|
1
|
+
{"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/headers.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts","../../src/common/serverErrorCounter.ts","../../src/common/tempGzipFile.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { OutgoingHttpHeaders } from \"node:http\";\nimport { performance } from \"node:perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.js\";\nimport type { LogRecord } from \"../common/requestLogger.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\n\ndeclare module \"@adonisjs/core/http\" {\n interface HttpContext {\n apitallyConsumer?: ApitallyConsumer | string;\n apitallyError?: Error;\n }\n}\n\nexport default class ApitallyMiddleware {\n async handle(ctx: HttpContext, next: NextFn) {\n const client: ApitallyClient =\n await ctx.containerResolver.make(\"apitallyClient\");\n const logsContext: AsyncLocalStorage<LogRecord[]> =\n await ctx.containerResolver.make(\"apitallyLogsContext\");\n\n if (\n !client.isEnabled() ||\n ctx.request.method().toUpperCase() === \"OPTIONS\"\n ) {\n await next();\n return;\n }\n\n return logsContext.run([], async () => {\n const path = ctx.route?.pattern;\n const timestamp = Date.now() / 1000;\n const startTime = performance.now();\n\n await next();\n\n const responseTime = performance.now() - startTime;\n const requestSize = parseContentLength(\n ctx.request.header(\"content-length\"),\n );\n const requestContentType = ctx.request.header(\"content-type\")?.toString();\n let responseStatus = ctx.response.getStatus();\n let responseHeaders = ctx.response.getHeaders();\n let responseSize: number | undefined;\n let responseContentType: string | undefined;\n\n const consumer = ctx.apitallyConsumer\n ? consumerFromStringOrObject(ctx.apitallyConsumer)\n : null;\n client.consumerRegistry.addOrUpdateConsumer(consumer);\n\n const onWriteHead = (\n statusCode: number,\n headers: OutgoingHttpHeaders,\n ) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\n\n if (path) {\n client.requestCounter.addRequest({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n statusCode: responseStatus,\n responseTime,\n requestSize,\n responseSize,\n });\n\n if (\n responseStatus === 422 &&\n ctx.apitallyError &&\n \"code\" in ctx.apitallyError &&\n \"messages\" in ctx.apitallyError &&\n ctx.apitallyError.code === \"E_VALIDATION_ERROR\" &&\n Array.isArray(ctx.apitallyError.messages)\n ) {\n ctx.apitallyError.messages.forEach((message) => {\n client.validationErrorCounter.addValidationError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n loc: message.field,\n msg: message.message,\n type: message.rule,\n });\n });\n }\n\n if (responseStatus === 500 && ctx.apitallyError) {\n client.serverErrorCounter.addServerError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n type: ctx.apitallyError.name,\n msg: ctx.apitallyError.message,\n traceback: ctx.apitallyError.stack || \"\",\n });\n }\n }\n };\n\n // Capture the final status code and response headers just before they are sent\n const originalWriteHead = ctx.response.response.writeHead;\n ctx.response.response.writeHead = (...args: any) => {\n originalWriteHead.apply(ctx.response.response, args);\n onWriteHead(args[0], typeof args[1] === \"string\" ? args[2] : args[1]);\n return ctx.response.response;\n };\n\n if (client.requestLogger.enabled) {\n const logs = logsContext.getStore();\n\n const onEnd = (chunk: any) => {\n const requestBody =\n client.requestLogger.config.logRequestBody &&\n client.requestLogger.isSupportedContentType(requestContentType)\n ? ctx.request.raw()\n : undefined;\n const responseBody =\n client.requestLogger.config.logResponseBody &&\n client.requestLogger.isSupportedContentType(responseContentType)\n ? chunk\n : undefined;\n client.requestLogger.logRequest(\n {\n timestamp,\n method: ctx.request.method(),\n path,\n url: ctx.request.completeUrl(true),\n headers: convertHeaders(ctx.request.headers()),\n size: requestSize,\n consumer: consumer?.identifier,\n body: requestBody ? Buffer.from(requestBody) : undefined,\n },\n {\n statusCode: responseStatus,\n responseTime: responseTime / 1000,\n headers: convertHeaders(responseHeaders),\n size: responseSize,\n body: responseBody ? Buffer.from(responseBody) : undefined,\n },\n ctx.apitallyError,\n logs,\n );\n };\n\n // Capture the final response body just before it is sent\n const originalEnd = ctx.response.response.end;\n ctx.response.response.end = (...args: any) => {\n originalEnd.apply(ctx.response.response, args);\n onEnd(typeof args[0] !== \"function\" ? args[0] : undefined);\n return ctx.response.response;\n };\n }\n });\n }\n}\n","import { ApitallyConsumer } from \"./types.js\";\n\nexport const consumerFromStringOrObject = (\n consumer: ApitallyConsumer | string,\n) => {\n if (typeof consumer === \"string\") {\n consumer = String(consumer).trim().substring(0, 128);\n return consumer ? { identifier: consumer } : null;\n } else {\n consumer.identifier = String(consumer.identifier).trim().substring(0, 128);\n consumer.name = consumer.name?.trim().substring(0, 64);\n consumer.group = consumer.group?.trim().substring(0, 64);\n return consumer.identifier ? consumer : null;\n }\n};\n\nexport default class ConsumerRegistry {\n private consumers: Map<string, ApitallyConsumer>;\n private updated: Set<string>;\n\n constructor() {\n this.consumers = new Map();\n this.updated = new Set();\n }\n\n public addOrUpdateConsumer(consumer?: ApitallyConsumer | null) {\n if (!consumer || (!consumer.name && !consumer.group)) {\n return;\n }\n const existing = this.consumers.get(consumer.identifier);\n if (!existing) {\n this.consumers.set(consumer.identifier, consumer);\n this.updated.add(consumer.identifier);\n } else {\n if (consumer.name && consumer.name !== existing.name) {\n existing.name = consumer.name;\n this.updated.add(consumer.identifier);\n }\n if (consumer.group && consumer.group !== existing.group) {\n existing.group = consumer.group;\n this.updated.add(consumer.identifier);\n }\n }\n }\n\n public getAndResetUpdatedConsumers() {\n const data: Array<ApitallyConsumer> = [];\n this.updated.forEach((identifier) => {\n const consumer = this.consumers.get(identifier);\n if (consumer) {\n data.push(consumer);\n }\n });\n this.updated.clear();\n return data;\n }\n}\n","import { OutgoingHttpHeader } from \"node:http\";\n\nexport function parseContentLength(\n contentLength: OutgoingHttpHeader | undefined | null,\n): number | undefined {\n if (contentLength === undefined || contentLength === null) {\n return undefined;\n }\n if (typeof contentLength === \"number\") {\n return contentLength;\n }\n if (typeof contentLength === \"string\") {\n const parsed = parseInt(contentLength);\n return isNaN(parsed) ? undefined : parsed;\n }\n if (Array.isArray(contentLength)) {\n return parseContentLength(contentLength[0]);\n }\n return undefined;\n}\n\nexport function mergeHeaders(base: Headers, merge: Headers) {\n const mergedHeaders = new Headers(base);\n for (const [name, value] of merge)\n if (name === \"set-cookie\") mergedHeaders.append(name, value);\n else mergedHeaders.set(name, value);\n return mergedHeaders;\n}\n","import AsyncLock from \"async-lock\";\nimport { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { unlinkSync, writeFileSync } from \"node:fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"node:http\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst MAX_LOG_MSG_LENGTH = 2048;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n \"text/html\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\nconst MASK_BODY_FIELD_PATTERNS = [\n /password/i,\n /pwd/i,\n /token/i,\n /secret/i,\n /auth/i,\n /card[-_ ]?number/i,\n /ccv/i,\n /ssn/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type LogRecord = {\n timestamp: number;\n logger?: string;\n level: string;\n message: string;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n captureLogs: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskBodyFields: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n captureLogs: false,\n maskQueryParams: [],\n maskHeaders: [],\n maskBodyFields: [],\n excludePaths: [],\n};\n\ntype RequestLogItem = {\n uuid: string;\n request: Request;\n response: Response;\n exception?: {\n type: string;\n message: string;\n stacktrace: string;\n sentryEventId?: string;\n };\n logs?: LogRecord[];\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: RequestLogItem[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskBodyField(name: string) {\n const patterns = [\n ...this.config.maskBodyFields,\n ...MASK_BODY_FIELD_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n private hasJsonContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return contentType ? /\\bjson\\b/i.test(contentType) : null;\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n private maskBody(data: any): any {\n if (typeof data === \"object\" && data !== null && !Array.isArray(data)) {\n const result: any = {};\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === \"string\" && this.shouldMaskBodyField(key)) {\n result[key] = MASKED;\n } else {\n result[key] = this.maskBody(value);\n }\n }\n return result;\n }\n if (Array.isArray(data)) {\n return data.map((item) => this.maskBody(item));\n }\n return data;\n }\n\n private applyMasking(item: RequestLogItem) {\n // Apply user-provided maskRequestBodyCallback function\n if (\n this.config.maskRequestBodyCallback &&\n item.request.body &&\n item.request.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskRequestBodyCallback(item.request);\n item.request.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.request.body = undefined;\n }\n }\n\n // Apply user-provided maskResponseBodyCallback function\n if (\n this.config.maskResponseBodyCallback &&\n item.response.body &&\n item.response.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskResponseBodyCallback(\n item.request,\n item.response,\n );\n item.response.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.response.body = undefined;\n }\n }\n\n // Check request and response body sizes\n if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {\n item.request.body = BODY_TOO_LARGE;\n }\n if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {\n item.response.body = BODY_TOO_LARGE;\n }\n\n // Mask request and response body fields\n for (const key of [\"request\", \"response\"] as const) {\n const bodyData = item[key].body;\n if (\n !bodyData ||\n bodyData === BODY_TOO_LARGE ||\n bodyData === BODY_MASKED\n ) {\n continue;\n }\n\n const headers = item[key].headers;\n const hasJsonContent = this.hasJsonContentType(headers);\n if (hasJsonContent === null || hasJsonContent) {\n try {\n const parsedBody = JSON.parse(bodyData.toString());\n const maskedBody = this.maskBody(parsedBody);\n item[key].body = Buffer.from(JSON.stringify(maskedBody));\n } catch {\n // If parsing fails, leave body as is\n }\n }\n }\n\n // Mask request and response headers\n item.request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(item.request.headers)\n : [];\n item.response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(item.response.headers)\n : [];\n\n // Mask query params\n const url = new URL(item.request.url);\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n item.request.url = url.toString();\n\n return item;\n }\n\n logRequest(\n request: Request,\n response: Response,\n error?: Error,\n logs?: LogRecord[],\n ) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n }\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n }\n\n if (request.size !== undefined && request.size < 0) {\n request.size = undefined;\n }\n if (response.size !== undefined && response.size < 0) {\n response.size = undefined;\n }\n\n const item: RequestLogItem = {\n uuid: randomUUID(),\n request: request,\n response: response,\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : undefined,\n };\n\n if (logs && logs.length > 0) {\n item.logs = logs.map((log) => ({\n timestamp: log.timestamp,\n logger: log.logger,\n level: log.level,\n message: truncateLogMessage(log.message),\n }));\n }\n this.pendingWrites.push(item);\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n let item = this.pendingWrites.shift();\n if (item) {\n item = this.applyMasking(item);\n\n const finalItem = {\n uuid: item.uuid,\n request: skipEmptyValues(item.request),\n response: skipEmptyValues(item.response),\n exception: item.exception,\n logs: item.logs,\n };\n\n // Set up body serialization for JSON\n [finalItem.request.body, finalItem.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Override Buffer's default JSON serialization\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n\n await this.currentFile.writeLine(\n Buffer.from(JSON.stringify(finalItem)),\n );\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>\n | undefined,\n) {\n if (!headers) {\n return [];\n }\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction truncateLogMessage(msg: string) {\n if (msg.length > MAX_LOG_MSG_LENGTH) {\n const suffix = \"... (truncated)\";\n return msg.slice(0, MAX_LOG_MSG_LENGTH - suffix.length) + suffix;\n }\n return msg;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n","import { createHash } from \"node:crypto\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport { ConsumerMethodPath, ServerError, ServerErrorsItem } from \"./types.js\";\n\nconst MAX_MSG_LENGTH = 2048;\nconst MAX_STACKTRACE_LENGTH = 65536;\n\nexport default class ServerErrorCounter {\n private errorCounts: Map<string, number>;\n private errorDetails: Map<string, ConsumerMethodPath & ServerError>;\n private sentryEventIds: Map<string, string>;\n\n constructor() {\n this.errorCounts = new Map();\n this.errorDetails = new Map();\n this.sentryEventIds = new Map();\n }\n\n public addServerError(serverError: ConsumerMethodPath & ServerError) {\n const key = this.getKey(serverError);\n if (!this.errorDetails.has(key)) {\n this.errorDetails.set(key, serverError);\n }\n this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);\n\n const sentryEventId = getSentryEventId();\n if (sentryEventId) {\n this.sentryEventIds.set(key, sentryEventId);\n }\n }\n\n public getAndResetServerErrors() {\n const data: Array<ServerErrorsItem> = [];\n this.errorCounts.forEach((count, key) => {\n const serverError = this.errorDetails.get(key);\n if (serverError) {\n data.push({\n consumer: serverError.consumer || null,\n method: serverError.method,\n path: serverError.path,\n type: serverError.type,\n msg: truncateExceptionMessage(serverError.msg),\n traceback: truncateExceptionStackTrace(serverError.traceback),\n sentry_event_id: this.sentryEventIds.get(key) || null,\n error_count: count,\n });\n }\n });\n this.errorCounts.clear();\n this.errorDetails.clear();\n this.sentryEventIds.clear();\n return data;\n }\n\n private getKey(serverError: ConsumerMethodPath & ServerError) {\n const hashInput = [\n serverError.consumer || \"\",\n serverError.method.toUpperCase(),\n serverError.path,\n serverError.type,\n serverError.msg.trim(),\n serverError.traceback.trim(),\n ].join(\"|\");\n return createHash(\"md5\").update(hashInput).digest(\"hex\");\n }\n}\n\nexport function truncateExceptionMessage(msg: string) {\n if (msg.length <= MAX_MSG_LENGTH) {\n return msg;\n }\n const suffix = \"... (truncated)\";\n const cutoff = MAX_MSG_LENGTH - suffix.length;\n return msg.substring(0, cutoff) + suffix;\n}\n\nexport function truncateExceptionStackTrace(stack: string) {\n const suffix = \"... (truncated) ...\";\n const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;\n const lines = stack.trim().split(\"\\n\");\n const truncatedLines: string[] = [];\n let length = 0;\n for (const line of lines) {\n if (length + line.length + 1 > cutoff) {\n truncatedLines.push(suffix);\n break;\n }\n truncatedLines.push(line);\n length += line.length + 1;\n }\n return truncatedLines.join(\"\\n\");\n}\n","import { Buffer } from \"node:buffer\";\nimport { randomUUID } from \"node:crypto\";\nimport { createWriteStream, readFile, WriteStream } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, Gzip } from \"node:zlib\";\n\nexport default class TempGzipFile {\n public uuid: string;\n private filePath: string;\n private gzip: Gzip;\n private writeStream: WriteStream;\n private readyPromise: Promise<void>;\n private closedPromise: Promise<void>;\n\n constructor() {\n this.uuid = randomUUID();\n this.filePath = join(tmpdir(), `apitally-${this.uuid}.gz`);\n this.writeStream = createWriteStream(this.filePath);\n this.readyPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"ready\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.closedPromise = new Promise<void>((resolve, reject) => {\n this.writeStream.once(\"close\", resolve);\n this.writeStream.once(\"error\", reject);\n });\n this.gzip = createGzip();\n this.gzip.pipe(this.writeStream);\n }\n\n get size() {\n return this.writeStream.bytesWritten;\n }\n\n async writeLine(data: Buffer) {\n await this.readyPromise;\n return new Promise<void>((resolve, reject) => {\n this.gzip.write(Buffer.concat([data, Buffer.from(\"\\n\")]), (error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n async getContent() {\n return new Promise<Buffer>((resolve, reject) => {\n readFile(this.filePath, (error, data) => {\n if (error) {\n reject(error);\n } else {\n resolve(data);\n }\n });\n });\n }\n\n async close() {\n await new Promise<void>((resolve) => {\n this.gzip.end(() => {\n resolve();\n });\n });\n await this.closedPromise;\n }\n\n async delete() {\n await this.close();\n await unlink(this.filePath);\n }\n}\n"],"mappings":"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","names":["performance","consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","AsyncLock","Buffer","randomUUID","unlinkSync","writeFileSync","tmpdir","join","sentry","e","createHash","Buffer","randomUUID","createWriteStream","readFile","unlink","tmpdir","join","createGzip","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","logsContext","isEnabled","request","method","toUpperCase","run","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","apitallyError","code","Array","isArray","messages","forEach","message","validationErrorCounter","addValidationError","loc","field","msg","type","rule","serverErrorCounter","addServerError","name","traceback","stack","originalWriteHead","writeHead","args","apply","requestLogger","enabled","logs","getStore","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
|