@foxford/den 3.7.0 → 4.1.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.mdx +58 -7
- package/adapter.d.cts +1 -1
- package/adapter.d.ts +1 -1
- package/bin/den.cjs +3 -3
- package/bin/den.js +1 -1
- package/builder/index.cjs +2 -2
- package/builder/index.d.cts +4 -2
- package/builder/index.d.ts +4 -2
- package/builder/index.js +1 -1
- package/{chunk-2GTQK2PZ.js → chunk-7FOG2MJV.js} +9 -6
- package/chunk-A2CYL5LY.js +240 -0
- package/chunk-HJFBEJT6.cjs +240 -0
- package/{chunk-P4J4M2WZ.cjs → chunk-IATA5QZ7.cjs} +9 -6
- package/{define-repository-AO7JI6Cb.d.ts → define-repository-Ck45La_2.d.ts} +1 -1
- package/{define-repository-C0MI-J32.d.cts → define-repository-DAyeXF48.d.cts} +1 -1
- package/{define-slot-CdWdFM16.d.ts → define-slot-BTlLQ__T.d.ts} +1 -1
- package/{define-slot-BEeHNEph.d.cts → define-slot-C6hrdJS3.d.cts} +1 -1
- package/define.d.cts +3 -3
- package/define.d.ts +3 -3
- package/{descriptor-BSaHM5B4.d.ts → descriptor-CNJFrEbS.d.ts} +2 -2
- package/{descriptor-C2j6Zz6c.d.cts → descriptor-DmfJP4jQ.d.cts} +2 -2
- package/index.d.cts +6 -6
- package/index.d.ts +6 -6
- package/island/index.d.cts +9 -6
- package/island/index.d.ts +9 -6
- package/ops/index.cjs +17 -0
- package/ops/index.d.cts +39 -0
- package/ops/index.d.ts +39 -0
- package/ops/index.js +17 -0
- package/package.json +25 -16
- package/{routes-BvV405k4.d.cts → routes-DQ4ud4bu.d.cts} +1 -1
- package/{routes-Di3K_0uQ.d.ts → routes-TkIxQ0Uc.d.ts} +1 -1
- package/serve/index.cjs +270 -17
- package/serve/index.d.cts +21 -6
- package/serve/index.d.ts +21 -6
- package/serve/index.js +273 -20
- package/{types-BxMQ-VOd.d.cts → types-Ccd5bmt1.d.cts} +23 -4
- package/{types-BxMQ-VOd.d.ts → types-Ccd5bmt1.d.ts} +23 -4
- package/{view-adapter-DMLlp3Iu.d.cts → view-adapter-CrHh7CjB.d.cts} +2 -2
- package/{view-adapter-DNLRSFka.d.ts → view-adapter-DPDvv09J.d.ts} +2 -2
package/README.mdx
CHANGED
|
@@ -83,7 +83,7 @@ import { renderApp } from '@foxford/den/island'
|
|
|
83
83
|
const { status, html, state, head, headHtml, redirect, assets } = await renderApp(app, {
|
|
84
84
|
pathname: '/legal/general',
|
|
85
85
|
search: '?from=footer',
|
|
86
|
-
context: { cookies, headers }, // реквизиты
|
|
86
|
+
context: { cookies, headers, backendHeaders }, // браузерные данные и реквизиты бэкенда
|
|
87
87
|
inputs: { 'host:User': user, 'host:Settings': settings }, // входные данные хоста по ключу token.description
|
|
88
88
|
})
|
|
89
89
|
```
|
|
@@ -182,14 +182,32 @@ describeDocument(): DocumentContribution {
|
|
|
182
182
|
}
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
+
Для нескольких процессов одного приложения:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
WORKERS=2 node build/server.mjs
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
По умолчанию `WORKERS=1`. Все воркеры слушают общий прикладной порт, primary держит
|
|
192
|
+
один служебный порт с их суммарными метриками. `server.configure` выполняется в каждом
|
|
193
|
+
воркере; память и контейнеры процессов отдельные. Значение задаётся каждому приложению
|
|
194
|
+
и должно быть положительным целым. После обновления Den приложение нужно пересобрать,
|
|
195
|
+
чтобы новая точка входа начала читать `WORKERS`. Переменная работает и в `den dev`,
|
|
196
|
+
но локальное хранилище dev-трасс между воркерами не объединяется.
|
|
197
|
+
|
|
198
|
+
При остановке Den дожидается активных запросов; зависшие воркеры принудительно
|
|
199
|
+
завершаются через 10 секунд. Ошибка запуска или выход любого воркера завершает группу
|
|
200
|
+
с ошибкой, чтобы внешний супервизор мог перезапустить приложение целиком.
|
|
201
|
+
|
|
185
202
|
Рантайм вокруг дескриптора отдаёт три ручки:
|
|
186
203
|
|
|
187
204
|
| Ручка | Ответ |
|
|
188
205
|
| -------------------------------------- | ------------------------------------------- |
|
|
189
|
-
| `QUERY /_render` + `{ url, inputs? }` | `200` + ответ `renderApp` телом |
|
|
206
|
+
| `QUERY /_render` + `{ url, inputs?, backendHeaders? }` | `200` + ответ `renderApp` телом |
|
|
190
207
|
| `GET /_render?url=<path+search>` | то же без входных данных хоста |
|
|
191
208
|
| `GET /_routes` | `200` + массив regex-строк заявки на адреса |
|
|
192
|
-
| `GET /
|
|
209
|
+
| `GET /healthz`, `GET /livez` на втором порту | `200 ok`; в кластере `/livez` проверяет heartbeat воркеров |
|
|
210
|
+
| `GET /metrics` на втором порту | Prometheus-метрики Node/process и HTTP |
|
|
193
211
|
|
|
194
212
|
`QUERY` — безопасный метод с телом (RFC 9110): адрес и входные данные хоста едут JSON-ом,
|
|
195
213
|
потому что заголовкам они не по размеру. `GET` остаётся для хоста, который входных данных
|
|
@@ -202,8 +220,42 @@ describeDocument(): DocumentContribution {
|
|
|
202
220
|
рендер уходит как `reply.send(error)`: Fastify зовёт `onError` у потребителя с исходным
|
|
203
221
|
исключением; тело — стандартный JSON ошибки Fastify.
|
|
204
222
|
|
|
205
|
-
|
|
206
|
-
`
|
|
223
|
+
Реквизиты запроса (`RequestContextToken`) отвечают на три разных вопроса: `headers` — что прислал
|
|
224
|
+
браузер (только из `X-Forwarded-*`), `cookies` — чему равна отдельная кука, `backendHeaders` — что
|
|
225
|
+
предъявлять бэкенду в каждом запросе. Последнее присылает хост телом (`backendHeaders`), по
|
|
226
|
+
умолчанию это `Cookie` хопа, а навешивает на клиент `@foxford/den-sdk`.
|
|
227
|
+
|
|
228
|
+
### Служебный порт
|
|
229
|
+
|
|
230
|
+
Апп слушает основной и служебный порты в одном жизненном цикле: production
|
|
231
|
+
`8080` и `9090`. Переопределение — `PORT`/`SERVICE_PORT`, интерфейсы — `HOST`/`OPS_HOST`.
|
|
232
|
+
Для совместной разработки объявляются локальные `server.port` и `server.opsPort`;
|
|
233
|
+
production server.mjs использует production-умолчания.
|
|
234
|
+
|
|
235
|
+
`/healthz` — простой GET с `200`, `/livez` — проверка доступности, `/metrics` — экспорт полного
|
|
236
|
+
штатного набора Node/process, дополнительных resourceUsage и HTTP-измерений.
|
|
237
|
+
На основном порту служебные пути отвечают `404`.
|
|
238
|
+
Метрики не зависят от DSN Sentry. Метод `close()` результата `await serveAppProcess(...)`
|
|
239
|
+
закрывает всю группу; у прямого `serveApp` прежний `instance.close()` закрывает оба
|
|
240
|
+
сервера одного процесса.
|
|
241
|
+
|
|
242
|
+
Для занятости event loop используйте `nodejs_eventloop_utilization_ratio`:
|
|
243
|
+
в кластере это максимум по воркерам, от 0 до 1. Сопоставляйте его с lag и временем
|
|
244
|
+
ответа. Штатный `nodejs_eventloop_utilization_summary` суммирует quantile воркеров
|
|
245
|
+
и для такого алерта не подходит. `/livez` остаётся зелёным, пока жив хотя бы один
|
|
246
|
+
воркер; `/metrics` при неответившем воркере вернёт `503` через 5 секунд. Ошибки и
|
|
247
|
+
длительность scrape тоже должны попадать в мониторинг.
|
|
248
|
+
|
|
249
|
+
Для своего сервера доступен отдельный подпуть без Fastify:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { getProcessMetrics, startOpsServer } from '@foxford/den/ops'
|
|
253
|
+
|
|
254
|
+
getProcessMetrics().startRuntime('my-app')
|
|
255
|
+
const ops = await startOpsServer({ port: 9090 })
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Детали API и агрегация cluster — в SPEC.md → «Служебный сервер».
|
|
207
259
|
|
|
208
260
|
### Расширение снаружи: пример с Sentry
|
|
209
261
|
|
|
@@ -233,7 +285,6 @@ import * as Sentry from '@sentry/node'
|
|
|
233
285
|
export default (instance) => {
|
|
234
286
|
Sentry.setupFastifyErrorHandler(instance)
|
|
235
287
|
|
|
236
|
-
instance.get('/metrics', () => renderMetrics())
|
|
237
288
|
}
|
|
238
289
|
```
|
|
239
290
|
|
|
@@ -320,6 +371,6 @@ export default defineAppConfig({
|
|
|
320
371
|
| `@foxford/den/island` | остров: жизненный цикл, шов `renderApp`, порт `ViewAdapter`, очередь клиентских падений (`reportIslandError`) |
|
|
321
372
|
|
|
322
373
|
|
|
323
|
-
| `@foxford/den/serve` | сетевой транспорт: `serveApp`, `appPlugin`
|
|
374
|
+
| `@foxford/den/serve` | сетевой транспорт: `serveAppProcess`, `serveApp`, `appPlugin` |
|
|
324
375
|
| `@foxford/den/build` | конфиг приложения и сборка: `defineAppConfig`, `buildApp` |
|
|
325
376
|
| `@foxford/den/bin` | точка входа команды `den` |
|
package/adapter.d.cts
CHANGED
package/adapter.d.ts
CHANGED
package/bin/den.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkIATA5QZ7cjs = require('../chunk-IATA5QZ7.cjs');
|
|
5
5
|
require('../chunk-KQAB6C6Q.cjs');
|
|
6
6
|
require('../chunk-A6ZPAM6Z.cjs');
|
|
7
7
|
|
|
@@ -70,11 +70,11 @@ function run(argv) {
|
|
|
70
70
|
}
|
|
71
71
|
const target = { base: options["base"], root: (_a = options["root"]) != null ? _a : process.cwd() };
|
|
72
72
|
if (command === "build") {
|
|
73
|
-
yield
|
|
73
|
+
yield _chunkIATA5QZ7cjs.buildApp.call(void 0, target);
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
if (command === "dev") {
|
|
77
|
-
yield
|
|
77
|
+
yield _chunkIATA5QZ7cjs.devApp.call(void 0, target);
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
80
|
fail(`\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \xAB${command}\xBB`);
|
package/bin/den.js
CHANGED
package/builder/index.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
var
|
|
5
|
+
var _chunkIATA5QZ7cjs = require('../chunk-IATA5QZ7.cjs');
|
|
6
6
|
require('../chunk-KQAB6C6Q.cjs');
|
|
7
7
|
require('../chunk-A6ZPAM6Z.cjs');
|
|
8
8
|
require('../chunk-T5QVCXVB.cjs');
|
|
@@ -10,4 +10,4 @@ require('../chunk-T5QVCXVB.cjs');
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
exports.buildApp =
|
|
13
|
+
exports.buildApp = _chunkIATA5QZ7cjs.buildApp; exports.defineAppConfig = _chunkIATA5QZ7cjs.defineAppConfig; exports.devApp = _chunkIATA5QZ7cjs.devApp;
|
package/builder/index.d.cts
CHANGED
|
@@ -54,13 +54,15 @@ interface AppServerConfig {
|
|
|
54
54
|
*/
|
|
55
55
|
container?: (container: Container) => void;
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
57
|
+
* Прикладной порт процесса в разработке; production — PORT или 8080.
|
|
58
58
|
*
|
|
59
59
|
* `PORT` в окружении важнее: порт назначает тот, кто процесс поднимает, — в проде оркестратор.
|
|
60
|
-
* Объявление здесь — умолчание
|
|
60
|
+
* Объявление здесь — локальное умолчание приложения, чтобы адрес не приходилось повторять в каждой
|
|
61
61
|
* команде запуска и в префиксе артефактов.
|
|
62
62
|
*/
|
|
63
63
|
port?: number;
|
|
64
|
+
/** Служебный порт в разработке; прод — SERVICE_PORT или 9090. */
|
|
65
|
+
opsPort?: number;
|
|
64
66
|
/**
|
|
65
67
|
* Расширение сетевого инстанса до начала прослушивания: Sentry, метрики, cors.
|
|
66
68
|
*
|
package/builder/index.d.ts
CHANGED
|
@@ -54,13 +54,15 @@ interface AppServerConfig {
|
|
|
54
54
|
*/
|
|
55
55
|
container?: (container: Container) => void;
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
57
|
+
* Прикладной порт процесса в разработке; production — PORT или 8080.
|
|
58
58
|
*
|
|
59
59
|
* `PORT` в окружении важнее: порт назначает тот, кто процесс поднимает, — в проде оркестратор.
|
|
60
|
-
* Объявление здесь — умолчание
|
|
60
|
+
* Объявление здесь — локальное умолчание приложения, чтобы адрес не приходилось повторять в каждой
|
|
61
61
|
* команде запуска и в префиксе артефактов.
|
|
62
62
|
*/
|
|
63
63
|
port?: number;
|
|
64
|
+
/** Служебный порт в разработке; прод — SERVICE_PORT или 9090. */
|
|
65
|
+
opsPort?: number;
|
|
64
66
|
/**
|
|
65
67
|
* Расширение сетевого инстанса до начала прослушивания: Sentry, метрики, cors.
|
|
66
68
|
*
|
package/builder/index.js
CHANGED
|
@@ -66,7 +66,7 @@ function collectAssets(outDir, base) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// src/serve/port.ts
|
|
69
|
-
var DEFAULT_PORT =
|
|
69
|
+
var DEFAULT_PORT = 8080;
|
|
70
70
|
function resolvePort(declared) {
|
|
71
71
|
var _a, _b;
|
|
72
72
|
return Number((_b = (_a = process.env["PORT"]) != null ? _a : declared) != null ? _b : DEFAULT_PORT);
|
|
@@ -134,7 +134,7 @@ function generateServerProcess(configPath, base, mode) {
|
|
|
134
134
|
` : "";
|
|
135
135
|
return `import { fileURLToPath } from 'node:url'
|
|
136
136
|
|
|
137
|
-
import { loadAppEnv,
|
|
137
|
+
import { loadAppEnv, serveAppProcess } from '@foxford/den/serve'
|
|
138
138
|
|
|
139
139
|
import app from ${JSON.stringify(VIRTUAL_SERVER_APP)}
|
|
140
140
|
import config from ${JSON.stringify(configPath)}
|
|
@@ -142,10 +142,12 @@ import config from ${JSON.stringify(configPath)}
|
|
|
142
142
|
// \u0423\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044F \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u2014 \u0434\u043E \u043F\u0435\u0440\u0432\u043E\u0433\u043E \u0447\u0442\u0435\u043D\u0438\u044F: \u043F\u043E\u0440\u0442 \u0438 \u0430\u0434\u0440\u0435\u0441\u0430 \u0447\u0438\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0438\u0436\u0435
|
|
143
143
|
loadAppEnv(fileURLToPath(new URL('..', import.meta.url)))
|
|
144
144
|
|
|
145
|
-
await
|
|
145
|
+
await serveAppProcess(app, {
|
|
146
146
|
${assets} configure: config.server?.configure,
|
|
147
147
|
host: process.env.HOST,
|
|
148
|
-
port: Number(process.env.
|
|
148
|
+
ops: { host: process.env.OPS_HOST, port: Number(process.env.SERVICE_PORT ?? ${mode === "development" ? "config.server?.opsPort ?? 9090" : "9090"}) },
|
|
149
|
+
port: Number(process.env.PORT ?? ${mode === "development" ? "config.server?.port ?? " : ""}${String(DEFAULT_PORT)}),
|
|
150
|
+
workers: Number(process.env.WORKERS ?? 1),
|
|
149
151
|
})
|
|
150
152
|
`;
|
|
151
153
|
}
|
|
@@ -201,7 +203,8 @@ function loadAppConfig(root) {
|
|
|
201
203
|
configFile: false,
|
|
202
204
|
logLevel: "silent",
|
|
203
205
|
root,
|
|
204
|
-
|
|
206
|
+
// Этот сервер только загружает конфиг; пересборкой в dev управляет watchSources.
|
|
207
|
+
server: { middlewareMode: true, watch: null }
|
|
205
208
|
});
|
|
206
209
|
try {
|
|
207
210
|
const loaded = yield server.ssrLoadModule(configPath);
|
|
@@ -324,7 +327,7 @@ import { pathToFileURL } from "url";
|
|
|
324
327
|
import os from "os";
|
|
325
328
|
var TOOL = "den";
|
|
326
329
|
var PAD = 11;
|
|
327
|
-
var ENDPOINTS = ["/_render?url=<\u043F\u0443\u0442\u044C>", "/_routes"
|
|
330
|
+
var ENDPOINTS = ["/_render?url=<\u043F\u0443\u0442\u044C>", "/_routes"];
|
|
328
331
|
var OUTPUTS = ["build/client", "build/server", "build/server.mjs"];
|
|
329
332
|
var SHORTCUTS = [
|
|
330
333
|
{ key: "r", title: "\u043F\u0435\u0440\u0435\u0441\u043E\u0431\u0440\u0430\u0442\u044C" },
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__async
|
|
3
|
+
} from "./chunk-6FNC3XMI.js";
|
|
4
|
+
|
|
5
|
+
// src/ops/index.ts
|
|
6
|
+
import { createServer } from "http";
|
|
7
|
+
import { ClusterRegistry } from "@prometheus-io/client";
|
|
8
|
+
|
|
9
|
+
// src/ops/metrics.ts
|
|
10
|
+
import { Counter, Gauge as Gauge2, Histogram, Registry } from "@prometheus-io/client";
|
|
11
|
+
|
|
12
|
+
// src/ops/runtime.ts
|
|
13
|
+
import cluster from "cluster";
|
|
14
|
+
import { performance as performance2 } from "perf_hooks";
|
|
15
|
+
import { collectDefaultMetrics, Gauge } from "@prometheus-io/client";
|
|
16
|
+
function collectRuntime(registry, appName) {
|
|
17
|
+
registry.setDefaultLabels({ app: appName, process_role: cluster.isWorker ? "worker" : "main" });
|
|
18
|
+
collectDefaultMetrics({ register: registry });
|
|
19
|
+
let previousElu = performance2.eventLoopUtilization();
|
|
20
|
+
new Gauge({
|
|
21
|
+
aggregator: "max",
|
|
22
|
+
collect() {
|
|
23
|
+
const current = performance2.eventLoopUtilization();
|
|
24
|
+
this.set(performance2.eventLoopUtilization(current, previousElu).utilization);
|
|
25
|
+
previousElu = current;
|
|
26
|
+
},
|
|
27
|
+
help: "Event loop utilization since the last scrape; maximum across workers",
|
|
28
|
+
name: "nodejs_eventloop_utilization_ratio",
|
|
29
|
+
registers: [registry]
|
|
30
|
+
});
|
|
31
|
+
let previousCpu = process.cpuUsage();
|
|
32
|
+
let previousTime = performance2.now();
|
|
33
|
+
new Gauge({
|
|
34
|
+
aggregator: "sum",
|
|
35
|
+
collect() {
|
|
36
|
+
const currentCpu = process.cpuUsage();
|
|
37
|
+
const currentTime = performance2.now();
|
|
38
|
+
const elapsed = currentTime - previousTime;
|
|
39
|
+
this.set(
|
|
40
|
+
elapsed > 0 ? (currentCpu.user - previousCpu.user + currentCpu.system - previousCpu.system) / (elapsed * 1e3) : 0
|
|
41
|
+
);
|
|
42
|
+
previousCpu = currentCpu;
|
|
43
|
+
previousTime = currentTime;
|
|
44
|
+
},
|
|
45
|
+
help: "CPU time divided by elapsed wall time since the last scrape",
|
|
46
|
+
name: "process_cpu_utilization_ratio",
|
|
47
|
+
registers: [registry]
|
|
48
|
+
});
|
|
49
|
+
new Gauge({
|
|
50
|
+
collect() {
|
|
51
|
+
this.set(process.uptime());
|
|
52
|
+
},
|
|
53
|
+
help: "Process uptime in seconds",
|
|
54
|
+
name: "process_uptime_seconds",
|
|
55
|
+
registers: [registry]
|
|
56
|
+
});
|
|
57
|
+
new Gauge({
|
|
58
|
+
collect() {
|
|
59
|
+
this.set(process.memoryUsage().arrayBuffers);
|
|
60
|
+
},
|
|
61
|
+
help: "Memory used by ArrayBuffers in bytes",
|
|
62
|
+
name: "nodejs_arraybuffer_memory_bytes",
|
|
63
|
+
registers: [registry]
|
|
64
|
+
});
|
|
65
|
+
new Gauge({
|
|
66
|
+
aggregator: "max",
|
|
67
|
+
collect() {
|
|
68
|
+
this.set(process.resourceUsage().maxRSS * 1024);
|
|
69
|
+
},
|
|
70
|
+
help: "Maximum resident memory in bytes",
|
|
71
|
+
name: "process_max_resident_memory_bytes",
|
|
72
|
+
registers: [registry]
|
|
73
|
+
});
|
|
74
|
+
for (const [name, field] of [
|
|
75
|
+
["minor_page_faults", "minorPageFault"],
|
|
76
|
+
["major_page_faults", "majorPageFault"],
|
|
77
|
+
["filesystem_reads", "fsRead"],
|
|
78
|
+
["filesystem_writes", "fsWrite"],
|
|
79
|
+
["voluntary_context_switches", "voluntaryContextSwitches"],
|
|
80
|
+
["involuntary_context_switches", "involuntaryContextSwitches"]
|
|
81
|
+
]) {
|
|
82
|
+
new Gauge({
|
|
83
|
+
collect() {
|
|
84
|
+
this.set(process.resourceUsage()[field]);
|
|
85
|
+
},
|
|
86
|
+
help: `Process ${name} since startup`,
|
|
87
|
+
name: `process_${name}_total`,
|
|
88
|
+
registers: [registry]
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/ops/metrics.ts
|
|
94
|
+
var HTTP_LABELS = ["method", "route", "status"];
|
|
95
|
+
var SECOND_BUCKETS = [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30];
|
|
96
|
+
var ProcessMetrics = class {
|
|
97
|
+
constructor() {
|
|
98
|
+
this.registry = new Registry();
|
|
99
|
+
this.runtimeStarted = false;
|
|
100
|
+
}
|
|
101
|
+
counter(name, labelNames = []) {
|
|
102
|
+
const existing = this.registry.getSingleMetric(name);
|
|
103
|
+
if (existing) {
|
|
104
|
+
return existing;
|
|
105
|
+
}
|
|
106
|
+
return new Counter({ help: name, labelNames, name, registers: [this.registry] });
|
|
107
|
+
}
|
|
108
|
+
histogram(name, labelNames = []) {
|
|
109
|
+
const existing = this.registry.getSingleMetric(name);
|
|
110
|
+
if (existing) {
|
|
111
|
+
return existing;
|
|
112
|
+
}
|
|
113
|
+
return new Histogram({ buckets: SECOND_BUCKETS, help: name, labelNames, name, registers: [this.registry] });
|
|
114
|
+
}
|
|
115
|
+
/** Полный штатный набор Node/process-метрик клиента и дополнительные данные процесса. */
|
|
116
|
+
startRuntime(appName) {
|
|
117
|
+
if (this.runtimeStarted) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.runtimeStarted = true;
|
|
121
|
+
collectRuntime(this.registry, appName);
|
|
122
|
+
}
|
|
123
|
+
/** HTTP-измерения с ограниченными метками маршрута; вызывается один раз на запрос. */
|
|
124
|
+
trackHttp(req, res, route) {
|
|
125
|
+
const active = this.registry.getSingleMetric("http_requests_in_flight");
|
|
126
|
+
const gauge = active != null ? active : new Gauge2({
|
|
127
|
+
help: "HTTP requests currently being served",
|
|
128
|
+
name: "http_requests_in_flight",
|
|
129
|
+
registers: [this.registry]
|
|
130
|
+
});
|
|
131
|
+
const started = performance.now();
|
|
132
|
+
gauge.inc();
|
|
133
|
+
let finished = false;
|
|
134
|
+
const finish = () => {
|
|
135
|
+
var _a;
|
|
136
|
+
if (finished) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
finished = true;
|
|
140
|
+
gauge.dec();
|
|
141
|
+
const labels = { method: (_a = req.method) != null ? _a : "UNKNOWN", route: route(), status: String(res.statusCode) };
|
|
142
|
+
this.counter("http_requests_total", HTTP_LABELS).inc(labels);
|
|
143
|
+
this.histogram("http_request_duration_seconds", HTTP_LABELS).observe(labels, (performance.now() - started) / 1e3);
|
|
144
|
+
};
|
|
145
|
+
res.once("finish", finish);
|
|
146
|
+
res.once("close", finish);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var PROCESS_METRICS = /* @__PURE__ */ Symbol.for("@foxford/den/process-metrics");
|
|
150
|
+
var storage = globalThis;
|
|
151
|
+
function getProcessMetrics() {
|
|
152
|
+
var _a;
|
|
153
|
+
return (_a = storage[PROCESS_METRICS]) != null ? _a : storage[PROCESS_METRICS] = new ProcessMetrics();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/ops/index.ts
|
|
157
|
+
var RESERVED_PATHS = /* @__PURE__ */ new Set(["/healthz", "/livez", "/metrics"]);
|
|
158
|
+
function isOpsPath(url = "/") {
|
|
159
|
+
var _a;
|
|
160
|
+
return RESERVED_PATHS.has((_a = url.split("?")[0]) != null ? _a : "/");
|
|
161
|
+
}
|
|
162
|
+
function clusterMetrics(metrics = getProcessMetrics()) {
|
|
163
|
+
const aggregator = new ClusterRegistry();
|
|
164
|
+
ClusterRegistry.setRegistries(metrics.registry);
|
|
165
|
+
const lifecycle = aggregator;
|
|
166
|
+
return {
|
|
167
|
+
collect: () => aggregator.clusterMetrics(),
|
|
168
|
+
contentType: metrics.registry.contentType,
|
|
169
|
+
shutdown: () => lifecycle.shutdown()
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function startOpsServer() {
|
|
173
|
+
return __async(this, arguments, function* (options = {}) {
|
|
174
|
+
var _a;
|
|
175
|
+
const metrics = (_a = options.metrics) != null ? _a : getProcessMetrics();
|
|
176
|
+
const server = createServer((req, res) => {
|
|
177
|
+
var _a2, _b, _c, _d;
|
|
178
|
+
const path = (_a2 = req.url) == null ? void 0 : _a2.split("?")[0];
|
|
179
|
+
res.setHeader("cache-control", "no-store");
|
|
180
|
+
if (req.method !== "GET" || !["/healthz", "/livez", "/metrics"].includes(path != null ? path : "")) {
|
|
181
|
+
res.writeHead(404).end();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (path !== "/metrics") {
|
|
185
|
+
res.writeHead(path === "/livez" && ((_b = options.isLive) == null ? void 0 : _b.call(options)) === false ? 503 : 200, {
|
|
186
|
+
"content-type": "text/plain; charset=utf-8"
|
|
187
|
+
}).end("ok");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
void ((_d = (_c = options.collect) == null ? void 0 : _c.call(options)) != null ? _d : metrics.registry.metrics()).then((body) => {
|
|
191
|
+
var _a3;
|
|
192
|
+
if (!res.destroyed) {
|
|
193
|
+
res.writeHead(200, { "content-type": (_a3 = options.contentType) != null ? _a3 : metrics.registry.contentType }).end(body);
|
|
194
|
+
}
|
|
195
|
+
}).catch(() => {
|
|
196
|
+
if (!res.destroyed) {
|
|
197
|
+
res.writeHead(503).end("metrics unavailable");
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
yield new Promise((resolve, reject) => {
|
|
202
|
+
var _a2, _b;
|
|
203
|
+
server.once("error", reject);
|
|
204
|
+
server.listen((_a2 = options.port) != null ? _a2 : 9090, (_b = options.host) != null ? _b : "0.0.0.0", () => {
|
|
205
|
+
server.removeListener("error", reject);
|
|
206
|
+
resolve();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
return server;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function closeHttpServer(server, timeoutMs = 5e3) {
|
|
213
|
+
return __async(this, null, function* () {
|
|
214
|
+
if (!server.listening) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
yield new Promise((resolve, reject) => {
|
|
218
|
+
const timer = setTimeout(() => server.closeAllConnections(), timeoutMs);
|
|
219
|
+
timer.unref();
|
|
220
|
+
server.close((error) => {
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
if (error) {
|
|
223
|
+
reject(error);
|
|
224
|
+
} else {
|
|
225
|
+
resolve();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
server.closeIdleConnections();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export {
|
|
234
|
+
ProcessMetrics,
|
|
235
|
+
getProcessMetrics,
|
|
236
|
+
isOpsPath,
|
|
237
|
+
clusterMetrics,
|
|
238
|
+
startOpsServer,
|
|
239
|
+
closeHttpServer
|
|
240
|
+
};
|