@phil-r/stats 1.4.3 → 2.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.
- package/README.md +79 -146
- package/dist/chunk-etf182vd.js +13 -0
- package/dist/chunk-nwt5rk8q.js +25 -0
- package/dist/elysia.d.ts +10 -0
- package/dist/elysia.js +29 -0
- package/dist/express.d.ts +13 -0
- package/dist/express.js +6 -0
- package/dist/fastify.d.ts +10 -0
- package/dist/fastify.js +29 -0
- package/dist/hono.d.ts +10 -0
- package/dist/hono.js +25 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +92 -0
- package/dist/internal.d.ts +10 -0
- package/dist/types.d.ts +66 -0
- package/docs/guide.md +82 -0
- package/package.json +88 -26
- package/.eslintrc +0 -15
- package/index.js +0 -122
- package/utils.js +0 -5
package/README.md
CHANGED
|
@@ -1,179 +1,112 @@
|
|
|
1
1
|
# stats
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@phil-r/stats)
|
|
6
|
-
[](https://travis-ci.org/phil-r/stats)
|
|
3
|
+
[](https://www.npmjs.com/package/@phil-r/stats)
|
|
4
|
+
[](https://github.com/phil-r/stats/actions)
|
|
7
5
|
[](https://codecov.io/gh/phil-r/stats)
|
|
8
|
-
[](https://github.com/prettier/prettier)
|
|
9
|
-
|
|
10
|
-
## Installation
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
Small in-memory request statistics collector for Elysia, Express/Node HTTP,
|
|
8
|
+
Fastify, Hono and NestJS (Express and Fastify platforms). TypeScript-first,
|
|
9
|
+
ESM-only, for Node **22+** and Bun. Other frameworks may be integrated through
|
|
10
|
+
an appropriate adapter.
|
|
15
11
|
|
|
16
|
-
##
|
|
12
|
+
## Installation
|
|
17
13
|
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
endpointStats: true,
|
|
22
|
-
complexEndpoints: ['/user/:id'],
|
|
23
|
-
customStats: true,
|
|
24
|
-
addHeader: true
|
|
25
|
-
});
|
|
14
|
+
```sh
|
|
15
|
+
npm install @phil-r/stats
|
|
16
|
+
# or: bun add @phil-r/stats
|
|
26
17
|
```
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
that returns current stats
|
|
32
|
-
|
|
33
|
-
#### Options
|
|
34
|
-
|
|
35
|
-
`initStats` accepts optional `options` object that may contain any of the following keys:
|
|
36
|
-
|
|
37
|
-
##### endpointStats
|
|
38
|
-
|
|
39
|
-
Defaults to `false`
|
|
40
|
-
|
|
41
|
-
Boolean that indicates whether to track per endpoint stats.
|
|
19
|
+
Install your framework separately. Framework peers are optional, and the root
|
|
20
|
+
export loads no framework. No stats endpoint is exposed automatically; protect
|
|
21
|
+
any endpoint you add.
|
|
42
22
|
|
|
43
|
-
|
|
23
|
+
## Usage
|
|
44
24
|
|
|
45
|
-
|
|
25
|
+
Create a collector and install the adapter **before routes or middleware that
|
|
26
|
+
may respond early**. Share a collector to aggregate counts across adapters;
|
|
27
|
+
install each adapter once per request path (Elysia deduplicates by collector).
|
|
46
28
|
|
|
47
|
-
|
|
29
|
+
### Elysia
|
|
48
30
|
|
|
49
|
-
|
|
31
|
+
```ts
|
|
32
|
+
import { Elysia } from 'elysia';
|
|
33
|
+
import { createStats } from '@phil-r/stats';
|
|
34
|
+
import { elysiaStats } from '@phil-r/stats/elysia';
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
const stats = createStats({ endpointStats: true, addHeader: true });
|
|
37
|
+
new Elysia()
|
|
38
|
+
.use(elysiaStats(stats))
|
|
39
|
+
.get('/', () => 'Hello')
|
|
40
|
+
.get('/stats', () => stats.getStats())
|
|
41
|
+
.listen(8080);
|
|
42
|
+
```
|
|
56
43
|
|
|
57
|
-
|
|
58
|
-
and allows measuring any parts of the app.
|
|
44
|
+
### Express / Node HTTP
|
|
59
45
|
|
|
60
|
-
|
|
46
|
+
```ts
|
|
47
|
+
import express from 'express';
|
|
48
|
+
import { createStats } from '@phil-r/stats';
|
|
49
|
+
import { expressStats } from '@phil-r/stats/express';
|
|
61
50
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
51
|
+
const stats = createStats();
|
|
52
|
+
const app = express();
|
|
53
|
+
app.use(expressStats(stats));
|
|
54
|
+
app.get('/', (_req, res) => res.send('Hello'));
|
|
55
|
+
app.listen(8080);
|
|
68
56
|
```
|
|
69
57
|
|
|
70
|
-
|
|
58
|
+
The middleware also accepts Node `IncomingMessage`/`ServerResponse` and `next`,
|
|
59
|
+
without importing Express at runtime.
|
|
71
60
|
|
|
72
|
-
|
|
61
|
+
### Fastify and Hono
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
[`expressjs/response-time`](https://github.com/expressjs/response-time)
|
|
63
|
+
With a collector named `stats`, register the appropriate adapter:
|
|
76
64
|
|
|
77
|
-
|
|
65
|
+
```ts
|
|
66
|
+
import { fastifyStats } from '@phil-r/stats/fastify';
|
|
67
|
+
// Fastify: native HTTP/1 hook, not a plugin for register().
|
|
68
|
+
app.addHook('onRequest', fastifyStats(stats));
|
|
69
|
+
```
|
|
78
70
|
|
|
79
|
-
|
|
71
|
+
```ts
|
|
72
|
+
import { honoStats } from '@phil-r/stats/hono';
|
|
73
|
+
// Hono on Node/Bun, not edge runtimes: install first.
|
|
74
|
+
app.use('*', honoStats(stats));
|
|
75
|
+
```
|
|
80
76
|
|
|
81
|
-
|
|
77
|
+
For NestJS, use its platform's adapter after `create`, before `init`/`listen`.
|
|
78
|
+
See [Nest examples and integration details](./docs/guide.md) or
|
|
79
|
+
[runnable apps](./examples).
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
const app = require('express')();
|
|
85
|
-
const initStats = require('@phil-r/stats');
|
|
81
|
+
## Collector API
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
complexEndpoints: ['/user/:id'],
|
|
90
|
-
customStats: true,
|
|
91
|
-
addHeader: true
|
|
92
|
-
});
|
|
83
|
+
`createStats(options?)` returns an independent collector. All options default to
|
|
84
|
+
false (or `[]`):
|
|
93
85
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
await new Promise(resolve => {
|
|
100
|
-
setTimeout(() => resolve(), 2000);
|
|
101
|
-
});
|
|
102
|
-
req.finishMeasurement(measurement);
|
|
103
|
-
res.end(`Long job finished`);
|
|
104
|
-
});
|
|
105
|
-
app.get('/stats', (req, res) => res.send(getStats()));
|
|
86
|
+
- `endpointStats`: per-method/path totals, excluding query strings.
|
|
87
|
+
- `complexEndpoints`: ordered regexparam patterns, e.g. `['/user/:id']`; first
|
|
88
|
+
match groups dynamic URLs. Use with `endpointStats` to limit unique keys.
|
|
89
|
+
- `customStats`: include named custom measurements in output.
|
|
90
|
+
- `addHeader`: add `X-Response-Time: Nms`, preserving existing values.
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
92
|
+
`stats.getStats()` returns request count, total/average milliseconds, status
|
|
93
|
+
counts, process identity/uptime and enabled endpoint/custom totals. Nested maps
|
|
94
|
+
are live; treat them as read-only. Counters are process-local and unbounded.
|
|
110
95
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"200": 5,
|
|
119
|
-
"404": 4
|
|
120
|
-
},
|
|
121
|
-
"uuid": "330d9cc6-7d40-4964-888c-4d2817905ee1",
|
|
122
|
-
"pid": 90603,
|
|
123
|
-
"totalTime": 4020.3912830000004,
|
|
124
|
-
"averageTime": 446.7101425555556,
|
|
125
|
-
"count": 9,
|
|
126
|
-
"endpointStats": {
|
|
127
|
-
"GET /long": {
|
|
128
|
-
"totalTime": 4009.410922,
|
|
129
|
-
"averageTime": 2004.705461,
|
|
130
|
-
"count": 2,
|
|
131
|
-
"statusCodes": {
|
|
132
|
-
"200": 2
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
"GET /favicon.ico": {
|
|
136
|
-
"totalTime": 4.286955,
|
|
137
|
-
"averageTime": 1.07173875,
|
|
138
|
-
"count": 4,
|
|
139
|
-
"statusCodes": {
|
|
140
|
-
"404": 4
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
"GET /stats": {
|
|
144
|
-
"totalTime": 6.227342999999999,
|
|
145
|
-
"averageTime": 6.227342999999999,
|
|
146
|
-
"count": 1,
|
|
147
|
-
"statusCodes": {
|
|
148
|
-
"200": 1
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
"GET /user/:id": {
|
|
152
|
-
"totalTime": 0.466063,
|
|
153
|
-
"averageTime": 0.2330315,
|
|
154
|
-
"count": 2,
|
|
155
|
-
"statusCodes": {
|
|
156
|
-
"200": 2
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
"customStats": {
|
|
161
|
-
"long": {
|
|
162
|
-
"totalTime": 4005.556455,
|
|
163
|
-
"averageTime": 2002.7782275,
|
|
164
|
-
"started": 2,
|
|
165
|
-
"count": 2
|
|
166
|
-
}
|
|
167
|
-
}
|
|
96
|
+
```ts
|
|
97
|
+
const stats = createStats({ customStats: true });
|
|
98
|
+
const measurement = stats.startMeasurement('work');
|
|
99
|
+
try {
|
|
100
|
+
await doWork();
|
|
101
|
+
} finally {
|
|
102
|
+
stats.finishMeasurement(measurement);
|
|
168
103
|
}
|
|
169
104
|
```
|
|
170
105
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
This is a fork of [zenmate/stats](https://github.com/zenmate/stats)
|
|
106
|
+
Finish each token once on its originating collector. Timings are monotonic
|
|
107
|
+
milliseconds, not body-transmission times. See the [guide](./docs/guide.md) for
|
|
108
|
+
precise adapter timing, supported versions, migration and development commands.
|
|
176
109
|
|
|
177
|
-
|
|
110
|
+
## License
|
|
178
111
|
|
|
179
|
-
[
|
|
112
|
+
[MIT](LICENSE). Fork of zenmate/stats.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/internal.ts
|
|
2
|
+
var collectors = new WeakMap;
|
|
3
|
+
function getCollector(stats) {
|
|
4
|
+
const state = collectors.get(stats);
|
|
5
|
+
if (!state)
|
|
6
|
+
throw new TypeError("Expected a collector from createStats()");
|
|
7
|
+
return state;
|
|
8
|
+
}
|
|
9
|
+
function hrTimeToMs([s, ns]) {
|
|
10
|
+
return s * 1000 + ns * 0.000001;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { collectors, getCollector, hrTimeToMs };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCollector,
|
|
3
|
+
hrTimeToMs
|
|
4
|
+
} from "./chunk-etf182vd.js";
|
|
5
|
+
|
|
6
|
+
// src/express.ts
|
|
7
|
+
import onHeaders from "on-headers";
|
|
8
|
+
function expressStats2(stats) {
|
|
9
|
+
const { record, addHeader } = getCollector(stats);
|
|
10
|
+
return function statsMiddleware(req, res, next) {
|
|
11
|
+
const start = process.hrtime();
|
|
12
|
+
const method = req.method || "GET";
|
|
13
|
+
const url = req.originalUrl || req.url || "/";
|
|
14
|
+
onHeaders(res, () => {
|
|
15
|
+
const elapsed = hrTimeToMs(process.hrtime(start));
|
|
16
|
+
if (addHeader && !res.hasHeader("X-Response-Time")) {
|
|
17
|
+
res.setHeader("X-Response-Time", `${elapsed.toFixed(0)}ms`);
|
|
18
|
+
}
|
|
19
|
+
record(method, url, res.statusCode, elapsed);
|
|
20
|
+
});
|
|
21
|
+
next();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { expressStats2 };
|
package/dist/elysia.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Elysia } from 'elysia';
|
|
2
|
+
import type { StatsCollector } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create a plugin measuring until Elysia produces its final Response, not body transmission.
|
|
5
|
+
* Install with `.use(elysiaStats(stats))` before routes. The collector UUID seeds
|
|
6
|
+
* plugin deduplication: reusing one collector counts each request only once.
|
|
7
|
+
* Existing response-time headers are preserved. Requires Elysia 1.4.x on Bun.
|
|
8
|
+
* @param stats - Collector returned by createStats().
|
|
9
|
+
*/
|
|
10
|
+
export declare function elysiaStats(stats: StatsCollector): Elysia;
|
package/dist/elysia.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCollector,
|
|
3
|
+
hrTimeToMs
|
|
4
|
+
} from "./chunk-etf182vd.js";
|
|
5
|
+
|
|
6
|
+
// src/elysia.ts
|
|
7
|
+
import { Elysia } from "elysia";
|
|
8
|
+
function elysiaStats(stats) {
|
|
9
|
+
const { record, addHeader } = getCollector(stats);
|
|
10
|
+
const wrap = (handle) => async (request, ...rest) => {
|
|
11
|
+
const start = process.hrtime();
|
|
12
|
+
let response = await handle(request, ...rest);
|
|
13
|
+
const elapsed = hrTimeToMs(process.hrtime(start));
|
|
14
|
+
if (addHeader && !response.headers.has("X-Response-Time")) {
|
|
15
|
+
try {
|
|
16
|
+
response.headers.set("X-Response-Time", `${elapsed.toFixed(0)}ms`);
|
|
17
|
+
} catch {
|
|
18
|
+
response = new Response(response.body, response);
|
|
19
|
+
response.headers.set("X-Response-Time", `${elapsed.toFixed(0)}ms`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
record(request.method, request.url, response.status, elapsed);
|
|
23
|
+
return response;
|
|
24
|
+
};
|
|
25
|
+
return new Elysia({ name: "stats", seed: stats.getStats().uuid }).wrap(wrap);
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
elysiaStats
|
|
29
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { StatsCollector } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create Express/Node HTTP middleware that records when response headers are written.
|
|
5
|
+
* Install before routes and any middleware that may respond early; for Nest's
|
|
6
|
+
* Express platform, install after create and before init/listen. Uses originalUrl
|
|
7
|
+
* for mounted routers and preserves existing response-time headers. Aborted
|
|
8
|
+
* requests without headers are not counted; body transmission is not measured.
|
|
9
|
+
* @param stats - Collector returned by createStats(); install once per request path.
|
|
10
|
+
*/
|
|
11
|
+
export declare function expressStats(stats: StatsCollector): (req: IncomingMessage & {
|
|
12
|
+
originalUrl?: string;
|
|
13
|
+
}, res: ServerResponse, next: () => void) => void;
|
package/dist/express.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { onRequestHookHandler } from 'fastify';
|
|
2
|
+
import type { StatsCollector } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create an HTTP/1 onRequest hook (not a plugin for register()).
|
|
5
|
+
* Add before routes and early hooks, once in the desired encapsulation scope.
|
|
6
|
+
* For Nest/Fastify, add to the native instance after create, before init/listen.
|
|
7
|
+
* Records at header write, not body completion; preserves staged/raw timing headers.
|
|
8
|
+
* @param stats - Collector returned by createStats().
|
|
9
|
+
*/
|
|
10
|
+
export declare function fastifyStats(stats: StatsCollector): onRequestHookHandler;
|
package/dist/fastify.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expressStats2
|
|
3
|
+
} from "./chunk-nwt5rk8q.js";
|
|
4
|
+
import {
|
|
5
|
+
getCollector
|
|
6
|
+
} from "./chunk-etf182vd.js";
|
|
7
|
+
|
|
8
|
+
// src/fastify.ts
|
|
9
|
+
import onHeaders from "on-headers";
|
|
10
|
+
function fastifyStats(stats) {
|
|
11
|
+
const middleware = expressStats2(stats);
|
|
12
|
+
const { addHeader } = getCollector(stats);
|
|
13
|
+
return function(request, reply, done) {
|
|
14
|
+
middleware(request.raw, reply.raw, () => {
|
|
15
|
+
if (addHeader) {
|
|
16
|
+
onHeaders(reply.raw, () => {
|
|
17
|
+
const existing = reply.getHeader("X-Response-Time");
|
|
18
|
+
if (!reply.raw.hasHeader("X-Response-Time") && existing !== undefined) {
|
|
19
|
+
reply.raw.setHeader("X-Response-Time", existing);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
done();
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
fastifyStats
|
|
29
|
+
};
|
package/dist/hono.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { StatsCollector } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create Node/Bun middleware; install first with app.use('*', honoStats(stats)).
|
|
5
|
+
* Records after downstream middleware and Hono's error handler finalize a Response,
|
|
6
|
+
* not after streaming its body. Escaping errors without a Response are not counted.
|
|
7
|
+
* Preserves existing response-time headers. Install once per request path.
|
|
8
|
+
* @param stats - Collector returned by createStats().
|
|
9
|
+
*/
|
|
10
|
+
export declare function honoStats(stats: StatsCollector): MiddlewareHandler;
|
package/dist/hono.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCollector,
|
|
3
|
+
hrTimeToMs
|
|
4
|
+
} from "./chunk-etf182vd.js";
|
|
5
|
+
|
|
6
|
+
// src/hono.ts
|
|
7
|
+
function honoStats(stats) {
|
|
8
|
+
const { record, addHeader } = getCollector(stats);
|
|
9
|
+
return async (context, next) => {
|
|
10
|
+
const start = process.hrtime();
|
|
11
|
+
const method = context.req.method;
|
|
12
|
+
const url = context.req.url;
|
|
13
|
+
await next();
|
|
14
|
+
const elapsed = hrTimeToMs(process.hrtime(start));
|
|
15
|
+
if (addHeader && !context.res.headers.has("X-Response-Time")) {
|
|
16
|
+
const response = new Response(context.res.body, context.res);
|
|
17
|
+
response.headers.set("X-Response-Time", `${elapsed.toFixed(0)}ms`);
|
|
18
|
+
context.res = response;
|
|
19
|
+
}
|
|
20
|
+
record(method, url, context.res.status, elapsed);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
honoStats
|
|
25
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StatsOptions, StatsCollector } from './types.js';
|
|
2
|
+
export type { StatsOptions, StatsCollector, Stats, Timing, EndpointStats, CustomStats, Measurement, } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create an independent in-memory collector for one or more framework adapters.
|
|
5
|
+
* Counters are unbounded and process-local; configure endpoint patterns to group
|
|
6
|
+
* dynamic URLs. No endpoint is exposed automatically.
|
|
7
|
+
* @param opts - Collection and response-header options, disabled by default.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createStats(opts?: StatsOptions): StatsCollector;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectors,
|
|
3
|
+
hrTimeToMs
|
|
4
|
+
} from "./chunk-etf182vd.js";
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
import { randomUUID as uuidv4 } from "node:crypto";
|
|
8
|
+
import { parse } from "regexparam";
|
|
9
|
+
import prettyTime from "pretty-time";
|
|
10
|
+
function createStats(opts = {}) {
|
|
11
|
+
const active = new WeakSet;
|
|
12
|
+
const stats = {
|
|
13
|
+
uuid: uuidv4(),
|
|
14
|
+
pid: process.pid,
|
|
15
|
+
totalTime: 0,
|
|
16
|
+
averageTime: 0,
|
|
17
|
+
count: 0
|
|
18
|
+
};
|
|
19
|
+
const statusCodes = {};
|
|
20
|
+
const endpointStats = {};
|
|
21
|
+
const customStats = {};
|
|
22
|
+
const complexEndpoints = (opts.complexEndpoints || []).map((path) => ({
|
|
23
|
+
...parse(path),
|
|
24
|
+
path
|
|
25
|
+
}));
|
|
26
|
+
function getStats() {
|
|
27
|
+
const result = {
|
|
28
|
+
uptime: process.uptime() * 1000,
|
|
29
|
+
uptimeHumanReadable: prettyTime(Math.floor(process.uptime() * 1e9)),
|
|
30
|
+
statusCodes,
|
|
31
|
+
...stats
|
|
32
|
+
};
|
|
33
|
+
if (opts.endpointStats)
|
|
34
|
+
result.endpointStats = endpointStats;
|
|
35
|
+
if (opts.customStats)
|
|
36
|
+
result.customStats = customStats;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function startMeasurement(name) {
|
|
40
|
+
if (!Object.prototype.hasOwnProperty.call(customStats, name)) {
|
|
41
|
+
Object.defineProperty(customStats, name, {
|
|
42
|
+
value: { totalTime: 0, averageTime: 0, started: 0, count: 0 },
|
|
43
|
+
enumerable: true
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
customStats[name].started++;
|
|
47
|
+
const measurement = { start: process.hrtime(), name };
|
|
48
|
+
active.add(measurement);
|
|
49
|
+
return measurement;
|
|
50
|
+
}
|
|
51
|
+
function finishMeasurement(measurement) {
|
|
52
|
+
if (!active.has(measurement))
|
|
53
|
+
throw new TypeError("Unknown or finished measurement");
|
|
54
|
+
active.delete(measurement);
|
|
55
|
+
const { name, start } = measurement;
|
|
56
|
+
const time = hrTimeToMs(process.hrtime(start));
|
|
57
|
+
customStats[name].totalTime += time;
|
|
58
|
+
customStats[name].count++;
|
|
59
|
+
customStats[name].averageTime = customStats[name].totalTime / customStats[name].count;
|
|
60
|
+
}
|
|
61
|
+
function record(method, url, status, time) {
|
|
62
|
+
stats.totalTime += time;
|
|
63
|
+
stats.count++;
|
|
64
|
+
stats.averageTime = stats.totalTime / stats.count;
|
|
65
|
+
statusCodes[status] = (statusCodes[status] || 0) + 1;
|
|
66
|
+
if (!opts.endpointStats)
|
|
67
|
+
return;
|
|
68
|
+
let path = url.startsWith("/") ? url.split(/[?#]/, 1)[0] : new URL(url, "http://localhost").pathname;
|
|
69
|
+
const complexPath = complexEndpoints.find((endpoint) => endpoint.pattern.test(path));
|
|
70
|
+
path = complexPath ? complexPath.path : path;
|
|
71
|
+
const endpoint = `${method} ${path}`;
|
|
72
|
+
if (!endpointStats[endpoint]) {
|
|
73
|
+
endpointStats[endpoint] = {
|
|
74
|
+
totalTime: 0,
|
|
75
|
+
averageTime: 0,
|
|
76
|
+
count: 0,
|
|
77
|
+
statusCodes: {}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const stat = endpointStats[endpoint];
|
|
81
|
+
stat.totalTime += time;
|
|
82
|
+
stat.count++;
|
|
83
|
+
stat.averageTime = stat.totalTime / stat.count;
|
|
84
|
+
stat.statusCodes[status] = (stat.statusCodes[status] || 0) + 1;
|
|
85
|
+
}
|
|
86
|
+
const collector = { getStats, startMeasurement, finishMeasurement };
|
|
87
|
+
collectors.set(collector, { record, addHeader: !!opts.addHeader });
|
|
88
|
+
return collector;
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
createStats
|
|
92
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StatsCollector } from './types.js';
|
|
2
|
+
export declare const collectors: WeakMap<StatsCollector, {
|
|
3
|
+
addHeader: boolean;
|
|
4
|
+
record(method: string, url: string, status: number, time: number): void;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function getCollector(stats: StatsCollector): {
|
|
7
|
+
addHeader: boolean;
|
|
8
|
+
record(method: string, url: string, status: number, time: number): void;
|
|
9
|
+
};
|
|
10
|
+
export declare function hrTimeToMs([s, ns]: [number, number]): number;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Collection features; every option defaults to false or an empty list. */
|
|
2
|
+
export interface StatsOptions {
|
|
3
|
+
/** Include counters keyed by HTTP method and path, excluding query strings. */
|
|
4
|
+
endpointStats?: boolean;
|
|
5
|
+
/** Ordered regexparam patterns; first match groups dynamic URLs. Requires endpointStats. */
|
|
6
|
+
complexEndpoints?: string[];
|
|
7
|
+
/** Include custom measurements in getStats(); measurement methods always work. */
|
|
8
|
+
customStats?: boolean;
|
|
9
|
+
/** Add X-Response-Time in milliseconds unless the response already has it. */
|
|
10
|
+
addHeader?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Token owned by its originating collector; pass it unchanged to finishMeasurement. */
|
|
13
|
+
export interface Measurement {
|
|
14
|
+
/** Monotonic start time, not a wall-clock timestamp. */
|
|
15
|
+
readonly start: [number, number];
|
|
16
|
+
/** Name used to aggregate this measurement. */
|
|
17
|
+
readonly name: string;
|
|
18
|
+
}
|
|
19
|
+
/** Completed request or custom measurement totals. All times are milliseconds. */
|
|
20
|
+
export interface Timing {
|
|
21
|
+
/** Sum of completed durations in milliseconds. */
|
|
22
|
+
totalTime: number;
|
|
23
|
+
/** totalTime / count, or zero before completion. */
|
|
24
|
+
averageTime: number;
|
|
25
|
+
/** Number of completed requests or measurements. */
|
|
26
|
+
count: number;
|
|
27
|
+
}
|
|
28
|
+
/** Request totals for a single method/path group. */
|
|
29
|
+
export interface EndpointStats extends Timing {
|
|
30
|
+
/** Counts keyed by final HTTP status code. */
|
|
31
|
+
statusCodes: Record<string, number>;
|
|
32
|
+
}
|
|
33
|
+
/** Totals for a named custom measurement, including unfinished starts. */
|
|
34
|
+
export interface CustomStats extends Timing {
|
|
35
|
+
/** Number of starts, including measurements not yet finished. */
|
|
36
|
+
started: number;
|
|
37
|
+
}
|
|
38
|
+
/** Collector totals; nested maps are live and should be treated as read-only. */
|
|
39
|
+
export interface Stats extends Timing {
|
|
40
|
+
/** Unique collector identity, also used for Elysia plugin deduplication. */
|
|
41
|
+
uuid: string;
|
|
42
|
+
/** ID of the hosting process. */
|
|
43
|
+
pid: number;
|
|
44
|
+
/** Process uptime in milliseconds, not collector age. */
|
|
45
|
+
uptime: number;
|
|
46
|
+
/** Human-readable process uptime. */
|
|
47
|
+
uptimeHumanReadable: string;
|
|
48
|
+
/** Request counts keyed by final HTTP status code. */
|
|
49
|
+
statusCodes: Record<string, number>;
|
|
50
|
+
/** Method/path totals, present only when endpointStats is enabled. */
|
|
51
|
+
endpointStats?: Record<string, EndpointStats>;
|
|
52
|
+
/** Named measurement totals, present only when customStats is enabled. */
|
|
53
|
+
customStats?: Record<string, CustomStats>;
|
|
54
|
+
}
|
|
55
|
+
/** Independent collector that can be shared across framework adapters. */
|
|
56
|
+
export interface StatsCollector {
|
|
57
|
+
/** Return current totals with live nested maps; clone for a durable snapshot. */
|
|
58
|
+
getStats(): Stats;
|
|
59
|
+
/** Start a named monotonic timing; overlapping measurements are supported. */
|
|
60
|
+
startMeasurement(name: string): Measurement;
|
|
61
|
+
/**
|
|
62
|
+
* Finish a token exactly once on its originating collector and update totals.
|
|
63
|
+
* @throws TypeError - If the token is unknown or already finished.
|
|
64
|
+
*/
|
|
65
|
+
finishMeasurement(measurement: Measurement): void;
|
|
66
|
+
}
|
package/docs/guide.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Integration guide
|
|
2
|
+
|
|
3
|
+
## NestJS
|
|
4
|
+
|
|
5
|
+
Use the platform adapter **after `create`, before `init`/`listen`** and before
|
|
6
|
+
middleware that may respond early. No Nest module, interceptor or `/nestjs`
|
|
7
|
+
export is needed. Install Nest and its platform dependencies separately.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { NestFactory } from '@nestjs/core';
|
|
11
|
+
import { createStats } from '@phil-r/stats';
|
|
12
|
+
import { expressStats } from '@phil-r/stats/express';
|
|
13
|
+
import { AppModule } from './app.module.js';
|
|
14
|
+
|
|
15
|
+
const stats = createStats();
|
|
16
|
+
const app = await NestFactory.create(AppModule);
|
|
17
|
+
app.use(expressStats(stats));
|
|
18
|
+
await app.listen(8080);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For Fastify, create with `new FastifyAdapter()` from
|
|
22
|
+
`@nestjs/platform-fastify`, then register its native hook instead of `app.use`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { fastifyStats } from '@phil-r/stats/fastify';
|
|
26
|
+
|
|
27
|
+
app.getHttpAdapter().getInstance().addHook('onRequest', fastifyStats(stats));
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Group patterns must include any global prefix, e.g. `/api/user/:id`.
|
|
31
|
+
See [runnable examples](../examples) for both platforms.
|
|
32
|
+
|
|
33
|
+
## Timing and adapter limits
|
|
34
|
+
|
|
35
|
+
- Express/Node, Fastify and Nest record the final status when headers are written;
|
|
36
|
+
`originalUrl` preserves mounted paths. Fastify's HTTP/1 hook is not a
|
|
37
|
+
`register()` plugin: register at root for all routes/404s, or inside a plugin
|
|
38
|
+
before its routes for that scope. HTTP/2 is unsupported.
|
|
39
|
+
- Elysia and Hono record when the final Response is ready, including early
|
|
40
|
+
responses, errors, validation failures and 404s. Streams are not buffered or
|
|
41
|
+
awaited; immutable headers do not discard the body. Hono requires Node/Bun,
|
|
42
|
+
must be registered first, and error handlers must return a Response normally.
|
|
43
|
+
Errors escaping without a Response and requests rejected by Bun before Elysia
|
|
44
|
+
are not counted.
|
|
45
|
+
- No adapter measures body transmission or counts requests aborted before a
|
|
46
|
+
response/headers. A stats JSON response excludes its own request, which is
|
|
47
|
+
recorded after serialization.
|
|
48
|
+
- Times are monotonic milliseconds; averages are total/count. Custom `started`
|
|
49
|
+
counts all starts, `count` only finishes. Uptime is process uptime, not collector
|
|
50
|
+
age. Counters and unique keys are in-memory and unbounded; nested output maps
|
|
51
|
+
are live/read-only. Clone or serialize for a durable snapshot.
|
|
52
|
+
|
|
53
|
+
Tested: Elysia **1.4.32** (advanced `wrap` API; restricted to 1.4.x), Express
|
|
54
|
+
**5.3.0**, Fastify **5.12.8**, Hono **4.13.18** (`@hono/node-server` **2.1.8**),
|
|
55
|
+
Nest and both platform adapters **12.5.0**, on Node **22.23.5/24.22.2** and Bun.
|
|
56
|
+
Older Nest majors and other platforms are unverified; retest before widening
|
|
57
|
+
Elysia support.
|
|
58
|
+
|
|
59
|
+
## From v1
|
|
60
|
+
|
|
61
|
+
Use named ESM `createStats` and framework-specific adapter imports, passing
|
|
62
|
+
the collector instead of options. Replace `statsMiddleware`/`statsPlugin` and
|
|
63
|
+
move measurement calls onto the collector. Node 22+ is required; there is no
|
|
64
|
+
CJS build, framework registry or dashboard.
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
Use the pinned [Bun](../.bun-version) and [Node](../.node-version) versions.
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
bun install --frozen-lockfile
|
|
72
|
+
bun run format:check # bun run format to fix
|
|
73
|
+
bun check
|
|
74
|
+
bun test
|
|
75
|
+
bun run build
|
|
76
|
+
bun run test:node
|
|
77
|
+
bun run test:package
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Build emits ESM and declarations; checks do not emit. CI tests Node 22/24,
|
|
81
|
+
including real framework listeners. The package smoke test verifies packed
|
|
82
|
+
exports and peer-free Node imports/declarations, not framework behavior.
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phil-r/stats",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Phil Rukin <philipp@rukin.me> (https://rukin.me)",
|
|
6
6
|
"description": "Request statistics middleware",
|
|
7
|
-
"main": "index.js",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"middleware",
|
|
10
10
|
"statistics",
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"koa",
|
|
17
17
|
"rayo",
|
|
18
18
|
"polka",
|
|
19
|
-
"fastify"
|
|
19
|
+
"fastify",
|
|
20
|
+
"elysia",
|
|
21
|
+
"hono",
|
|
22
|
+
"nestjs"
|
|
20
23
|
],
|
|
21
24
|
"bugs": {
|
|
22
25
|
"url": "https://github.com/phil-r/stats/issues"
|
|
@@ -27,33 +30,92 @@
|
|
|
27
30
|
},
|
|
28
31
|
"homepage": "https://github.com/phil-r/stats#readme",
|
|
29
32
|
"scripts": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"prettier": {
|
|
40
|
-
"singleQuote": true
|
|
33
|
+
"check": "bun check --no-script",
|
|
34
|
+
"build": "bun scripts/build.ts",
|
|
35
|
+
"test": "bun test tests/*.test.ts",
|
|
36
|
+
"test:node": "node --test tests/*.integration.mjs",
|
|
37
|
+
"test:package": "bun scripts/package-smoke.ts",
|
|
38
|
+
"prepack": "bun run build",
|
|
39
|
+
"format": "bun fmt --no-script",
|
|
40
|
+
"format:check": "bun fmt --no-script --check",
|
|
41
|
+
"prepare": "node -e \"if (require('node:fs').existsSync('.git') && process.env.NODE_ENV !== 'production' && process.env.HUSKY !== '0') import('husky').then(({default: install}) => install())\""
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"on-headers": "^1.0
|
|
44
|
+
"on-headers": "^1.1.0",
|
|
44
45
|
"pretty-time": "^1.1.0",
|
|
45
|
-
"regexparam": "^
|
|
46
|
-
"uuid": "^7.0.2"
|
|
46
|
+
"regexparam": "^3.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
49
|
+
"@hono/node-server": "^2.1.8",
|
|
50
|
+
"@nestjs/common": "^12.5.0",
|
|
51
|
+
"@nestjs/core": "^12.5.0",
|
|
52
|
+
"@nestjs/platform-express": "^12.5.0",
|
|
53
|
+
"@nestjs/platform-fastify": "^12.5.0",
|
|
54
|
+
"@types/bun": "^1.3.0",
|
|
55
|
+
"@types/express": "^5.0.0",
|
|
56
|
+
"@types/node": "^22.19.1",
|
|
57
|
+
"@types/on-headers": "^1.0.4",
|
|
58
|
+
"@types/pretty-time": "^1.1.5",
|
|
59
|
+
"elysia": "1.4.32",
|
|
60
|
+
"express": "^5.2.1",
|
|
61
|
+
"fastify": "^5.12.8",
|
|
62
|
+
"hono": "^4.13.18",
|
|
63
|
+
"husky": "^9.1.7",
|
|
64
|
+
"reflect-metadata": "^0.2.2",
|
|
65
|
+
"rxjs": "^7.8.2",
|
|
66
|
+
"typescript": "5.9.3"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"elysia": ">=1.4.32 <1.5.0",
|
|
70
|
+
"express": ">=4.18.0 <6",
|
|
71
|
+
"fastify": ">=5.12.8 <6",
|
|
72
|
+
"hono": ">=4.13.18 <5"
|
|
73
|
+
},
|
|
74
|
+
"peerDependenciesMeta": {
|
|
75
|
+
"elysia": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"express": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"fastify": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"hono": {
|
|
85
|
+
"optional": true
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"type": "module",
|
|
89
|
+
"types": "./dist/index.d.ts",
|
|
90
|
+
"files": [
|
|
91
|
+
"dist",
|
|
92
|
+
"docs/guide.md"
|
|
93
|
+
],
|
|
94
|
+
"engines": {
|
|
95
|
+
"node": ">=22"
|
|
96
|
+
},
|
|
97
|
+
"packageManager": "bun@1.4.4",
|
|
98
|
+
"sideEffects": false,
|
|
99
|
+
"exports": {
|
|
100
|
+
".": {
|
|
101
|
+
"types": "./dist/index.d.ts",
|
|
102
|
+
"import": "./dist/index.js"
|
|
103
|
+
},
|
|
104
|
+
"./elysia": {
|
|
105
|
+
"types": "./dist/elysia.d.ts",
|
|
106
|
+
"import": "./dist/elysia.js"
|
|
107
|
+
},
|
|
108
|
+
"./express": {
|
|
109
|
+
"types": "./dist/express.d.ts",
|
|
110
|
+
"import": "./dist/express.js"
|
|
111
|
+
},
|
|
112
|
+
"./fastify": {
|
|
113
|
+
"types": "./dist/fastify.d.ts",
|
|
114
|
+
"import": "./dist/fastify.js"
|
|
115
|
+
},
|
|
116
|
+
"./hono": {
|
|
117
|
+
"types": "./dist/hono.d.ts",
|
|
118
|
+
"import": "./dist/hono.js"
|
|
119
|
+
}
|
|
58
120
|
}
|
|
59
121
|
}
|
package/.eslintrc
DELETED
package/index.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
const urlParse = require('url').parse;
|
|
2
|
-
const onHeaders = require('on-headers');
|
|
3
|
-
const { v4: uuidv4 } = require('uuid');
|
|
4
|
-
const regexparam = require('regexparam');
|
|
5
|
-
const prettyTime = require('pretty-time');
|
|
6
|
-
|
|
7
|
-
const { hrTimeToMs } = require('./utils');
|
|
8
|
-
|
|
9
|
-
function initMiddleware(opts = {}) {
|
|
10
|
-
const stats = {
|
|
11
|
-
uuid: uuidv4(),
|
|
12
|
-
pid: process.pid,
|
|
13
|
-
totalTime: 0,
|
|
14
|
-
averageTime: 0,
|
|
15
|
-
count: 0
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const statusCodes = {};
|
|
19
|
-
const endpointStats = {};
|
|
20
|
-
const complexEndpoints =
|
|
21
|
-
(opts.complexEndpoints &&
|
|
22
|
-
opts.complexEndpoints.map(path => ({ ...regexparam(path), path }))) ||
|
|
23
|
-
[];
|
|
24
|
-
const customStats = {};
|
|
25
|
-
|
|
26
|
-
function getStats() {
|
|
27
|
-
const result = {
|
|
28
|
-
uptime: process.uptime() * 1e3, // convert to ms
|
|
29
|
-
uptimeHumanReadable: prettyTime(Math.floor(process.uptime() * 1e9)),
|
|
30
|
-
statusCodes,
|
|
31
|
-
...stats
|
|
32
|
-
};
|
|
33
|
-
if (opts.endpointStats) {
|
|
34
|
-
result.endpointStats = endpointStats;
|
|
35
|
-
}
|
|
36
|
-
if (opts.customStats) {
|
|
37
|
-
result.customStats = customStats;
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function startMeasurement(name) {
|
|
43
|
-
if (!customStats[name]) {
|
|
44
|
-
customStats[name] = {
|
|
45
|
-
totalTime: 0,
|
|
46
|
-
averageTime: 0,
|
|
47
|
-
started: 0,
|
|
48
|
-
count: 0
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
customStats[name].started++;
|
|
52
|
-
return { start: process.hrtime(), name };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function finishMeasurement({ name, start }) {
|
|
56
|
-
const time = hrTimeToMs(process.hrtime(start));
|
|
57
|
-
customStats[name].totalTime += time;
|
|
58
|
-
customStats[name].count++;
|
|
59
|
-
customStats[name].averageTime =
|
|
60
|
-
customStats[name].totalTime / customStats[name].count;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function statsMiddleware(req, res, next) {
|
|
64
|
-
const requestStart = process.hrtime();
|
|
65
|
-
|
|
66
|
-
if (opts.customStats) {
|
|
67
|
-
req.startMeasurement = startMeasurement;
|
|
68
|
-
req.finishMeasurement = finishMeasurement;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
onHeaders(res, () => {
|
|
72
|
-
const time = hrTimeToMs(process.hrtime(requestStart));
|
|
73
|
-
|
|
74
|
-
if (opts.addHeader) {
|
|
75
|
-
if (!res.getHeader('X-Response-Time')) {
|
|
76
|
-
res.setHeader('X-Response-Time', `${time.toFixed(0)}ms`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
stats.totalTime += time;
|
|
81
|
-
stats.count++;
|
|
82
|
-
stats.averageTime = stats.totalTime / stats.count;
|
|
83
|
-
statusCodes[res.statusCode] = statusCodes[res.statusCode]
|
|
84
|
-
? statusCodes[res.statusCode] + 1
|
|
85
|
-
: 1;
|
|
86
|
-
|
|
87
|
-
if (opts.endpointStats) {
|
|
88
|
-
// prefer using `req.originalUrl` as some frameworks replace `req.url`
|
|
89
|
-
const url = req.originalUrl || req.url;
|
|
90
|
-
let path = urlParse(url).pathname;
|
|
91
|
-
const complexPath = complexEndpoints.find(endpoint =>
|
|
92
|
-
endpoint.pattern.test(path)
|
|
93
|
-
);
|
|
94
|
-
path = complexPath ? complexPath.path : path;
|
|
95
|
-
const endpoint = `${req.method} ${path}`;
|
|
96
|
-
|
|
97
|
-
if (!endpointStats[endpoint]) {
|
|
98
|
-
endpointStats[endpoint] = {
|
|
99
|
-
totalTime: 0,
|
|
100
|
-
averageTime: 0,
|
|
101
|
-
count: 0,
|
|
102
|
-
statusCodes: {}
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
endpointStats[endpoint].totalTime += time;
|
|
107
|
-
endpointStats[endpoint].count++;
|
|
108
|
-
endpointStats[endpoint].averageTime =
|
|
109
|
-
endpointStats[endpoint].totalTime / endpointStats[endpoint].count;
|
|
110
|
-
const eStatusCodes = endpointStats[endpoint].statusCodes;
|
|
111
|
-
eStatusCodes[res.statusCode] = eStatusCodes[res.statusCode]
|
|
112
|
-
? eStatusCodes[res.statusCode] + 1
|
|
113
|
-
: 1;
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
return next();
|
|
118
|
-
}
|
|
119
|
-
return { getStats, statsMiddleware };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
module.exports = initMiddleware;
|