apitally 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -31
- package/configure.ts +35 -0
- package/dist/adonisjs/index.cjs +6 -4
- package/dist/adonisjs/index.cjs.map +1 -1
- package/dist/adonisjs/index.d.cts +2 -2
- package/dist/adonisjs/index.d.ts +2 -2
- package/dist/adonisjs/index.js +6 -4
- package/dist/adonisjs/index.js.map +1 -1
- package/dist/adonisjs/middleware.cjs +19 -19
- package/dist/adonisjs/middleware.cjs.map +1 -1
- package/dist/adonisjs/middleware.js +19 -19
- package/dist/adonisjs/middleware.js.map +1 -1
- package/dist/adonisjs/provider.cjs +11 -4
- package/dist/adonisjs/provider.cjs.map +1 -1
- package/dist/adonisjs/provider.js +11 -4
- package/dist/adonisjs/provider.js.map +1 -1
- package/dist/common/client.cjs +5 -0
- package/dist/common/client.cjs.map +1 -1
- package/dist/common/client.js +5 -0
- package/dist/common/client.js.map +1 -1
- package/dist/common/{utils.cjs → headers.cjs} +14 -5
- package/dist/common/headers.cjs.map +1 -0
- package/dist/common/{utils.d.cts → headers.d.cts} +2 -1
- package/dist/common/{utils.d.ts → headers.d.ts} +2 -1
- package/dist/common/{utils.js → headers.js} +10 -2
- package/dist/common/headers.js.map +1 -0
- package/dist/common/requestLogger.cjs +5 -0
- package/dist/common/requestLogger.cjs.map +1 -1
- package/dist/common/requestLogger.d.cts +1 -0
- package/dist/common/requestLogger.d.ts +1 -0
- package/dist/common/requestLogger.js +5 -0
- package/dist/common/requestLogger.js.map +1 -1
- package/dist/common/response.cjs +106 -0
- package/dist/common/response.cjs.map +1 -0
- package/dist/common/response.d.cts +6 -0
- package/dist/common/response.d.ts +6 -0
- package/dist/common/response.js +80 -0
- package/dist/common/response.js.map +1 -0
- package/dist/express/index.cjs +69 -42
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.d.cts +2 -2
- package/dist/express/index.d.ts +2 -2
- package/dist/express/index.js +67 -41
- package/dist/express/index.js.map +1 -1
- package/dist/express/middleware.cjs +69 -42
- package/dist/express/middleware.cjs.map +1 -1
- package/dist/express/middleware.d.cts +5 -4
- package/dist/express/middleware.d.ts +5 -4
- package/dist/express/middleware.js +67 -41
- package/dist/express/middleware.js.map +1 -1
- package/dist/fastify/index.cjs +44 -29
- package/dist/fastify/index.cjs.map +1 -1
- package/dist/fastify/index.d.cts +2 -2
- package/dist/fastify/index.d.ts +2 -2
- package/dist/fastify/index.js +41 -27
- package/dist/fastify/index.js.map +1 -1
- package/dist/fastify/plugin.cjs +46 -28
- package/dist/fastify/plugin.cjs.map +1 -1
- package/dist/fastify/plugin.d.cts +3 -2
- package/dist/fastify/plugin.d.ts +3 -2
- package/dist/fastify/plugin.js +41 -27
- package/dist/fastify/plugin.js.map +1 -1
- package/dist/h3/index.cjs +1336 -0
- package/dist/h3/index.cjs.map +1 -0
- package/dist/h3/index.d.cts +9 -0
- package/dist/h3/index.d.ts +9 -0
- package/dist/h3/index.js +1305 -0
- package/dist/h3/index.js.map +1 -0
- package/dist/h3/plugin.cjs +1334 -0
- package/dist/h3/plugin.cjs.map +1 -0
- package/dist/h3/plugin.d.cts +21 -0
- package/dist/h3/plugin.d.ts +21 -0
- package/dist/h3/plugin.js +1305 -0
- package/dist/h3/plugin.js.map +1 -0
- package/dist/h3/utils.cjs +127 -0
- package/dist/h3/utils.cjs.map +1 -0
- package/dist/h3/utils.d.cts +14 -0
- package/dist/h3/utils.d.ts +14 -0
- package/dist/h3/utils.js +107 -0
- package/dist/h3/utils.js.map +1 -0
- package/dist/hono/index.cjs +177 -141
- package/dist/hono/index.cjs.map +1 -1
- package/dist/hono/index.d.cts +2 -2
- package/dist/hono/index.d.ts +2 -2
- package/dist/hono/index.js +175 -140
- package/dist/hono/index.js.map +1 -1
- package/dist/hono/middleware.cjs +177 -141
- package/dist/hono/middleware.cjs.map +1 -1
- package/dist/hono/middleware.d.cts +4 -3
- package/dist/hono/middleware.d.ts +4 -3
- package/dist/hono/middleware.js +175 -140
- package/dist/hono/middleware.js.map +1 -1
- package/dist/hono/utils.cjs +126 -0
- package/dist/hono/utils.cjs.map +1 -0
- package/dist/hono/utils.d.cts +14 -0
- package/dist/hono/utils.d.ts +14 -0
- package/dist/hono/utils.js +106 -0
- package/dist/hono/utils.js.map +1 -0
- package/dist/koa/index.cjs +41 -17
- package/dist/koa/index.cjs.map +1 -1
- package/dist/koa/index.d.cts +2 -2
- package/dist/koa/index.d.ts +2 -2
- package/dist/koa/index.js +40 -17
- package/dist/koa/index.js.map +1 -1
- package/dist/koa/middleware.cjs +41 -17
- package/dist/koa/middleware.cjs.map +1 -1
- package/dist/koa/middleware.d.cts +4 -3
- package/dist/koa/middleware.d.ts +4 -3
- package/dist/koa/middleware.js +40 -17
- package/dist/koa/middleware.js.map +1 -1
- package/dist/nestjs/index.cjs +66 -44
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +2 -2
- package/dist/nestjs/index.d.ts +2 -2
- package/dist/nestjs/index.js +65 -43
- package/dist/nestjs/index.js.map +1 -1
- package/package.json +25 -4
- package/stubs/config/apitally.stub +15 -0
- package/stubs/main.ts +3 -0
- package/dist/common/utils.cjs.map +0 -1
- package/dist/common/utils.js.map +0 -1
package/README.md
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
<picture>
|
|
4
4
|
<source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-horizontal-new-dark.png">
|
|
5
5
|
<source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-horizontal-new-light.png">
|
|
6
|
-
<img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-
|
|
6
|
+
<img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-horizontal-new-light.png" width="220">
|
|
7
7
|
</picture>
|
|
8
8
|
</a>
|
|
9
9
|
</p>
|
|
10
|
-
<p align="center"><b>
|
|
11
|
-
<p align="center" style="color: #ccc;">
|
|
10
|
+
<p align="center"><b>API monitoring & analytics made simple</b></p>
|
|
11
|
+
<p align="center" style="color: #ccc;">Real-time metrics, request logs, and alerts for your APIs — with just a few lines of code.</p>
|
|
12
12
|
<br>
|
|
13
13
|
<img alt="Apitally screenshots" src="https://assets.apitally.io/screenshots/overview.png">
|
|
14
14
|
<br>
|
|
@@ -27,6 +27,7 @@ frameworks:
|
|
|
27
27
|
- [Fastify](https://docs.apitally.io/frameworks/fastify)
|
|
28
28
|
- [Koa](https://docs.apitally.io/frameworks/koa)
|
|
29
29
|
- [Hono](https://docs.apitally.io/frameworks/hono)
|
|
30
|
+
- [H3](https://docs.apitally.io/frameworks/h3) (v2)
|
|
30
31
|
- [AdonisJS](https://docs.apitally.io/frameworks/adonisjs)
|
|
31
32
|
|
|
32
33
|
Learn more about Apitally on our 🌎 [website](https://apitally.io) or check out
|
|
@@ -180,46 +181,51 @@ useApitally(app, {
|
|
|
180
181
|
});
|
|
181
182
|
```
|
|
182
183
|
|
|
183
|
-
###
|
|
184
|
+
### H3
|
|
184
185
|
|
|
185
|
-
This is an example of how to
|
|
186
|
+
This is an example of how to register the Apitally plugin with an H3 application.
|
|
186
187
|
For further instructions, see our
|
|
187
|
-
[setup guide for
|
|
188
|
-
|
|
189
|
-
Create a configuration file at `config/apitally.ts`:
|
|
188
|
+
[setup guide for H3](https://docs.apitally.io/frameworks/h3).
|
|
190
189
|
|
|
191
190
|
```javascript
|
|
192
|
-
import {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
import { H3 } from "h3";
|
|
192
|
+
import { apitallyPlugin } from "apitally/h3";
|
|
193
|
+
|
|
194
|
+
const app = new H3({
|
|
195
|
+
plugins: [
|
|
196
|
+
apitallyPlugin({
|
|
197
|
+
clientId: "your-client-id",
|
|
198
|
+
env: "dev", // or "prod" etc.
|
|
199
|
+
}),
|
|
200
|
+
],
|
|
197
201
|
});
|
|
198
|
-
|
|
199
|
-
export default apitallyConfig;
|
|
200
202
|
```
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
_Note:_ Apitally only works with H3 v2 and currently doesn't support nested apps.
|
|
203
205
|
|
|
204
|
-
|
|
205
|
-
export default defineConfig({
|
|
206
|
-
// ... existing code ...
|
|
207
|
-
providers: [
|
|
208
|
-
// ... existing providers ...
|
|
209
|
-
() => import("apitally/adonisjs/provider"),
|
|
210
|
-
],
|
|
211
|
-
});
|
|
212
|
-
```
|
|
206
|
+
### AdonisJS
|
|
213
207
|
|
|
214
|
-
|
|
208
|
+
You can use the built-in Ace command to set up Apitally in your AdonisJS application:
|
|
215
209
|
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
() => import("apitally/adonisjs/middleware"),
|
|
219
|
-
// ... other middleware ...
|
|
220
|
-
]);
|
|
210
|
+
```bash
|
|
211
|
+
node ace add apitally
|
|
221
212
|
```
|
|
222
213
|
|
|
214
|
+
This command will automatically:
|
|
215
|
+
|
|
216
|
+
- Create the configuration file at `config/apitally.ts`
|
|
217
|
+
- Register the Apitally provider in `adonisrc.ts`
|
|
218
|
+
- Add the Apitally middleware to your `start/kernel.ts` file
|
|
219
|
+
- Add environment variables to `.env` and validation to `start/env.ts`
|
|
220
|
+
|
|
221
|
+
After running the command, you'll need to:
|
|
222
|
+
|
|
223
|
+
- Set your `APITALLY_CLIENT_ID` in the `.env` file
|
|
224
|
+
- Modify our exception handler in `app/exceptions/handler.ts` to capture validation and server errors in Apitally
|
|
225
|
+
|
|
226
|
+
For further instructions, see our
|
|
227
|
+
[setup guide for AdonisJS](https://docs.apitally.io/frameworks/adonisjs).
|
|
228
|
+
|
|
223
229
|
## Getting help
|
|
224
230
|
|
|
225
231
|
If you need help please [create a new discussion](https://github.com/orgs/apitally/discussions/categories/q-a) on GitHub
|
package/configure.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allows users to add Apitally to their AdonisJS application using the built-in Ace command.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type Configure from "@adonisjs/core/commands/configure";
|
|
6
|
+
import { stubsRoot } from "./stubs/main.js";
|
|
7
|
+
|
|
8
|
+
export async function configure(command: Configure) {
|
|
9
|
+
const codemods = await command.createCodemods();
|
|
10
|
+
|
|
11
|
+
await codemods.makeUsingStub(stubsRoot, "config/apitally.stub", {});
|
|
12
|
+
|
|
13
|
+
await codemods.registerMiddleware("router", [
|
|
14
|
+
{
|
|
15
|
+
path: "apitally/adonisjs/middleware",
|
|
16
|
+
},
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
await codemods.updateRcFile((rcFile) => {
|
|
20
|
+
rcFile.addProvider("apitally/adonisjs/provider");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await codemods.defineEnvVariables({
|
|
24
|
+
APITALLY_CLIENT_ID: "",
|
|
25
|
+
APITALLY_ENV: "dev",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await codemods.defineEnvValidations({
|
|
29
|
+
leadingComment: "Variables for configuring the apitally package",
|
|
30
|
+
variables: {
|
|
31
|
+
APITALLY_CLIENT_ID: "Env.schema.string()",
|
|
32
|
+
APITALLY_ENV: "Env.schema.string.optional()",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
package/dist/adonisjs/index.cjs
CHANGED
|
@@ -25,14 +25,16 @@ __export(adonisjs_exports, {
|
|
|
25
25
|
defineConfig: () => defineConfig
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(adonisjs_exports);
|
|
28
|
-
|
|
28
|
+
function defineConfig(config) {
|
|
29
29
|
return config;
|
|
30
|
-
}
|
|
31
|
-
|
|
30
|
+
}
|
|
31
|
+
__name(defineConfig, "defineConfig");
|
|
32
|
+
function captureError(error, ctx) {
|
|
32
33
|
if (error instanceof Error) {
|
|
33
34
|
ctx.apitallyError = error;
|
|
34
35
|
}
|
|
35
|
-
}
|
|
36
|
+
}
|
|
37
|
+
__name(captureError, "captureError");
|
|
36
38
|
// Annotate the CommonJS export names for ESM import in node:
|
|
37
39
|
0 && (module.exports = {
|
|
38
40
|
captureError,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adonisjs/index.ts"],"sourcesContent":["import type { HttpContext } from \"@adonisjs/core/http\";\n\nimport type { ApitallyConfig } from \"../common/types.js\";\nexport type { ApitallyConfig, ApitallyConsumer } from \"../common/types.js\";\n\nexport
|
|
1
|
+
{"version":3,"sources":["../../src/adonisjs/index.ts"],"sourcesContent":["import type { HttpContext } from \"@adonisjs/core/http\";\n\nimport type { ApitallyConfig } from \"../common/types.js\";\nexport type { ApitallyConfig, ApitallyConsumer } from \"../common/types.js\";\n\nexport function defineConfig(config: ApitallyConfig) {\n return config;\n}\n\nexport function captureError(error: unknown, ctx: HttpContext) {\n if (error instanceof Error) {\n ctx.apitallyError = error;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAKA;;;;;;AAAO,SAASA,aAAaC,QAAsB;AACjD,SAAOA;AACT;AAFgBD;AAIT,SAASE,aAAaC,OAAgBC,KAAgB;AAC3D,MAAID,iBAAiBE,OAAO;AAC1BD,QAAIE,gBAAgBH;EACtB;AACF;AAJgBD;","names":["defineConfig","config","captureError","error","ctx","Error","apitallyError"]}
|
|
@@ -8,7 +8,7 @@ import 'buffer';
|
|
|
8
8
|
import 'http';
|
|
9
9
|
import '../common/tempGzipFile.cjs';
|
|
10
10
|
|
|
11
|
-
declare
|
|
12
|
-
declare
|
|
11
|
+
declare function defineConfig(config: ApitallyConfig): ApitallyConfig;
|
|
12
|
+
declare function captureError(error: unknown, ctx: HttpContext): void;
|
|
13
13
|
|
|
14
14
|
export { ApitallyConfig, captureError, defineConfig };
|
package/dist/adonisjs/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import 'buffer';
|
|
|
8
8
|
import 'http';
|
|
9
9
|
import '../common/tempGzipFile.js';
|
|
10
10
|
|
|
11
|
-
declare
|
|
12
|
-
declare
|
|
11
|
+
declare function defineConfig(config: ApitallyConfig): ApitallyConfig;
|
|
12
|
+
declare function captureError(error: unknown, ctx: HttpContext): void;
|
|
13
13
|
|
|
14
14
|
export { ApitallyConfig, captureError, defineConfig };
|
package/dist/adonisjs/index.js
CHANGED
|
@@ -2,14 +2,16 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
4
|
// src/adonisjs/index.ts
|
|
5
|
-
|
|
5
|
+
function defineConfig(config) {
|
|
6
6
|
return config;
|
|
7
|
-
}
|
|
8
|
-
|
|
7
|
+
}
|
|
8
|
+
__name(defineConfig, "defineConfig");
|
|
9
|
+
function captureError(error, ctx) {
|
|
9
10
|
if (error instanceof Error) {
|
|
10
11
|
ctx.apitallyError = error;
|
|
11
12
|
}
|
|
12
|
-
}
|
|
13
|
+
}
|
|
14
|
+
__name(captureError, "captureError");
|
|
13
15
|
export {
|
|
14
16
|
captureError,
|
|
15
17
|
defineConfig
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adonisjs/index.ts"],"sourcesContent":["import type { HttpContext } from \"@adonisjs/core/http\";\n\nimport type { ApitallyConfig } from \"../common/types.js\";\nexport type { ApitallyConfig, ApitallyConsumer } from \"../common/types.js\";\n\nexport
|
|
1
|
+
{"version":3,"sources":["../../src/adonisjs/index.ts"],"sourcesContent":["import type { HttpContext } from \"@adonisjs/core/http\";\n\nimport type { ApitallyConfig } from \"../common/types.js\";\nexport type { ApitallyConfig, ApitallyConsumer } from \"../common/types.js\";\n\nexport function defineConfig(config: ApitallyConfig) {\n return config;\n}\n\nexport function captureError(error: unknown, ctx: HttpContext) {\n if (error instanceof Error) {\n ctx.apitallyError = error;\n }\n}\n"],"mappings":";;;;AAKO,SAASA,aAAaC,QAAsB;AACjD,SAAOA;AACT;AAFgBD;AAIT,SAASE,aAAaC,OAAgBC,KAAgB;AAC3D,MAAID,iBAAiBE,OAAO;AAC1BD,QAAIE,gBAAgBH;EACtB;AACF;AAJgBD;","names":["defineConfig","config","captureError","error","ctx","Error","apitallyError"]}
|
|
@@ -52,6 +52,25 @@ var consumerFromStringOrObject = /* @__PURE__ */ __name((consumer) => {
|
|
|
52
52
|
}
|
|
53
53
|
}, "consumerFromStringOrObject");
|
|
54
54
|
|
|
55
|
+
// src/common/headers.ts
|
|
56
|
+
function parseContentLength(contentLength) {
|
|
57
|
+
if (contentLength === void 0 || contentLength === null) {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
if (typeof contentLength === "number") {
|
|
61
|
+
return contentLength;
|
|
62
|
+
}
|
|
63
|
+
if (typeof contentLength === "string") {
|
|
64
|
+
const parsed = parseInt(contentLength);
|
|
65
|
+
return isNaN(parsed) ? void 0 : parsed;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(contentLength)) {
|
|
68
|
+
return parseContentLength(contentLength[0]);
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
__name(parseContentLength, "parseContentLength");
|
|
73
|
+
|
|
55
74
|
// src/common/requestLogger.ts
|
|
56
75
|
var import_async_lock = __toESM(require("async-lock"), 1);
|
|
57
76
|
var import_buffer = require("buffer");
|
|
@@ -92,25 +111,6 @@ function convertHeaders(headers) {
|
|
|
92
111
|
}
|
|
93
112
|
__name(convertHeaders, "convertHeaders");
|
|
94
113
|
|
|
95
|
-
// src/common/utils.ts
|
|
96
|
-
function parseContentLength(contentLength) {
|
|
97
|
-
if (contentLength === void 0 || contentLength === null) {
|
|
98
|
-
return void 0;
|
|
99
|
-
}
|
|
100
|
-
if (typeof contentLength === "number") {
|
|
101
|
-
return contentLength;
|
|
102
|
-
}
|
|
103
|
-
if (typeof contentLength === "string") {
|
|
104
|
-
const parsed = parseInt(contentLength);
|
|
105
|
-
return isNaN(parsed) ? void 0 : parsed;
|
|
106
|
-
}
|
|
107
|
-
if (Array.isArray(contentLength)) {
|
|
108
|
-
return parseContentLength(contentLength[0]);
|
|
109
|
-
}
|
|
110
|
-
return void 0;
|
|
111
|
-
}
|
|
112
|
-
__name(parseContentLength, "parseContentLength");
|
|
113
|
-
|
|
114
114
|
// src/adonisjs/middleware.ts
|
|
115
115
|
var _ApitallyMiddleware = class _ApitallyMiddleware {
|
|
116
116
|
async handle(ctx, next) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts","../../src/common/utils.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport type { OutgoingHttpHeaders } from \"http\";\nimport { performance } from \"perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\nimport { parseContentLength } from \"../common/utils.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\n if (!client.isEnabled()) {\n await next();\n return;\n }\n\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 = (statusCode: number, headers: OutgoingHttpHeaders) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\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 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\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 );\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","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 AsyncLock from \"async-lock\";\nimport { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"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 BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\"application/json\", \"text/plain\"];\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];\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 RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: 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 maskQueryParams: [],\n maskHeaders: [],\n excludePaths: [],\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: string[] = [];\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 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 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 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 logRequest(request: Request, response: Response, error?: Error) {\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 // Process query params\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n request.url = url.toString();\n\n // Process request body\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n } else if (request.body) {\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n } else if (this.config.maskRequestBodyCallback) {\n try {\n request.body =\n this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n }\n } catch {\n request.body = undefined;\n }\n }\n }\n\n // Process response body\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n } else if (response.body) {\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n } else if (this.config.maskResponseBodyCallback) {\n try {\n response.body =\n this.config.maskResponseBodyCallback(request, response) ??\n BODY_MASKED;\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n }\n } catch {\n response.body = undefined;\n }\n }\n }\n\n // Process headers\n request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(request.headers)\n : [];\n response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(response.headers)\n : [];\n\n const item = {\n uuid: randomUUID(),\n request: skipEmptyValues(request),\n response: skipEmptyValues(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 : null,\n };\n [item.request.body, item.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Different return type\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n this.pendingWrites.push(JSON.stringify(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 const item = this.pendingWrites.shift();\n if (item) {\n await this.currentFile.writeLine(Buffer.from(item));\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 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 { OutgoingHttpHeader } from \"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"],"mappings":"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","names":["consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","sentry","e","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","isEnabled","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","request","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","method","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","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"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport type { OutgoingHttpHeaders } from \"http\";\nimport { performance } from \"perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.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\n if (!client.isEnabled()) {\n await next();\n return;\n }\n\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 = (statusCode: number, headers: OutgoingHttpHeaders) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\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 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\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 );\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","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 \"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 \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"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 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];\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 RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: 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 maskQueryParams: [],\n maskHeaders: [],\n excludePaths: [],\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: string[] = [];\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 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 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 logRequest(request: Request, response: Response, error?: Error) {\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 // Process query params\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n request.url = url.toString();\n\n // Process request body\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n } else if (request.body) {\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n } else if (this.config.maskRequestBodyCallback) {\n try {\n request.body =\n this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n }\n } catch {\n request.body = undefined;\n }\n }\n }\n\n // Process response body\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n } else if (response.body) {\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n } else if (this.config.maskResponseBodyCallback) {\n try {\n response.body =\n this.config.maskResponseBodyCallback(request, response) ??\n BODY_MASKED;\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n }\n } catch {\n response.body = undefined;\n }\n }\n }\n\n // Process headers\n request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(request.headers)\n : [];\n response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(response.headers)\n : [];\n\n const item = {\n uuid: randomUUID(),\n request: skipEmptyValues(request),\n response: skipEmptyValues(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 : null,\n };\n [item.request.body, item.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Different return type\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n this.pendingWrites.push(JSON.stringify(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 const item = this.pendingWrites.shift();\n if (item) {\n await this.currentFile.writeLine(Buffer.from(item));\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 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"],"mappings":"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","names":["consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","sentry","e","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","isEnabled","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","request","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","method","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","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
|
|
@@ -20,6 +20,25 @@ var consumerFromStringOrObject = /* @__PURE__ */ __name((consumer) => {
|
|
|
20
20
|
}
|
|
21
21
|
}, "consumerFromStringOrObject");
|
|
22
22
|
|
|
23
|
+
// src/common/headers.ts
|
|
24
|
+
function parseContentLength(contentLength) {
|
|
25
|
+
if (contentLength === void 0 || contentLength === null) {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
if (typeof contentLength === "number") {
|
|
29
|
+
return contentLength;
|
|
30
|
+
}
|
|
31
|
+
if (typeof contentLength === "string") {
|
|
32
|
+
const parsed = parseInt(contentLength);
|
|
33
|
+
return isNaN(parsed) ? void 0 : parsed;
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(contentLength)) {
|
|
36
|
+
return parseContentLength(contentLength[0]);
|
|
37
|
+
}
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
__name(parseContentLength, "parseContentLength");
|
|
41
|
+
|
|
23
42
|
// src/common/requestLogger.ts
|
|
24
43
|
import AsyncLock from "async-lock";
|
|
25
44
|
import { Buffer as Buffer2 } from "buffer";
|
|
@@ -60,25 +79,6 @@ function convertHeaders(headers) {
|
|
|
60
79
|
}
|
|
61
80
|
__name(convertHeaders, "convertHeaders");
|
|
62
81
|
|
|
63
|
-
// src/common/utils.ts
|
|
64
|
-
function parseContentLength(contentLength) {
|
|
65
|
-
if (contentLength === void 0 || contentLength === null) {
|
|
66
|
-
return void 0;
|
|
67
|
-
}
|
|
68
|
-
if (typeof contentLength === "number") {
|
|
69
|
-
return contentLength;
|
|
70
|
-
}
|
|
71
|
-
if (typeof contentLength === "string") {
|
|
72
|
-
const parsed = parseInt(contentLength);
|
|
73
|
-
return isNaN(parsed) ? void 0 : parsed;
|
|
74
|
-
}
|
|
75
|
-
if (Array.isArray(contentLength)) {
|
|
76
|
-
return parseContentLength(contentLength[0]);
|
|
77
|
-
}
|
|
78
|
-
return void 0;
|
|
79
|
-
}
|
|
80
|
-
__name(parseContentLength, "parseContentLength");
|
|
81
|
-
|
|
82
82
|
// src/adonisjs/middleware.ts
|
|
83
83
|
var _ApitallyMiddleware = class _ApitallyMiddleware {
|
|
84
84
|
async handle(ctx, next) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts","../../src/common/utils.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport type { OutgoingHttpHeaders } from \"http\";\nimport { performance } from \"perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\nimport { parseContentLength } from \"../common/utils.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\n if (!client.isEnabled()) {\n await next();\n return;\n }\n\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 = (statusCode: number, headers: OutgoingHttpHeaders) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\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 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\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 );\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","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 AsyncLock from \"async-lock\";\nimport { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"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 BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\"application/json\", \"text/plain\"];\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];\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 RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: 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 maskQueryParams: [],\n maskHeaders: [],\n excludePaths: [],\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: string[] = [];\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 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 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 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 logRequest(request: Request, response: Response, error?: Error) {\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 // Process query params\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n request.url = url.toString();\n\n // Process request body\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n } else if (request.body) {\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n } else if (this.config.maskRequestBodyCallback) {\n try {\n request.body =\n this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n }\n } catch {\n request.body = undefined;\n }\n }\n }\n\n // Process response body\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n } else if (response.body) {\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n } else if (this.config.maskResponseBodyCallback) {\n try {\n response.body =\n this.config.maskResponseBodyCallback(request, response) ??\n BODY_MASKED;\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n }\n } catch {\n response.body = undefined;\n }\n }\n }\n\n // Process headers\n request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(request.headers)\n : [];\n response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(response.headers)\n : [];\n\n const item = {\n uuid: randomUUID(),\n request: skipEmptyValues(request),\n response: skipEmptyValues(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 : null,\n };\n [item.request.body, item.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Different return type\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n this.pendingWrites.push(JSON.stringify(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 const item = this.pendingWrites.shift();\n if (item) {\n await this.currentFile.writeLine(Buffer.from(item));\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 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 { OutgoingHttpHeader } from \"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"],"mappings":"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","names":["performance","consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","AsyncLock","Buffer","sentry","e","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","isEnabled","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","request","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","method","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","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"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport type { OutgoingHttpHeaders } from \"http\";\nimport { performance } from \"perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.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\n if (!client.isEnabled()) {\n await next();\n return;\n }\n\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 = (statusCode: number, headers: OutgoingHttpHeaders) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\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 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\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 );\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","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 \"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 \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"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 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];\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 RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: 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 maskQueryParams: [],\n maskHeaders: [],\n excludePaths: [],\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: string[] = [];\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 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 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 logRequest(request: Request, response: Response, error?: Error) {\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 // Process query params\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n request.url = url.toString();\n\n // Process request body\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n } else if (request.body) {\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n } else if (this.config.maskRequestBodyCallback) {\n try {\n request.body =\n this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n }\n } catch {\n request.body = undefined;\n }\n }\n }\n\n // Process response body\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n } else if (response.body) {\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n } else if (this.config.maskResponseBodyCallback) {\n try {\n response.body =\n this.config.maskResponseBodyCallback(request, response) ??\n BODY_MASKED;\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n }\n } catch {\n response.body = undefined;\n }\n }\n }\n\n // Process headers\n request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(request.headers)\n : [];\n response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(response.headers)\n : [];\n\n const item = {\n uuid: randomUUID(),\n request: skipEmptyValues(request),\n response: skipEmptyValues(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 : null,\n };\n [item.request.body, item.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Different return type\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n this.pendingWrites.push(JSON.stringify(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 const item = this.pendingWrites.shift();\n if (item) {\n await this.currentFile.writeLine(Buffer.from(item));\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 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"],"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","sentry","e","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","isEnabled","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","request","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","method","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","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
|