@ecdt/server-common 1.4.0 → 1.5.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 +62 -0
- package/package.json +3 -2
- package/src/index.d.ts +128 -66
- package/src/index.js +2 -2
- package/src/services/redMetricsService.js +42 -14
- package/test/redMetricsService.test.js +97 -0
package/README.md
CHANGED
|
@@ -127,6 +127,35 @@ topk(10, sum by (route)(rate(http_request_duration_seconds_count{service="dev-mk
|
|
|
127
127
|
|
|
128
128
|
Para wiring manual, a lib também exporta `redMetricsMiddleware(options?)` e `metricsHandler(registry?)`.
|
|
129
129
|
|
|
130
|
+
#### Servidor que não é express (Nitro/h3, fastify, http puro)
|
|
131
|
+
|
|
132
|
+
`setupRedMetrics` precisa de um `app` do express. Fora dele, monte as três peças à mão:
|
|
133
|
+
|
|
134
|
+
- `initRedMetrics(options?)` — label `service`, métricas default do Node e o histograma. Devolve `{ registry, histogram }`.
|
|
135
|
+
- `redMetricsMiddleware({ routeResolver })` — o middleware `(req, res, next)`. Sem `req.route` (que é do express) o label `route` cai em `unmatched`, então informe um resolver. **Nunca use a URL crua**: cada id na rota vira uma série temporal nova e a métrica fica impagável.
|
|
136
|
+
- `renderMetrics(registry?)` — `{ contentType, body }` para servir o endpoint.
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
// Nitro: server/plugins/red-metrics.ts
|
|
140
|
+
import { initRedMetrics, redMetricsMiddleware } from "@ecdt/server-common";
|
|
141
|
+
|
|
142
|
+
initRedMetrics({ serviceName: "site-vue3" });
|
|
143
|
+
|
|
144
|
+
// Nitro: server/middleware/01.red-metrics.ts
|
|
145
|
+
export default fromNodeMiddleware(
|
|
146
|
+
redMetricsMiddleware({ routeResolver: (req) => templateDaRota(req.url) }),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Nitro: server/routes/metrics.get.ts
|
|
150
|
+
import { renderMetrics } from "@ecdt/server-common";
|
|
151
|
+
|
|
152
|
+
export default defineEventHandler(async (event) => {
|
|
153
|
+
const { contentType, body } = await renderMetrics();
|
|
154
|
+
setResponseHeader(event, "content-type", contentType);
|
|
155
|
+
return body;
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
130
159
|
---
|
|
131
160
|
|
|
132
161
|
## Instalação
|
|
@@ -155,3 +184,36 @@ app.listen(3000);
|
|
|
155
184
|
```
|
|
156
185
|
|
|
157
186
|
---
|
|
187
|
+
|
|
188
|
+
## TypeScript
|
|
189
|
+
|
|
190
|
+
O pacote publica os tipos em `src/index.d.ts` (declarado em `types` no `package.json`), tipados com `express` e `http`. **Não é necessário declarar `declare module '@ecdt/server-common'` na aplicação** — se você tem um arquivo desses em `src/@types/`, pode apagar: uma declaração ambiente local sombreia os tipos do pacote e passa a mentir em silêncio se as assinaturas divergirem.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import express from "express";
|
|
194
|
+
import http from "http";
|
|
195
|
+
import { expressCommonMiddlewares, expressCors, setupRedMetrics, setupGracefulShutdown } from "@ecdt/server-common";
|
|
196
|
+
|
|
197
|
+
const app = express();
|
|
198
|
+
|
|
199
|
+
app.use(...expressCommonMiddlewares()); // RequestHandler[]
|
|
200
|
+
app.use(expressCors({ econodataOrigins: true })); // RequestHandler
|
|
201
|
+
setupRedMetrics(app, { serviceName: "meu-ms" });
|
|
202
|
+
|
|
203
|
+
const server = http.createServer(app);
|
|
204
|
+
server.listen(3000);
|
|
205
|
+
setupGracefulShutdown(server, { onShutdown: () => pool.end() });
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
As interfaces de options são exportadas para reuso: `ExpressCommonMiddlewaresOptions`, `ExpressCorsOptions` (e sua base `ExpressCorsOptionsBase`), `GracefulShutdownOptions`, `RedMetricsOptions` e `MetricsRegistry`.
|
|
209
|
+
|
|
210
|
+
`ExpressCorsOptions` é uma união que exige `origins` e/ou `econodataOrigins: true` — sem nenhum dos dois o `expressCors` lança no boot, então o erro aparece já em compilação:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
expressCors({ econodataOrigins: true }); // ok
|
|
214
|
+
expressCors({ methods: ["GET"] }); // erro TS2345 — lançaria no boot
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`prom-client` aparece nos tipos como a interface estrutural `MetricsRegistry`, e não como `import` do pacote — assim quem não usa métricas compila sem tê-lo instalado.
|
|
218
|
+
|
|
219
|
+
---
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecdt/server-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Conjunto de ferramentas e configurações comuns nos servidores da Econodata",
|
|
5
5
|
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"test": "
|
|
8
|
+
"test": "node --test"
|
|
8
9
|
},
|
|
9
10
|
"author": "",
|
|
10
11
|
"license": "ISC",
|
package/src/index.d.ts
CHANGED
|
@@ -1,75 +1,137 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Nome(s) do cookie de sessão, em ordem de preferência.
|
|
5
|
-
* Quando omitido, o nome é resolvido por requisição a partir da origem (origin > referer > host):
|
|
6
|
-
* origens com subdomínio "hml" usam `hml-ecdt_token_site`, as demais usam `ecdt_token_site`.
|
|
7
|
-
*/
|
|
8
|
-
cookieName?: string | string[];
|
|
9
|
-
}
|
|
1
|
+
import type { Express, RequestHandler } from 'express';
|
|
2
|
+
import type { Server } from 'http';
|
|
10
3
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
): Array<(req: unknown, res: unknown, next: (err?: unknown) => void) => void>;
|
|
4
|
+
export interface ExpressCommonMiddlewaresOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Nome(s) do cookie de sessão, em ordem de preferência.
|
|
7
|
+
* Quando omitido, o nome é resolvido por requisição a partir da origem (origin > referer > host):
|
|
8
|
+
* origens com subdomínio "hml" usam `hml-ecdt_token_site`, as demais usam `ecdt_token_site`.
|
|
9
|
+
*/
|
|
10
|
+
cookieName?: string | string[];
|
|
11
|
+
}
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/** Lista exata de métodos permitidos. Default: todos (GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS). */
|
|
31
|
-
methods?: string[];
|
|
32
|
-
}
|
|
13
|
+
/**
|
|
14
|
+
* Aplica bodyParser.text(), bodyParser.json(), bodyParser.urlencoded({extended: false}) e
|
|
15
|
+
* devMktTokenSanitaze() no middleware do express.
|
|
16
|
+
*
|
|
17
|
+
* Uso: app.use(...expressCommonMiddlewares())
|
|
18
|
+
*/
|
|
19
|
+
export declare function expressCommonMiddlewares(
|
|
20
|
+
options?: ExpressCommonMiddlewaresOptions
|
|
21
|
+
): RequestHandler[];
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
export interface ExpressCorsOptionsBase {
|
|
24
|
+
/** Origins permitidas (ex: ["https://app.econodata.com.br"]). */
|
|
25
|
+
origins?: string[];
|
|
26
|
+
/** Permite todas as origins https do domínio econodata.com.br (qualquer subdomínio). */
|
|
27
|
+
econodataOrigins?: boolean;
|
|
28
|
+
/** Headers adicionais além dos padrões (Authorization, Content-Type, X-Requested-With). */
|
|
29
|
+
extraHeaders?: string[];
|
|
30
|
+
/** Lista exata de headers permitidos — substitui os padrões. Não combinar com extraHeaders. */
|
|
31
|
+
headers?: string[];
|
|
32
|
+
/** Lista exata de métodos permitidos. Default: todos (GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS). */
|
|
33
|
+
methods?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Exige `origins` e/ou `econodataOrigins: true`: sem nenhum dos dois o expressCors lança no boot,
|
|
38
|
+
* então a união recusa em compilação o que o runtime já recusa.
|
|
39
|
+
*/
|
|
40
|
+
export type ExpressCorsOptions =
|
|
41
|
+
| (ExpressCorsOptionsBase & { origins: string[] })
|
|
42
|
+
| (ExpressCorsOptionsBase & { econodataOrigins: true });
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cria o middleware de CORS padrão dos servidores Econodata.
|
|
46
|
+
* Credentials sempre habilitado; origins não listadas não recebem headers CORS.
|
|
47
|
+
*
|
|
48
|
+
* Uso: app.use(expressCors({ origins: ["https://app.econodata.com.br"] }))
|
|
49
|
+
*/
|
|
50
|
+
export declare function expressCors(options: ExpressCorsOptions): RequestHandler;
|
|
51
|
+
|
|
52
|
+
export interface GracefulShutdownOptions {
|
|
53
|
+
/** Cleanup extra (fechar pools, Redis, etc.) executado após o server fechar. */
|
|
54
|
+
onShutdown?: () => void | Promise<void>;
|
|
55
|
+
/** Tempo máximo do graceful antes de forçar a saída. Default: 10000 (grace de 40s − 20s de preStop). */
|
|
56
|
+
failsafeTimeoutMs?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Registra o graceful shutdown padrão dos microsserviços em Kubernetes: em SIGTERM/SIGINT fecha o
|
|
61
|
+
* server, drena as conexões em voo, roda o onShutdown e encerra o processo.
|
|
62
|
+
*
|
|
63
|
+
* Uso: setupGracefulShutdown(server, { onShutdown: () => pool.end() })
|
|
64
|
+
*/
|
|
65
|
+
export declare function setupGracefulShutdown(
|
|
66
|
+
server: Server,
|
|
67
|
+
options?: GracefulShutdownOptions
|
|
68
|
+
): void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Registry do prom-client, descrito estruturalmente para não acoplar a resolução destes tipos ao
|
|
72
|
+
* pacote: quem não usa métricas continua compilando sem `prom-client` instalado.
|
|
73
|
+
*/
|
|
74
|
+
export interface MetricsRegistry {
|
|
75
|
+
contentType: string;
|
|
76
|
+
metrics(): Promise<string>;
|
|
77
|
+
setDefaultLabels(labels: Record<string, string>): void;
|
|
78
|
+
getSingleMetric(name: string): unknown;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface RedMetricsOptions {
|
|
82
|
+
/** Nome do microsserviço. Vira o label `service` em todas as métricas. Fallback: env SERVICE_NAME > npm_package_name. */
|
|
83
|
+
serviceName?: string;
|
|
84
|
+
/** Caminho do endpoint de métricas. Default: '/metrics'. */
|
|
85
|
+
metricsPath?: string;
|
|
86
|
+
/** Buckets do histograma de latência, em segundos. */
|
|
87
|
+
buckets?: number[];
|
|
88
|
+
/** Coletar métricas default do Node (heap, event loop, GC...). Default: true. */
|
|
89
|
+
collectDefaultMetrics?: boolean;
|
|
90
|
+
/** Registry alternativo do prom-client. Default: registry global. */
|
|
91
|
+
registry?: MetricsRegistry;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Configura as métricas RED (Rate, Errors, Duration) no app express: registra o middleware de
|
|
96
|
+
* medição (antes das rotas) e o endpoint /metrics.
|
|
97
|
+
*
|
|
98
|
+
* Alimenta o histograma `http_request_duration_seconds{method,route,status_code}`, que habilita
|
|
99
|
+
* latência por endpoint, latência total, RPS, erros e top endpoints com erro.
|
|
100
|
+
*
|
|
101
|
+
* Requer `prom-client` instalado no microsserviço (peerDependency).
|
|
102
|
+
*
|
|
103
|
+
* Uso: setupRedMetrics(app, { serviceName: "dev-mkt-busca" })
|
|
104
|
+
*/
|
|
105
|
+
export declare function setupRedMetrics(app: Express, options?: RedMetricsOptions): void;
|
|
41
106
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
collectDefaultMetrics?: boolean;
|
|
51
|
-
/** Registry alternativo do prom-client. Default: registry global. */
|
|
52
|
-
registry?: unknown;
|
|
53
|
-
}
|
|
107
|
+
/**
|
|
108
|
+
* Registra o label `service`, as métricas default do Node e o histograma RED, sem depender de
|
|
109
|
+
* framework. Use quando o servidor não é express (ex.: Nitro/h3) e você mesmo vai montar o
|
|
110
|
+
* middleware e a rota de métricas.
|
|
111
|
+
*/
|
|
112
|
+
export declare function initRedMetrics(
|
|
113
|
+
options?: Omit<RedMetricsOptions, 'metricsPath'>
|
|
114
|
+
): { registry: MetricsRegistry; histogram: unknown };
|
|
54
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Middleware `(req, res, next)` que alimenta o histograma `http_request_duration_seconds`.
|
|
118
|
+
* Compatível com express e com adaptadores de middleware de Node (ex.: `fromNodeMiddleware` do h3).
|
|
119
|
+
*/
|
|
120
|
+
export declare function redMetricsMiddleware(
|
|
121
|
+
options?: Pick<RedMetricsOptions, 'buckets' | 'registry'> & {
|
|
55
122
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* Alimenta o histograma `http_request_duration_seconds{method,route,status_code}`,
|
|
60
|
-
* que habilita latência por endpoint, latência total, RPS, erros e top endpoints com erro.
|
|
61
|
-
*
|
|
62
|
-
* Requer `prom-client` instalado no microsserviço (peerDependency).
|
|
63
|
-
*
|
|
64
|
-
* Uso: setupRedMetrics(app, { serviceName: "dev-mkt-busca" })
|
|
123
|
+
* Resolve o label `route`. Default: template do express (`req.route.path`). Framework sem
|
|
124
|
+
* `req.route` precisa informar o seu, senão todo request cai em `unmatched` — e nunca use a
|
|
125
|
+
* URL crua, que explode a cardinalidade da métrica.
|
|
65
126
|
*/
|
|
66
|
-
|
|
127
|
+
routeResolver?: (req: unknown) => string | undefined;
|
|
128
|
+
}
|
|
129
|
+
): RequestHandler;
|
|
67
130
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
options?: Pick<RedMetricsOptions, "buckets" | "registry">
|
|
71
|
-
): (req: unknown, res: unknown, next: (err?: unknown) => void) => void;
|
|
131
|
+
/** Handler do endpoint de métricas (exposição no formato Prometheus). */
|
|
132
|
+
export declare function metricsHandler(registry?: MetricsRegistry): RequestHandler;
|
|
72
133
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
134
|
+
/** Content-type e corpo das métricas, para servir fora do express. */
|
|
135
|
+
export declare function renderMetrics(
|
|
136
|
+
registry?: MetricsRegistry
|
|
137
|
+
): Promise<{ contentType: string; body: string }>;
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ const bodyParser = require("body-parser");
|
|
|
2
2
|
const { devMktTokenSanitaze } = require("./services/tokenService.js");
|
|
3
3
|
const { expressCors } = require("./services/corsService.js");
|
|
4
4
|
const { setupGracefulShutdown } = require("./services/shutdownService.js");
|
|
5
|
-
const { setupRedMetrics, redMetricsMiddleware, metricsHandler } = require("./services/redMetricsService.js");
|
|
5
|
+
const { setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics } = require("./services/redMetricsService.js");
|
|
6
6
|
|
|
7
7
|
function expressCommonMiddlewares({ cookieName } = {}){
|
|
8
8
|
return [bodyParser.text(), bodyParser.json(), bodyParser.urlencoded({extended: false}), devMktTokenSanitaze({ cookieName })];
|
|
@@ -11,4 +11,4 @@ function expressCommonMiddlewares({ cookieName } = {}){
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
module.exports = { expressCommonMiddlewares, expressCors, setupGracefulShutdown, setupRedMetrics, redMetricsMiddleware, metricsHandler }
|
|
14
|
+
module.exports = { expressCommonMiddlewares, expressCors, setupGracefulShutdown, setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics }
|
|
@@ -34,24 +34,28 @@ function routeTemplate(req) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Middleware
|
|
37
|
+
* Middleware (req, res, next) que mede cada requisição e alimenta o histograma RED
|
|
38
38
|
* (http_request_duration_seconds) com os labels method, route e status_code.
|
|
39
39
|
*
|
|
40
40
|
* @param {object} [options]
|
|
41
41
|
* @param {number[]} [options.buckets] - buckets de latência em segundos
|
|
42
42
|
* @param {import("prom-client").Registry} [options.registry] - registry (default: global do prom-client)
|
|
43
|
-
* @
|
|
43
|
+
* @param {(req: object) => string} [options.routeResolver] - resolve o label `route`. Default: template
|
|
44
|
+
* do express (`req.route.path`). Framework sem `req.route` (ex.: h3/Nitro) precisa informar o
|
|
45
|
+
* seu, senão todo request cai em "unmatched".
|
|
46
|
+
* @returns middleware compatível com express e com adaptadores de middleware de Node
|
|
44
47
|
*/
|
|
45
|
-
function redMetricsMiddleware({ buckets, registry } = {}) {
|
|
48
|
+
function redMetricsMiddleware({ buckets, registry, routeResolver } = {}) {
|
|
46
49
|
const reg = registry || client.register;
|
|
47
50
|
const histogram = getHistogram(reg, buckets || DEFAULT_BUCKETS);
|
|
51
|
+
const resolveRoute = routeResolver || routeTemplate;
|
|
48
52
|
return function (req, res, next) {
|
|
49
53
|
const stop = histogram.startTimer();
|
|
50
54
|
// 'finish' dispara após a rota resolver -> req.route já está preenchido aqui.
|
|
51
55
|
res.on("finish", () =>
|
|
52
56
|
stop({
|
|
53
57
|
method: req.method,
|
|
54
|
-
route:
|
|
58
|
+
route: resolveRoute(req) || "unmatched",
|
|
55
59
|
status_code: String(res.statusCode),
|
|
56
60
|
})
|
|
57
61
|
);
|
|
@@ -59,6 +63,16 @@ function redMetricsMiddleware({ buckets, registry } = {}) {
|
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Exposição das métricas sem depender de framework: devolve o content-type e o corpo no
|
|
68
|
+
* formato Prometheus. Use quando o servidor não é express (ex.: rota do Nitro).
|
|
69
|
+
* @param {import("prom-client").Registry} [registry] - default: global do prom-client
|
|
70
|
+
* @returns {Promise<{ contentType: string, body: string }>}
|
|
71
|
+
*/
|
|
72
|
+
async function renderMetrics(registry = client.register) {
|
|
73
|
+
return { contentType: registry.contentType, body: await registry.metrics() };
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
/**
|
|
63
77
|
* Handler do endpoint de métricas (exposição no formato Prometheus).
|
|
64
78
|
* @param {import("prom-client").Registry} [registry] - default: global do prom-client
|
|
@@ -66,28 +80,26 @@ function redMetricsMiddleware({ buckets, registry } = {}) {
|
|
|
66
80
|
*/
|
|
67
81
|
function metricsHandler(registry = client.register) {
|
|
68
82
|
return async function (_req, res) {
|
|
69
|
-
|
|
70
|
-
res.
|
|
83
|
+
const { contentType, body } = await renderMetrics(registry);
|
|
84
|
+
res.set("Content-Type", contentType);
|
|
85
|
+
res.end(body);
|
|
71
86
|
};
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
90
|
+
* Registra o label `service`, as métricas default do Node e o histograma RED — sem tocar em
|
|
91
|
+
* framework nenhum. É a parte do setup que serve para express e para qualquer outro servidor.
|
|
77
92
|
*
|
|
78
|
-
* Uso: setupRedMetrics(app, { serviceName: "dev-mkt-busca" })
|
|
79
|
-
*
|
|
80
|
-
* @param {object} app - instância do express
|
|
81
93
|
* @param {object} [options]
|
|
82
94
|
* @param {string} [options.serviceName] - nome do MS; vira o label `service` em todas as métricas.
|
|
83
95
|
* Fallback: env SERVICE_NAME > npm_package_name.
|
|
84
|
-
* @param {string} [options.metricsPath] - caminho do endpoint de métricas (default: "/metrics")
|
|
85
96
|
* @param {number[]} [options.buckets] - buckets de latência em segundos
|
|
86
97
|
* @param {boolean} [options.collectDefaultMetrics] - coletar métricas default do Node
|
|
87
98
|
* (heap, event loop, GC...). Default: true.
|
|
88
99
|
* @param {import("prom-client").Registry} [options.registry] - registry alternativo (default: global)
|
|
100
|
+
* @returns {{ registry: import("prom-client").Registry, histogram: object }}
|
|
89
101
|
*/
|
|
90
|
-
function
|
|
102
|
+
function initRedMetrics(options = {}) {
|
|
91
103
|
const registry = options.registry || client.register;
|
|
92
104
|
const serviceName =
|
|
93
105
|
options.serviceName || process.env.SERVICE_NAME || process.env.npm_package_name;
|
|
@@ -101,8 +113,24 @@ function setupRedMetrics(app, options = {}) {
|
|
|
101
113
|
client.collectDefaultMetrics({ register: registry });
|
|
102
114
|
}
|
|
103
115
|
|
|
116
|
+
return { registry, histogram: getHistogram(registry, options.buckets || DEFAULT_BUCKETS) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Configura as métricas RED no app express em uma linha. Registra o middleware
|
|
121
|
+
* (aplique ANTES das rotas) e o endpoint /metrics.
|
|
122
|
+
*
|
|
123
|
+
* Uso: setupRedMetrics(app, { serviceName: "dev-mkt-busca" })
|
|
124
|
+
*
|
|
125
|
+
* @param {object} app - instância do express
|
|
126
|
+
* @param {object} [options] - ver initRedMetrics, mais:
|
|
127
|
+
* @param {string} [options.metricsPath] - caminho do endpoint de métricas (default: "/metrics")
|
|
128
|
+
*/
|
|
129
|
+
function setupRedMetrics(app, options = {}) {
|
|
130
|
+
const { registry } = initRedMetrics(options);
|
|
131
|
+
|
|
104
132
|
app.use(redMetricsMiddleware({ buckets: options.buckets, registry }));
|
|
105
133
|
app.get(options.metricsPath || "/metrics", metricsHandler(registry));
|
|
106
134
|
}
|
|
107
135
|
|
|
108
|
-
module.exports = { setupRedMetrics, redMetricsMiddleware, metricsHandler };
|
|
136
|
+
module.exports = { setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const { test, describe } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
const client = require('prom-client');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
initRedMetrics,
|
|
8
|
+
redMetricsMiddleware,
|
|
9
|
+
renderMetrics,
|
|
10
|
+
} = require('../src/services/redMetricsService.js');
|
|
11
|
+
|
|
12
|
+
const registryLimpo = () => new client.Registry();
|
|
13
|
+
|
|
14
|
+
const medir = (registry, req, options = {}) => {
|
|
15
|
+
const middleware = redMetricsMiddleware({ registry, ...options });
|
|
16
|
+
const res = Object.assign(new EventEmitter(), { statusCode: 200 });
|
|
17
|
+
let chamouNext = false;
|
|
18
|
+
|
|
19
|
+
middleware(req, res, () => {
|
|
20
|
+
chamouNext = true;
|
|
21
|
+
});
|
|
22
|
+
res.emit('finish');
|
|
23
|
+
|
|
24
|
+
return { chamouNext };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('redMetricsMiddleware', () => {
|
|
28
|
+
test('usa o routeResolver quando o framework não tem req.route', async () => {
|
|
29
|
+
const registry = registryLimpo();
|
|
30
|
+
|
|
31
|
+
const { chamouNext } = medir(registry, { method: 'GET', url: '/consulta-empresa/123-x' }, {
|
|
32
|
+
routeResolver: () => '/consulta-empresa/:slug',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.equal(chamouNext, true);
|
|
36
|
+
const texto = await registry.metrics();
|
|
37
|
+
assert.match(texto, /route="\/consulta-empresa\/:slug"/);
|
|
38
|
+
assert.match(texto, /method="GET"/);
|
|
39
|
+
assert.match(texto, /status_code="200"/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('sem req.route e sem resolver o label cai em unmatched', async () => {
|
|
43
|
+
const registry = registryLimpo();
|
|
44
|
+
|
|
45
|
+
medir(registry, { method: 'POST', url: '/qualquer/coisa' });
|
|
46
|
+
|
|
47
|
+
assert.match(await registry.metrics(), /route="unmatched"/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('resolver que devolve vazio também cai em unmatched', async () => {
|
|
51
|
+
const registry = registryLimpo();
|
|
52
|
+
|
|
53
|
+
medir(registry, { method: 'GET', url: '/x' }, { routeResolver: () => undefined });
|
|
54
|
+
|
|
55
|
+
assert.match(await registry.metrics(), /route="unmatched"/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('template do express continua sendo o default', async () => {
|
|
59
|
+
const registry = registryLimpo();
|
|
60
|
+
|
|
61
|
+
medir(registry, { method: 'GET', baseUrl: '/api', route: { path: '/users/:id' } });
|
|
62
|
+
|
|
63
|
+
assert.match(await registry.metrics(), /route="\/api\/users\/:id"/);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('initRedMetrics', () => {
|
|
68
|
+
test('aplica o label service e devolve registry e histograma', async () => {
|
|
69
|
+
const registry = registryLimpo();
|
|
70
|
+
|
|
71
|
+
const { histogram } = initRedMetrics({ registry, serviceName: 'site-vue3', collectDefaultMetrics: false });
|
|
72
|
+
|
|
73
|
+
assert.ok(histogram);
|
|
74
|
+
histogram.observe({ method: 'GET', route: '/', status_code: '200' }, 0.1);
|
|
75
|
+
assert.match(await registry.metrics(), /service="site-vue3"/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('chamar duas vezes não duplica métrica', () => {
|
|
79
|
+
const registry = registryLimpo();
|
|
80
|
+
|
|
81
|
+
initRedMetrics({ registry, collectDefaultMetrics: false });
|
|
82
|
+
|
|
83
|
+
assert.doesNotThrow(() => initRedMetrics({ registry, collectDefaultMetrics: false }));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('renderMetrics', () => {
|
|
88
|
+
test('devolve content-type e corpo no formato Prometheus', async () => {
|
|
89
|
+
const registry = registryLimpo();
|
|
90
|
+
initRedMetrics({ registry, serviceName: 'site-vue3', collectDefaultMetrics: false });
|
|
91
|
+
|
|
92
|
+
const { contentType, body } = await renderMetrics(registry);
|
|
93
|
+
|
|
94
|
+
assert.match(contentType, /text\/plain/);
|
|
95
|
+
assert.match(body, /http_request_duration_seconds/);
|
|
96
|
+
});
|
|
97
|
+
});
|