@andretimm/pharus 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +175 -0
  2. package/dist/core/collector.d.ts +18 -0
  3. package/dist/core/collector.js +61 -0
  4. package/dist/core/collector.js.map +1 -0
  5. package/dist/core/collector.spec.d.ts +1 -0
  6. package/dist/core/collector.spec.js +50 -0
  7. package/dist/core/collector.spec.js.map +1 -0
  8. package/dist/core/transport.d.ts +6 -0
  9. package/dist/core/transport.js +29 -0
  10. package/dist/core/transport.js.map +1 -0
  11. package/dist/core/types.d.ts +36 -0
  12. package/dist/core/types.js +3 -0
  13. package/dist/core/types.js.map +1 -0
  14. package/dist/core/utils.d.ts +7 -0
  15. package/dist/core/utils.js +97 -0
  16. package/dist/core/utils.js.map +1 -0
  17. package/dist/express/index.d.ts +1 -0
  18. package/dist/express/index.js +18 -0
  19. package/dist/express/index.js.map +1 -0
  20. package/dist/express/middleware.d.ts +3 -0
  21. package/dist/express/middleware.js +25 -0
  22. package/dist/express/middleware.js.map +1 -0
  23. package/dist/fastify/index.d.ts +3 -0
  24. package/dist/fastify/index.js +10 -0
  25. package/dist/fastify/index.js.map +1 -0
  26. package/dist/fastify/plugin.d.ts +4 -0
  27. package/dist/fastify/plugin.js +31 -0
  28. package/dist/fastify/plugin.js.map +1 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +19 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/nestjs/index.d.ts +2 -0
  33. package/dist/nestjs/index.js +19 -0
  34. package/dist/nestjs/index.js.map +1 -0
  35. package/dist/nestjs/interceptor.d.ts +11 -0
  36. package/dist/nestjs/interceptor.js +80 -0
  37. package/dist/nestjs/interceptor.js.map +1 -0
  38. package/dist/nestjs/module.d.ts +10 -0
  39. package/dist/nestjs/module.js +67 -0
  40. package/dist/nestjs/module.js.map +1 -0
  41. package/dist/tsconfig.tsbuildinfo +1 -0
  42. package/package.json +89 -0
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # @andretimm/pharus
2
+
3
+ A framework-agnostic metrics collection library for Node.js applications. Designed to easily integrate with **Express**, **Fastify**, and **NestJS**. It automatically collects request and response metrics (such as latency, HTTP status codes, and payload sizes) and sends them to a configured ingestion endpoint.
4
+
5
+ ## Key Features
6
+
7
+ - **Framework Agnostic:** Core logic separated from framework adapters (`express`, `fastify`, `nestjs`).
8
+ - **Automatic Collection:** Captures response duration, HTTP status code, request/response sizes, route, and HTTP method.
9
+ - **Optimized Batching:** Metrics are accumulated in memory and periodically sent in batches to reduce network overhead.
10
+ - **Highly Configurable:** Full control over batch size, auto-flush intervals, and captured data (body, query params, route params).
11
+ - **Native Security:** Built-in support for redacting sensitive data using regular expressions (e.g., passwords, tokens).
12
+
13
+ ---
14
+
15
+ ## 📦 Installation
16
+
17
+ You can install the package directly using npm or yarn:
18
+
19
+ ```bash
20
+ npm install @andretimm/pharus
21
+ # or
22
+ yarn add @andretimm/pharus
23
+ ```
24
+
25
+ ---
26
+
27
+ ## ⚙️ Configuration Options (`MetricsOptions`)
28
+
29
+ All integrations accept the same options interface:
30
+
31
+ | Property | Type | Required | Default | Description |
32
+ | -------------------- | --------- | -------- | ------- | --------------------------------------------------------------------------- |
33
+ | `projectId` | `string` | **Yes** | - | The ID of the project associated with the metrics being sent. |
34
+ | `ingestUrl` | `string` | **Yes** | - | The destination URL where metric batches will be ingested. |
35
+ | `batchSize` | `number` | No | `50` | Number of metrics to accumulate in memory before flushing the batch. |
36
+ | `flushIntervalMs` | `number` | No | `5000` | Maximum time (in ms) to wait before automatically flushing the batch. |
37
+ | `enabled` | `boolean` | No | `true` | Enables or disables metric collection globally. |
38
+ | `captureBody` | `boolean` | No | `false` | Whether to capture the request `body` payload. |
39
+ | `captureQuery` | `boolean` | No | `false` | Whether to capture URL `query string` parameters. |
40
+ | `captureParams` | `boolean` | No | `false` | Whether to capture dynamic route parameters (e.g., `/user/:id`). |
41
+ | `sensitiveKeysRegex` | `RegExp` | No | - | Regular expression to hide/redact (replace with `*****`) sensitive keys. |
42
+
43
+ ---
44
+
45
+ ## 🚀 Usage Guide by Framework
46
+
47
+ Below is how to configure and plug `@andretimm/pharus` into each of the main supported Node.js frameworks.
48
+
49
+ ### 1. Express.js
50
+
51
+ Import the `metricsMiddleware` from the specific Express entry point.
52
+
53
+ **Important Notice**: If you want to capture request bodies (`captureBody: true`), you MUST register your body parsing middleware (e.g., `express.json()`) **before** adding the metrics middleware.
54
+
55
+ ```typescript
56
+ import express from 'express';
57
+ import { metricsMiddleware } from '@andretimm/pharus/express';
58
+
59
+ const app = express();
60
+
61
+ // JSON Parsing (Required before metrics if using `captureBody: true`)
62
+ app.use(express.json());
63
+
64
+ // Registering the metrics middleware
65
+ app.use(metricsMiddleware({
66
+ projectId: 'my-super-project',
67
+ ingestUrl: 'https://my-metrics-api.com/ingest',
68
+ captureBody: true, // Optional: Captures request payloads
69
+ batchSize: 10, // Optional: Sends a batch every 10 processed requests
70
+ }));
71
+
72
+ app.get('/', (req, res) => {
73
+ res.send('Metrics are being captured silently!');
74
+ });
75
+
76
+ app.listen(3000, () => {
77
+ console.log('Server is running on port 3000');
78
+ });
79
+ ```
80
+
81
+ ### 2. Fastify
82
+
83
+ The package exposes a plugin fully compatible with Fastify's internal ecosystem, safely handling the request/response lifecycle (`onRequest` and `onResponse`).
84
+
85
+ ```typescript
86
+ import fastify from 'fastify';
87
+ import { metricsPlugin } from '@andretimm/pharus/fastify';
88
+
89
+ const app = fastify();
90
+
91
+ // Register the metrics plugin in Fastify
92
+ app.register(metricsPlugin, {
93
+ projectId: 'my-super-project',
94
+ ingestUrl: 'https://my-metrics-api.com/ingest',
95
+ flushIntervalMs: 2000, // Optional: Ensures metrics are sent at least every 2 seconds
96
+ captureQuery: true, // Optional: Saves all query strings
97
+ });
98
+
99
+ app.get('/', async () => {
100
+ return { hello: 'world' };
101
+ });
102
+
103
+ app.listen({ port: 3000 }, (err) => {
104
+ if (err) {
105
+ console.error(err);
106
+ process.exit(1);
107
+ }
108
+ console.log('Fastify is running on port 3000');
109
+ });
110
+ ```
111
+
112
+ ### 3. NestJS
113
+
114
+ The package provides a native NestJS `MetricsModule`. Simply import the root module via `forRoot()` in your application, and it will automatically inject the global execution time and payload size `Interceptors`.
115
+
116
+ ```typescript
117
+ import { Module } from '@nestjs/common';
118
+ import { MetricsModule } from '@andretimm/pharus/nestjs';
119
+ import { AppController } from './app.controller';
120
+ import { AppService } from './app.service';
121
+
122
+ @Module({
123
+ imports: [
124
+ // Register the module globally
125
+ MetricsModule.forRoot({
126
+ projectId: 'my-super-project',
127
+ ingestUrl: 'https://my-metrics-api.com/ingest',
128
+ sensitiveKeysRegex: /password|token|secret|credit_card/i, // Redacts critical body data
129
+ captureBody: true,
130
+ }),
131
+ ],
132
+ controllers: [AppController],
133
+ providers: [AppService],
134
+ })
135
+ export class AppModule {}
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 🛡 Advanced Features
141
+
142
+ ### Masking / Redacting Sensitive Data
143
+
144
+ One of the most useful features when logging full payloads is masking passwords or credit card data. You can control this using the `sensitiveKeysRegex` option.
145
+
146
+ ```typescript
147
+ metricsMiddleware({
148
+ // ... other initial options
149
+ captureBody: true,
150
+ sensitiveKeysRegex: /password|credit_card|cvc|secret_key/i
151
+ })
152
+ ```
153
+
154
+ Any object or key in the extracted payload/parameters that matches the regular expression `/password|credit_card/i` will have its value replaced in memory with `"*****"` before being cached or sent to the metrics API.
155
+
156
+ ### Manual / Forced Flush
157
+
158
+ In serverless environments (e.g., AWS Lambda) or during a **graceful shutdown** (cloud interruptions), Node might terminate your process before the `flushIntervalMs` triggers organically.
159
+
160
+ To ensure no in-memory data loss, you can leverage the core singleton:
161
+
162
+ ```typescript
163
+ import { MetricsCollector } from '@andretimm/pharus';
164
+
165
+ const collector = MetricsCollector.getInstance();
166
+ await collector.flush(); // Forces an immediate push of all pending metrics
167
+ await collector.shutdown(); // And then, safely terminates the global dispatch loop
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 📜 License
173
+
174
+ This software is licensed under the [ISC License](./LICENSE).
175
+
@@ -0,0 +1,18 @@
1
+ import { Metric, MetricsOptions } from './types';
2
+ export declare class MetricsCollector {
3
+ private static instance;
4
+ private buffer;
5
+ private flushInterval;
6
+ private readonly batchSize;
7
+ private readonly flushIntervalMs;
8
+ private readonly transport;
9
+ private readonly enabled;
10
+ private readonly projectId;
11
+ private constructor();
12
+ static initialize(options: MetricsOptions): MetricsCollector;
13
+ static getInstance(): MetricsCollector;
14
+ add(metric: Metric): void;
15
+ flush(): Promise<void>;
16
+ private startFlushInterval;
17
+ shutdown(): Promise<void>;
18
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetricsCollector = void 0;
4
+ const transport_1 = require("./transport");
5
+ class MetricsCollector {
6
+ constructor(options) {
7
+ this.buffer = [];
8
+ this.flushInterval = null;
9
+ this.batchSize = options.batchSize || 50;
10
+ this.flushIntervalMs = options.flushIntervalMs || 5000;
11
+ this.transport = new transport_1.HttpTransport(options.ingestUrl);
12
+ this.enabled = options.enabled !== false;
13
+ this.projectId = options.projectId;
14
+ if (this.enabled) {
15
+ this.startFlushInterval();
16
+ }
17
+ }
18
+ static initialize(options) {
19
+ if (!MetricsCollector.instance) {
20
+ MetricsCollector.instance = new MetricsCollector(options);
21
+ }
22
+ return MetricsCollector.instance;
23
+ }
24
+ static getInstance() {
25
+ if (!MetricsCollector.instance) {
26
+ throw new Error('MetricsCollector not initialized. Call MetricsCollector.initialize() first.');
27
+ }
28
+ return MetricsCollector.instance;
29
+ }
30
+ add(metric) {
31
+ if (!this.enabled)
32
+ return;
33
+ this.buffer.push(metric);
34
+ if (this.buffer.length >= this.batchSize) {
35
+ void this.flush();
36
+ }
37
+ }
38
+ async flush() {
39
+ if (this.buffer.length === 0)
40
+ return;
41
+ const metricsToSend = [...this.buffer];
42
+ this.buffer = [];
43
+ await this.transport.send(metricsToSend);
44
+ }
45
+ startFlushInterval() {
46
+ if (this.flushInterval) {
47
+ clearInterval(this.flushInterval);
48
+ }
49
+ this.flushInterval = setInterval(() => {
50
+ void this.flush();
51
+ }, this.flushIntervalMs);
52
+ }
53
+ async shutdown() {
54
+ if (this.flushInterval) {
55
+ clearInterval(this.flushInterval);
56
+ }
57
+ await this.flush();
58
+ }
59
+ }
60
+ exports.MetricsCollector = MetricsCollector;
61
+ //# sourceMappingURL=collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.js","sourceRoot":"","sources":["../../src/core/collector.ts"],"names":[],"mappings":";;;AACA,2CAA4C;AAE5C,MAAa,gBAAgB;IAU3B,YAAoB,OAAuB;QARnC,WAAM,GAAa,EAAE,CAAC;QACtB,kBAAa,GAA0B,IAAI,CAAC;QAQlD,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;QACzC,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,IAAI,IAAI,CAAC;QACvD,IAAI,CAAC,SAAS,GAAG,IAAI,yBAAa,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACtD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC;QACzC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QAEnC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IAEM,MAAM,CAAC,UAAU,CAAC,OAAuB;QAC9C,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC;YAC/B,gBAAgB,CAAC,QAAQ,GAAG,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC5D,CAAC;QACD,OAAO,gBAAgB,CAAC,QAAQ,CAAC;IACnC,CAAC;IAEM,MAAM,CAAC,WAAW;QACvB,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CACb,6EAA6E,CAC9E,CAAC;QACJ,CAAC;QACD,OAAO,gBAAgB,CAAC,QAAQ,CAAC;IACnC,CAAC;IAEM,GAAG,CAAC,MAAc;QACvB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAE1B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACzC,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,KAAK;QAChB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAErC,MAAM,aAAa,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC3C,CAAC;IAEO,kBAAkB;QACxB,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACpC,CAAC;QACD,IAAI,CAAC,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE;YACpC,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;IAC3B,CAAC;IAEM,KAAK,CAAC,QAAQ;QACnB,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF;AAvED,4CAuEC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const collector_1 = require("./collector");
4
+ const transport_1 = require("./transport");
5
+ jest.mock('./transport');
6
+ describe('MetricsCollector', () => {
7
+ beforeEach(() => {
8
+ collector_1.MetricsCollector.instance = undefined;
9
+ jest.clearAllMocks();
10
+ });
11
+ it('should throw if getInstance is called before initialize', () => {
12
+ expect(() => collector_1.MetricsCollector.getInstance()).toThrow('MetricsCollector not initialized');
13
+ });
14
+ it('should initialize correctly as a singleton', () => {
15
+ const options = {
16
+ apiKey: 'test-api-key',
17
+ ingestUrl: 'http://test.url/ingest',
18
+ projectId: 'test-project',
19
+ enabled: false,
20
+ };
21
+ const collector1 = collector_1.MetricsCollector.initialize(options);
22
+ const collector2 = collector_1.MetricsCollector.getInstance();
23
+ expect(collector1).toBeDefined();
24
+ expect(collector1).toBe(collector2);
25
+ });
26
+ it('should add metrics to buffer and not flush if below batch size', () => {
27
+ const options = {
28
+ apiKey: 'test-api-key',
29
+ ingestUrl: 'http://test.url/ingest',
30
+ projectId: 'test-project',
31
+ batchSize: 5,
32
+ enabled: true,
33
+ };
34
+ const collector = collector_1.MetricsCollector.initialize(options);
35
+ collector.add({
36
+ projectId: 'test-project',
37
+ duration: 100,
38
+ requestSize: 100,
39
+ responseSize: 100,
40
+ route: '/test',
41
+ method: 'GET',
42
+ statusCode: 200,
43
+ timestamp: new Date().toISOString(),
44
+ });
45
+ const mockTransportInstance = transport_1.HttpTransport.mock.instances[0];
46
+ expect(mockTransportInstance.send).not.toHaveBeenCalled();
47
+ void collector.shutdown();
48
+ });
49
+ });
50
+ //# sourceMappingURL=collector.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.spec.js","sourceRoot":"","sources":["../../src/core/collector.spec.ts"],"names":[],"mappings":";;AAAA,2CAA+C;AAC/C,2CAA4C;AAE5C,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AAEzB,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,UAAU,CAAC,GAAG,EAAE;QAEb,4BAAwB,CAAC,QAAQ,GAAG,SAAS,CAAC;QAC/C,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,GAAG,EAAE,CAAC,4BAAgB,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,kCAAkC,CAAC,CAAC;IAC3F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,OAAO,GAAG;YACd,MAAM,EAAE,cAAc;YACtB,SAAS,EAAE,wBAAwB;YACnC,SAAS,EAAE,cAAc;YACzB,OAAO,EAAE,KAAK;SACf,CAAC;QAEF,MAAM,UAAU,GAAG,4BAAgB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,4BAAgB,CAAC,WAAW,EAAE,CAAC;QAElD,MAAM,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;QACjC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,OAAO,GAAG;YACd,MAAM,EAAE,cAAc;YACtB,SAAS,EAAE,wBAAwB;YACnC,SAAS,EAAE,cAAc;YACzB,SAAS,EAAE,CAAC;YACZ,OAAO,EAAE,IAAI;SACd,CAAC;QAEF,MAAM,SAAS,GAAG,4BAAgB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAGvD,SAAS,CAAC,GAAG,CAAC;YACZ,SAAS,EAAE,cAAc;YACzB,QAAQ,EAAE,GAAG;YACb,WAAW,EAAE,GAAG;YAChB,YAAY,EAAE,GAAG;YACjB,KAAK,EAAE,OAAO;YACd,MAAM,EAAE,KAAK;YACb,UAAU,EAAE,GAAG;YACf,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC,CAAC;QAGH,MAAM,qBAAqB,GAAI,yBAA2B,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC7E,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QAG1D,KAAK,SAAS,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { Metric, Transport } from './types';
2
+ export declare class HttpTransport implements Transport {
3
+ private readonly ingestUrl;
4
+ constructor(ingestUrl: string);
5
+ send(metrics: Metric[]): Promise<void>;
6
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpTransport = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ class HttpTransport {
9
+ constructor(ingestUrl) {
10
+ this.ingestUrl = ingestUrl;
11
+ }
12
+ async send(metrics) {
13
+ try {
14
+ await axios_1.default.post(this.ingestUrl, { metrics }, {
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ },
18
+ timeout: 5000,
19
+ });
20
+ console.log('[Metrics] Metrics sent successfully');
21
+ }
22
+ catch (error) {
23
+ console.log(error);
24
+ console.error('[Metrics] Failed to send metrics:', axios_1.default.isAxiosError(error) ? error.message : error);
25
+ }
26
+ }
27
+ }
28
+ exports.HttpTransport = HttpTransport;
29
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/core/transport.ts"],"names":[],"mappings":";;;;;;AAAA,kDAA0B;AAG1B,MAAa,aAAa;IACxB,YAA6B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;IAElD,KAAK,CAAC,IAAI,CAAC,OAAiB;QAC1B,IAAI,CAAC;YACH,MAAM,eAAK,CAAC,IAAI,CACd,IAAI,CAAC,SAAS,EACd,EAAE,OAAO,EAAE,EACX;gBACE,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;iBACnC;gBACD,OAAO,EAAE,IAAI;aACd,CACF,CAAC;YACF,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;QACrD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAEnB,OAAO,CAAC,KAAK,CACX,mCAAmC,EACnC,eAAK,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAClD,CAAC;QACJ,CAAC;IACH,CAAC;CACF;AAzBD,sCAyBC"}
@@ -0,0 +1,36 @@
1
+ export interface Metric {
2
+ route?: string;
3
+ method: string;
4
+ statusCode: number;
5
+ duration: number;
6
+ requestSize?: number;
7
+ responseSize?: number;
8
+ timestamp: string;
9
+ userAgent?: string;
10
+ ip?: string;
11
+ error?: string;
12
+ metadata?: Record<string, any>;
13
+ body?: any;
14
+ query?: any;
15
+ params?: any;
16
+ projectId: string;
17
+ }
18
+ export interface MetricsOptions {
19
+ ingestUrl: string;
20
+ projectId: string;
21
+ batchSize?: number;
22
+ flushIntervalMs?: number;
23
+ enabled?: boolean;
24
+ captureBody?: boolean;
25
+ captureQuery?: boolean;
26
+ captureParams?: boolean;
27
+ sensitiveKeysRegex?: RegExp;
28
+ }
29
+ export interface Route {
30
+ path: string;
31
+ method: string;
32
+ metadata?: Record<string, any>;
33
+ }
34
+ export interface Transport {
35
+ send(metrics: Metric[]): Promise<void>;
36
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ import { Metric, MetricsOptions } from './types';
2
+ export declare class MetricsExtractor {
3
+ private static readonly DEFAULT_SENSITIVE_REGEX;
4
+ static extract(options: MetricsOptions, req: any, res: any, duration: number, requestId?: string): Metric;
5
+ static calculateResponseSize(data: any): number;
6
+ private static redact;
7
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetricsExtractor = void 0;
4
+ class MetricsExtractor {
5
+ static extract(options, req, res, duration, requestId) {
6
+ var _a;
7
+ const method = req.method;
8
+ const url = req.url || req.originalUrl || req.path;
9
+ const ip = req.ip ||
10
+ ((_a = req.socket) === null || _a === void 0 ? void 0 : _a.remoteAddress) ||
11
+ (req.headers ? req.headers['x-forwarded-for'] : undefined);
12
+ const userAgent = req.headers ? req.headers['user-agent'] : undefined;
13
+ let statusCode = 200;
14
+ if (res.statusCode) {
15
+ statusCode = res.statusCode;
16
+ }
17
+ else if (res.raw && res.raw.statusCode) {
18
+ statusCode = res.raw.statusCode;
19
+ }
20
+ let requestSize = 0;
21
+ if (req.headers && req.headers['content-length']) {
22
+ requestSize = parseInt(req.headers['content-length'], 10);
23
+ }
24
+ let responseSize = 0;
25
+ const contentLength = res.getHeader
26
+ ? res.getHeader('content-length')
27
+ : res.headers
28
+ ? res.headers['content-length']
29
+ : null;
30
+ if (contentLength) {
31
+ responseSize = parseInt(contentLength, 10);
32
+ }
33
+ let route = undefined;
34
+ if (req.route && req.route.path) {
35
+ route = req.route.path;
36
+ }
37
+ else if (req.routeOptions && req.routeOptions.url) {
38
+ route = req.routeOptions.url;
39
+ }
40
+ else if (req.routerPath) {
41
+ route = req.routerPath;
42
+ }
43
+ const metric = {
44
+ route,
45
+ method,
46
+ statusCode,
47
+ duration,
48
+ requestSize,
49
+ responseSize,
50
+ timestamp: new Date().toISOString(),
51
+ userAgent,
52
+ ip,
53
+ projectId: options.projectId,
54
+ };
55
+ return metric;
56
+ }
57
+ static calculateResponseSize(data) {
58
+ if (!data)
59
+ return 0;
60
+ if (typeof data === 'string') {
61
+ return Buffer.byteLength(data);
62
+ }
63
+ if (Buffer.isBuffer(data)) {
64
+ return data.length;
65
+ }
66
+ try {
67
+ return Buffer.byteLength(JSON.stringify(data));
68
+ }
69
+ catch (e) {
70
+ return 0;
71
+ }
72
+ }
73
+ static redact(data, regex) {
74
+ if (!data)
75
+ return data;
76
+ if (typeof data !== 'object')
77
+ return data;
78
+ if (Array.isArray(data)) {
79
+ return data.map((item) => MetricsExtractor.redact(item, regex));
80
+ }
81
+ const redacted = {};
82
+ for (const key in data) {
83
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
84
+ if (regex.test(key)) {
85
+ redacted[key] = '*****';
86
+ }
87
+ else {
88
+ redacted[key] = MetricsExtractor.redact(data[key], regex);
89
+ }
90
+ }
91
+ }
92
+ return redacted;
93
+ }
94
+ }
95
+ exports.MetricsExtractor = MetricsExtractor;
96
+ MetricsExtractor.DEFAULT_SENSITIVE_REGEX = /pass(word|wd)?|secret|token|auth|api[._-]?key|credit[._-]?card|cvv|ccv|ssn/i;
97
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export * from './middleware';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./middleware"), exports);
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/express/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+CAA6B"}
@@ -0,0 +1,3 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { MetricsOptions } from '../core/types';
3
+ export declare const metricsMiddleware: (options: MetricsOptions) => (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.metricsMiddleware = void 0;
4
+ const collector_1 = require("../core/collector");
5
+ const utils_1 = require("../core/utils");
6
+ const uuid_1 = require("uuid");
7
+ const metricsMiddleware = (options) => {
8
+ const collector = collector_1.MetricsCollector.initialize(options);
9
+ return (req, res, next) => {
10
+ if (options.enabled === false) {
11
+ return next();
12
+ }
13
+ const start = Date.now();
14
+ const requestId = (0, uuid_1.v4)();
15
+ req.requestId = requestId;
16
+ res.on('finish', () => {
17
+ const duration = Date.now() - start;
18
+ const metric = utils_1.MetricsExtractor.extract(options, req, res, duration, requestId);
19
+ collector.add(metric);
20
+ });
21
+ next();
22
+ };
23
+ };
24
+ exports.metricsMiddleware = metricsMiddleware;
25
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/express/middleware.ts"],"names":[],"mappings":";;;AACA,iDAAqD;AAErD,yCAAiD;AACjD,+BAAoC;AAE7B,MAAM,iBAAiB,GAAG,CAAC,OAAuB,EAAE,EAAE;IAC3D,MAAM,SAAS,GAAG,4BAAgB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAEvD,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACzD,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YAC9B,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,IAAA,SAAM,GAAE,CAAC;QAC1B,GAAW,CAAC,SAAS,GAAG,SAAS,CAAC;QAEnC,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;YACpB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;YACpC,MAAM,MAAM,GAAG,wBAAgB,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;YAChF,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACxB,CAAC,CAAC,CAAC;QAEH,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC,CAAC;AApBW,QAAA,iBAAiB,qBAoB5B"}
@@ -0,0 +1,3 @@
1
+ import metricsPlugin from './plugin';
2
+ export { metricsPlugin };
3
+ export default metricsPlugin;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.metricsPlugin = void 0;
7
+ const plugin_1 = __importDefault(require("./plugin"));
8
+ exports.metricsPlugin = plugin_1.default;
9
+ exports.default = plugin_1.default;
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/fastify/index.ts"],"names":[],"mappings":";;;;;;AAAA,sDAAqC;AAC5B,wBADF,gBAAa,CACE;AACtB,kBAAe,gBAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import { MetricsOptions } from '../core/types';
3
+ declare const _default: FastifyPluginAsync<MetricsOptions>;
4
+ export default _default;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
7
+ const uuid_1 = require("uuid");
8
+ const collector_1 = require("../core/collector");
9
+ const utils_1 = require("../core/utils");
10
+ const metricsPlugin = async (fastify, options) => {
11
+ const collector = collector_1.MetricsCollector.initialize(options);
12
+ if (options.enabled === false) {
13
+ return;
14
+ }
15
+ fastify.addHook('onRequest', async (req, reply) => {
16
+ req.metricsStartTime = Date.now();
17
+ if (!req.id) {
18
+ req.id = (0, uuid_1.v4)();
19
+ }
20
+ });
21
+ fastify.addHook('onResponse', async (req, reply) => {
22
+ const start = req.metricsStartTime || Date.now();
23
+ const duration = Date.now() - start;
24
+ const metric = utils_1.MetricsExtractor.extract(options, req, reply, duration, req.id);
25
+ collector.add(metric);
26
+ });
27
+ };
28
+ exports.default = (0, fastify_plugin_1.default)(metricsPlugin, {
29
+ name: 'metrics-plugin',
30
+ });
31
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/fastify/plugin.ts"],"names":[],"mappings":";;;;;AACA,oEAAgC;AAChC,+BAAoC;AACpC,iDAAqD;AAErD,yCAAiD;AAEjD,MAAM,aAAa,GAAuC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;IACnF,MAAM,SAAS,GAAG,4BAAgB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAEvD,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC9B,OAAO;IACT,CAAC;IAID,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;QAC/C,GAAW,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3C,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,GAAG,CAAC,EAAE,GAAG,IAAA,SAAM,GAAE,CAAC;QACpB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;QACjD,MAAM,KAAK,GAAI,GAAW,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;QAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QAMpC,MAAM,MAAM,GAAG,wBAAgB,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAY,CAAC,CAAC;QACzF,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,kBAAe,IAAA,wBAAE,EAAC,aAAa,EAAE;IAC/B,IAAI,EAAE,gBAAgB;CACvB,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './core/types';
2
+ export * from './core/collector';