@ecdt/server-common 1.5.0 → 1.6.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 +62 -0
- package/package.json +2 -2
- package/src/index.d.ts +37 -2
- package/src/index.js +3 -2
- package/src/services/motorEventosService.js +56 -0
- package/src/services/redMetricsService.js +42 -14
- package/test/motorEventosService.test.js +165 -0
- package/test/redMetricsService.test.js +97 -0
package/README.md
CHANGED
|
@@ -127,6 +127,68 @@ 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
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### `publicarEvento(options)`
|
|
162
|
+
|
|
163
|
+
Envia um evento para o coletor de eventos configurado, sem bloquear o fluxo que o gerou.
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
const { publicarEvento } = require('@ecdt/server-common');
|
|
167
|
+
|
|
168
|
+
publicarEvento({
|
|
169
|
+
tipo: 'meu_evento',
|
|
170
|
+
autorizacao: req.headers.authorization,
|
|
171
|
+
payload: { campo: 'valor' },
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Opção | Default | Descrição |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `tipo` | — | Nome do evento, em `snake_case` |
|
|
178
|
+
| `autorizacao` | — | Header `Authorization` da requisição que originou o evento |
|
|
179
|
+
| `payload` | `{}` | Campos do evento |
|
|
180
|
+
| `url` | `process.env.MOTOR_EVENTOS_URL` | URL completa do coletor; a função não monta caminho nenhum |
|
|
181
|
+
| `timeoutMs` | `2000` | Aborta a requisição |
|
|
182
|
+
| `aoFalhar` | `console.error` | Recebe a mensagem quando a publicação não acontece |
|
|
183
|
+
|
|
184
|
+
A identidade sai do token, nunca do corpo: o coletor resolve o usuário a partir do `Authorization`
|
|
185
|
+
repassado. Sem `tipo` ou sem `autorizacao` a função não faz nada; sem `url` ela ainda registra um
|
|
186
|
+
`console.warn`, porque url ausente é configuração faltando e não um evento que não se aplica.
|
|
187
|
+
|
|
188
|
+
**A função nunca lança e nunca devolve promise.** O evento é secundário ao fluxo que o gerou —
|
|
189
|
+
falha de publicação não pode virar erro de quem chamou, e o retorno `undefined` impede que alguém
|
|
190
|
+
consiga dar `await` e acoplar a latência do request ao motor.
|
|
191
|
+
|
|
130
192
|
---
|
|
131
193
|
|
|
132
194
|
## Instalação
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecdt/server-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Conjunto de ferramentas e configurações comuns nos servidores da Econodata",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "
|
|
8
|
+
"test": "node --test"
|
|
9
9
|
},
|
|
10
10
|
"author": "",
|
|
11
11
|
"license": "ISC",
|
package/src/index.d.ts
CHANGED
|
@@ -104,10 +104,45 @@ export interface RedMetricsOptions {
|
|
|
104
104
|
*/
|
|
105
105
|
export declare function setupRedMetrics(app: Express, options?: RedMetricsOptions): void;
|
|
106
106
|
|
|
107
|
-
/**
|
|
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 };
|
|
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
|
+
*/
|
|
108
120
|
export declare function redMetricsMiddleware(
|
|
109
|
-
options?: Pick<RedMetricsOptions, 'buckets' | 'registry'>
|
|
121
|
+
options?: Pick<RedMetricsOptions, 'buckets' | 'registry'> & {
|
|
122
|
+
/**
|
|
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.
|
|
126
|
+
*/
|
|
127
|
+
routeResolver?: (req: unknown) => string | undefined;
|
|
128
|
+
}
|
|
110
129
|
): RequestHandler;
|
|
111
130
|
|
|
112
131
|
/** Handler do endpoint de métricas (exposição no formato Prometheus). */
|
|
113
132
|
export declare function metricsHandler(registry?: MetricsRegistry): RequestHandler;
|
|
133
|
+
|
|
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 }>;
|
|
138
|
+
|
|
139
|
+
export interface PublicarEventoOptions {
|
|
140
|
+
tipo: string;
|
|
141
|
+
autorizacao: string;
|
|
142
|
+
payload?: Record<string, unknown>;
|
|
143
|
+
url?: string;
|
|
144
|
+
timeoutMs?: number;
|
|
145
|
+
aoFalhar?: (mensagem: string) => void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export declare function publicarEvento(options?: PublicarEventoOptions): void;
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,8 @@ 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
|
+
const { publicarEvento } = require("./services/motorEventosService.js");
|
|
6
7
|
|
|
7
8
|
function expressCommonMiddlewares({ cookieName } = {}){
|
|
8
9
|
return [bodyParser.text(), bodyParser.json(), bodyParser.urlencoded({extended: false}), devMktTokenSanitaze({ cookieName })];
|
|
@@ -11,4 +12,4 @@ function expressCommonMiddlewares({ cookieName } = {}){
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
module.exports = { expressCommonMiddlewares, expressCors, setupGracefulShutdown, setupRedMetrics, redMetricsMiddleware, metricsHandler }
|
|
15
|
+
module.exports = { expressCommonMiddlewares, expressCors, setupGracefulShutdown, setupRedMetrics, initRedMetrics, redMetricsMiddleware, metricsHandler, renderMetrics, publicarEvento }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const TIMEOUT_PADRAO_MS = 2000;
|
|
2
|
+
|
|
3
|
+
function urlConfigurada(url) {
|
|
4
|
+
const configurada = url ?? process.env.MOTOR_EVENTOS_URL;
|
|
5
|
+
|
|
6
|
+
if (typeof configurada !== "string" || configurada.trim() === "") {
|
|
7
|
+
console.warn("motorEventos: url do coletor ausente, evento nao publicado");
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return configurada.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function publicarEvento({
|
|
15
|
+
tipo,
|
|
16
|
+
autorizacao,
|
|
17
|
+
payload,
|
|
18
|
+
url,
|
|
19
|
+
timeoutMs = TIMEOUT_PADRAO_MS,
|
|
20
|
+
aoFalhar = (mensagem) => console.error(mensagem),
|
|
21
|
+
} = {}) {
|
|
22
|
+
try {
|
|
23
|
+
const destino = urlConfigurada(url);
|
|
24
|
+
|
|
25
|
+
if (!destino || !tipo || !autorizacao) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fetch(destino, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json", Authorization: autorizacao },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
tipo,
|
|
34
|
+
dt_evento: new Date().toISOString(),
|
|
35
|
+
payload: payload ?? {},
|
|
36
|
+
}),
|
|
37
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
38
|
+
})
|
|
39
|
+
.then((resposta) => {
|
|
40
|
+
if (!resposta.ok) {
|
|
41
|
+
aoFalhar(`motorEventos: ${tipo} recusado com status ${resposta.status}`);
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.catch((erro) => {
|
|
45
|
+
aoFalhar(`motorEventos: ${tipo} nao publicado (${erro?.message})`);
|
|
46
|
+
});
|
|
47
|
+
} catch (erro) {
|
|
48
|
+
try {
|
|
49
|
+
aoFalhar(`motorEventos: ${tipo} nao publicado (${erro?.message})`);
|
|
50
|
+
} catch (_erroNoCallback) {
|
|
51
|
+
/* empty */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { publicarEvento };
|
|
@@ -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,165 @@
|
|
|
1
|
+
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
|
|
4
|
+
const { publicarEvento } = require('../src/services/motorEventosService.js');
|
|
5
|
+
|
|
6
|
+
const URL = 'http://coletor.interno/eventos';
|
|
7
|
+
|
|
8
|
+
const esperarMicrotasks = () => new Promise((resolve) => setImmediate(resolve));
|
|
9
|
+
|
|
10
|
+
describe('publicarEvento', () => {
|
|
11
|
+
let fetchOriginal;
|
|
12
|
+
let warnOriginal;
|
|
13
|
+
let urlOriginal;
|
|
14
|
+
let chamadas;
|
|
15
|
+
let falhas;
|
|
16
|
+
let avisos;
|
|
17
|
+
|
|
18
|
+
const aoFalhar = (mensagem) => falhas.push(mensagem);
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
fetchOriginal = global.fetch;
|
|
22
|
+
warnOriginal = console.warn;
|
|
23
|
+
urlOriginal = process.env.MOTOR_EVENTOS_URL;
|
|
24
|
+
chamadas = [];
|
|
25
|
+
falhas = [];
|
|
26
|
+
avisos = [];
|
|
27
|
+
console.warn = (mensagem) => avisos.push(mensagem);
|
|
28
|
+
global.fetch = (url, opcoes) => {
|
|
29
|
+
chamadas.push({ url, opcoes });
|
|
30
|
+
return Promise.resolve({ ok: true, status: 201 });
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
global.fetch = fetchOriginal;
|
|
36
|
+
console.warn = warnOriginal;
|
|
37
|
+
if (urlOriginal === undefined) {
|
|
38
|
+
delete process.env.MOTOR_EVENTOS_URL;
|
|
39
|
+
} else {
|
|
40
|
+
process.env.MOTOR_EVENTOS_URL = urlOriginal;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('envia POST na url configurada com o token de quem chamou', async () => {
|
|
45
|
+
publicarEvento({
|
|
46
|
+
tipo: 'meu_evento',
|
|
47
|
+
autorizacao: 'Bearer jwt',
|
|
48
|
+
payload: { campo: 'valor' },
|
|
49
|
+
url: URL,
|
|
50
|
+
aoFalhar,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await esperarMicrotasks();
|
|
54
|
+
|
|
55
|
+
assert.equal(chamadas.length, 1);
|
|
56
|
+
assert.equal(chamadas[0].url, URL);
|
|
57
|
+
assert.equal(chamadas[0].opcoes.method, 'POST');
|
|
58
|
+
assert.equal(chamadas[0].opcoes.headers.Authorization, 'Bearer jwt');
|
|
59
|
+
|
|
60
|
+
const corpo = JSON.parse(chamadas[0].opcoes.body);
|
|
61
|
+
assert.equal(corpo.tipo, 'meu_evento');
|
|
62
|
+
assert.deepEqual(corpo.payload, { campo: 'valor' });
|
|
63
|
+
assert.ok(!Number.isNaN(Date.parse(corpo.dt_evento)));
|
|
64
|
+
assert.deepEqual(falhas, []);
|
|
65
|
+
assert.deepEqual(avisos, []);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('nao acrescenta caminho nenhum a url configurada', async () => {
|
|
69
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: 'http://coletor.interno/v2/x', aoFalhar });
|
|
70
|
+
await esperarMicrotasks();
|
|
71
|
+
|
|
72
|
+
assert.equal(chamadas[0].url, 'http://coletor.interno/v2/x');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('cai na MOTOR_EVENTOS_URL quando a url nao e passada', async () => {
|
|
76
|
+
process.env.MOTOR_EVENTOS_URL = URL;
|
|
77
|
+
|
|
78
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', aoFalhar });
|
|
79
|
+
await esperarMicrotasks();
|
|
80
|
+
|
|
81
|
+
assert.equal(chamadas[0].url, URL);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('avisa e nao publica quando a url esta ausente', async () => {
|
|
85
|
+
delete process.env.MOTOR_EVENTOS_URL;
|
|
86
|
+
|
|
87
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', aoFalhar });
|
|
88
|
+
await esperarMicrotasks();
|
|
89
|
+
|
|
90
|
+
assert.equal(chamadas.length, 0);
|
|
91
|
+
assert.equal(avisos.length, 1);
|
|
92
|
+
assert.match(avisos[0], /url do coletor ausente/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('avisa quando a url nao e string ou e so espaco', async () => {
|
|
96
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: 123, aoFalhar });
|
|
97
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: ' ', aoFalhar });
|
|
98
|
+
await esperarMicrotasks();
|
|
99
|
+
|
|
100
|
+
assert.equal(chamadas.length, 0);
|
|
101
|
+
assert.equal(avisos.length, 2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('nao publica sem tipo ou sem autorizacao', async () => {
|
|
105
|
+
publicarEvento({ autorizacao: 'Bearer jwt', url: URL, aoFalhar });
|
|
106
|
+
publicarEvento({ tipo: 'meu_evento', url: URL, aoFalhar });
|
|
107
|
+
await esperarMicrotasks();
|
|
108
|
+
|
|
109
|
+
assert.equal(chamadas.length, 0);
|
|
110
|
+
assert.deepEqual(falhas, []);
|
|
111
|
+
assert.deepEqual(avisos, []);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('nao quebra quando chamado sem argumento nenhum', () => {
|
|
115
|
+
delete process.env.MOTOR_EVENTOS_URL;
|
|
116
|
+
|
|
117
|
+
assert.doesNotThrow(() => publicarEvento());
|
|
118
|
+
assert.equal(chamadas.length, 0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('nao propaga rejeicao do fetch e reporta a falha', async () => {
|
|
122
|
+
global.fetch = () => Promise.reject(new Error('connect ECONNREFUSED'));
|
|
123
|
+
|
|
124
|
+
assert.doesNotThrow(() =>
|
|
125
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: URL, aoFalhar })
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await esperarMicrotasks();
|
|
129
|
+
|
|
130
|
+
assert.equal(falhas.length, 1);
|
|
131
|
+
assert.match(falhas[0], /meu_evento/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('reporta resposta de erro sem lancar', async () => {
|
|
135
|
+
global.fetch = () => Promise.resolve({ ok: false, status: 400 });
|
|
136
|
+
|
|
137
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: URL, aoFalhar });
|
|
138
|
+
await esperarMicrotasks();
|
|
139
|
+
|
|
140
|
+
assert.equal(falhas.length, 1);
|
|
141
|
+
assert.match(falhas[0], /400/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('nao propaga erro sincrono do fetch', () => {
|
|
145
|
+
global.fetch = () => {
|
|
146
|
+
throw new TypeError('opcoes invalidas');
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
assert.doesNotThrow(() =>
|
|
150
|
+
publicarEvento({ tipo: 'meu_evento', autorizacao: 'Bearer jwt', url: URL, aoFalhar })
|
|
151
|
+
);
|
|
152
|
+
assert.equal(falhas.length, 1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('nao devolve promise, para ninguem conseguir esperar por ela', () => {
|
|
156
|
+
const retorno = publicarEvento({
|
|
157
|
+
tipo: 'meu_evento',
|
|
158
|
+
autorizacao: 'Bearer jwt',
|
|
159
|
+
url: URL,
|
|
160
|
+
aoFalhar,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assert.equal(retorno, undefined);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -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
|
+
});
|