@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.
Files changed (40) hide show
  1. package/README.mdx +58 -7
  2. package/adapter.d.cts +1 -1
  3. package/adapter.d.ts +1 -1
  4. package/bin/den.cjs +3 -3
  5. package/bin/den.js +1 -1
  6. package/builder/index.cjs +2 -2
  7. package/builder/index.d.cts +4 -2
  8. package/builder/index.d.ts +4 -2
  9. package/builder/index.js +1 -1
  10. package/{chunk-2GTQK2PZ.js → chunk-7FOG2MJV.js} +9 -6
  11. package/chunk-A2CYL5LY.js +240 -0
  12. package/chunk-HJFBEJT6.cjs +240 -0
  13. package/{chunk-P4J4M2WZ.cjs → chunk-IATA5QZ7.cjs} +9 -6
  14. package/{define-repository-AO7JI6Cb.d.ts → define-repository-Ck45La_2.d.ts} +1 -1
  15. package/{define-repository-C0MI-J32.d.cts → define-repository-DAyeXF48.d.cts} +1 -1
  16. package/{define-slot-CdWdFM16.d.ts → define-slot-BTlLQ__T.d.ts} +1 -1
  17. package/{define-slot-BEeHNEph.d.cts → define-slot-C6hrdJS3.d.cts} +1 -1
  18. package/define.d.cts +3 -3
  19. package/define.d.ts +3 -3
  20. package/{descriptor-BSaHM5B4.d.ts → descriptor-CNJFrEbS.d.ts} +2 -2
  21. package/{descriptor-C2j6Zz6c.d.cts → descriptor-DmfJP4jQ.d.cts} +2 -2
  22. package/index.d.cts +6 -6
  23. package/index.d.ts +6 -6
  24. package/island/index.d.cts +9 -6
  25. package/island/index.d.ts +9 -6
  26. package/ops/index.cjs +17 -0
  27. package/ops/index.d.cts +39 -0
  28. package/ops/index.d.ts +39 -0
  29. package/ops/index.js +17 -0
  30. package/package.json +25 -16
  31. package/{routes-BvV405k4.d.cts → routes-DQ4ud4bu.d.cts} +1 -1
  32. package/{routes-Di3K_0uQ.d.ts → routes-TkIxQ0Uc.d.ts} +1 -1
  33. package/serve/index.cjs +270 -17
  34. package/serve/index.d.cts +21 -6
  35. package/serve/index.d.ts +21 -6
  36. package/serve/index.js +273 -20
  37. package/{types-BxMQ-VOd.d.cts → types-Ccd5bmt1.d.cts} +23 -4
  38. package/{types-BxMQ-VOd.d.ts → types-Ccd5bmt1.d.ts} +23 -4
  39. package/{view-adapter-DMLlp3Iu.d.cts → view-adapter-CrHh7CjB.d.cts} +2 -2
  40. 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 /_health` | `200 ok` |
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
- Браузерные заголовки рантайм читает только из `X-Forwarded-*` и кладёт в `RequestContextToken`;
206
- `Cookie` — обычным именем.
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
@@ -1,4 +1,4 @@
1
- export { R as RequestContext } from './types-BxMQ-VOd.cjs';
1
+ export { R as RequestContext } from './types-Ccd5bmt1.cjs';
2
2
  import { Token } from '@foxford/ioc';
3
3
 
4
4
  /**
package/adapter.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { R as RequestContext } from './types-BxMQ-VOd.js';
1
+ export { R as RequestContext } from './types-Ccd5bmt1.js';
2
2
  import { Token } from '@foxford/ioc';
3
3
 
4
4
  /**
package/bin/den.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
 
4
- var _chunkP4J4M2WZcjs = require('../chunk-P4J4M2WZ.cjs');
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 _chunkP4J4M2WZcjs.buildApp.call(void 0, target);
73
+ yield _chunkIATA5QZ7cjs.buildApp.call(void 0, target);
74
74
  return;
75
75
  }
76
76
  if (command === "dev") {
77
- yield _chunkP4J4M2WZcjs.devApp.call(void 0, target);
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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  buildApp,
3
3
  devApp
4
- } from "../chunk-2GTQK2PZ.js";
4
+ } from "../chunk-7FOG2MJV.js";
5
5
  import "../chunk-RG7ZV5BD.js";
6
6
  import "../chunk-TPBI6TOU.js";
7
7
  import {
package/builder/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkP4J4M2WZcjs = require('../chunk-P4J4M2WZ.cjs');
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 = _chunkP4J4M2WZcjs.buildApp; exports.defineAppConfig = _chunkP4J4M2WZcjs.defineAppConfig; exports.devApp = _chunkP4J4M2WZcjs.devApp;
13
+ exports.buildApp = _chunkIATA5QZ7cjs.buildApp; exports.defineAppConfig = _chunkIATA5QZ7cjs.defineAppConfig; exports.devApp = _chunkIATA5QZ7cjs.devApp;
@@ -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
  *
@@ -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
@@ -2,7 +2,7 @@ import {
2
2
  buildApp,
3
3
  defineAppConfig,
4
4
  devApp
5
- } from "../chunk-2GTQK2PZ.js";
5
+ } from "../chunk-7FOG2MJV.js";
6
6
  import "../chunk-RG7ZV5BD.js";
7
7
  import "../chunk-TPBI6TOU.js";
8
8
  import "../chunk-6FNC3XMI.js";
@@ -66,7 +66,7 @@ function collectAssets(outDir, base) {
66
66
  }
67
67
 
68
68
  // src/serve/port.ts
69
- var DEFAULT_PORT = 3e3;
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, serveApp } from '@foxford/den/serve'
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 serveApp(app, {
145
+ await serveAppProcess(app, {
146
146
  ${assets} configure: config.server?.configure,
147
147
  host: process.env.HOST,
148
- port: Number(process.env.PORT ?? config.server?.port ?? ${String(DEFAULT_PORT)}),
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
- server: { middlewareMode: true }
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", "/_health"];
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
+ };