@foxford/den 3.7.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.mdx +31 -6
  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-2P5TAWRW.cjs +227 -0
  11. package/chunk-DUU4MQ4B.js +227 -0
  12. package/{chunk-2GTQK2PZ.js → chunk-FMRORLLE.js} +6 -4
  13. package/{chunk-P4J4M2WZ.cjs → chunk-OC4LQ2CK.cjs} +6 -4
  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 +70 -16
  34. package/serve/index.d.cts +10 -5
  35. package/serve/index.d.ts +10 -5
  36. package/serve/index.js +71 -17
  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
  ```
@@ -186,10 +186,11 @@ describeDocument(): DocumentContribution {
186
186
 
187
187
  | Ручка | Ответ |
188
188
  | -------------------------------------- | ------------------------------------------- |
189
- | `QUERY /_render` + `{ url, inputs? }` | `200` + ответ `renderApp` телом |
189
+ | `QUERY /_render` + `{ url, inputs?, backendHeaders? }` | `200` + ответ `renderApp` телом |
190
190
  | `GET /_render?url=<path+search>` | то же без входных данных хоста |
191
191
  | `GET /_routes` | `200` + массив regex-строк заявки на адреса |
192
- | `GET /_health` | `200 ok` |
192
+ | `GET /healthz`, `GET /livez` на втором порту | `200 ok` |
193
+ | `GET /metrics` на втором порту | Prometheus-метрики Node/process и HTTP |
193
194
 
194
195
  `QUERY` — безопасный метод с телом (RFC 9110): адрес и входные данные хоста едут JSON-ом,
195
196
  потому что заголовкам они не по размеру. `GET` остаётся для хоста, который входных данных
@@ -202,8 +203,33 @@ describeDocument(): DocumentContribution {
202
203
  рендер уходит как `reply.send(error)`: Fastify зовёт `onError` у потребителя с исходным
203
204
  исключением; тело — стандартный JSON ошибки Fastify.
204
205
 
205
- Браузерные заголовки рантайм читает только из `X-Forwarded-*` и кладёт в `RequestContextToken`;
206
- `Cookie` — обычным именем.
206
+ Реквизиты запроса (`RequestContextToken`) отвечают на три разных вопроса: `headers` — что прислал
207
+ браузер (только из `X-Forwarded-*`), `cookies` — чему равна отдельная кука, `backendHeaders` — что
208
+ предъявлять бэкенду в каждом запросе. Последнее присылает хост телом (`backendHeaders`), по
209
+ умолчанию это `Cookie` хопа, а навешивает на клиент `@foxford/den-sdk`.
210
+
211
+ ### Служебный порт
212
+
213
+ Апп слушает основной и служебный порты в одном жизненном цикле: production
214
+ `8080` и `9090`. Переопределение — `PORT`/`SERVICE_PORT`, интерфейсы — `HOST`/`OPS_HOST`.
215
+ Для совместной разработки объявляются локальные `server.port` и `server.opsPort`;
216
+ production server.mjs использует production-умолчания.
217
+
218
+ `/healthz` — простой GET с `200`, `/livez` — `200`, `/metrics` — экспорт полного
219
+ штатного набора Node/process, дополнительных resourceUsage и HTTP-измерений.
220
+ На основном порту служебные пути отвечают `404`.
221
+ Метрики не зависят от DSN Sentry; `instance.close()` закрывает оба сервера.
222
+
223
+ Для своего сервера доступен отдельный подпуть без Fastify:
224
+
225
+ ```ts
226
+ import { getProcessMetrics, startOpsServer } from '@foxford/den/ops'
227
+
228
+ getProcessMetrics().startRuntime('my-app')
229
+ const ops = await startOpsServer({ port: 9090 })
230
+ ```
231
+
232
+ Детали API и агрегация cluster — в SPEC.md → «Служебный сервер».
207
233
 
208
234
  ### Расширение снаружи: пример с Sentry
209
235
 
@@ -233,7 +259,6 @@ import * as Sentry from '@sentry/node'
233
259
  export default (instance) => {
234
260
  Sentry.setupFastifyErrorHandler(instance)
235
261
 
236
- instance.get('/metrics', () => renderMetrics())
237
262
  }
238
263
  ```
239
264
 
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 _chunkOC4LQ2CKcjs = require('../chunk-OC4LQ2CK.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 _chunkOC4LQ2CKcjs.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 _chunkOC4LQ2CKcjs.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-FMRORLLE.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 _chunkOC4LQ2CKcjs = require('../chunk-OC4LQ2CK.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 = _chunkOC4LQ2CKcjs.buildApp; exports.defineAppConfig = _chunkOC4LQ2CKcjs.defineAppConfig; exports.devApp = _chunkOC4LQ2CKcjs.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-FMRORLLE.js";
6
6
  import "../chunk-RG7ZV5BD.js";
7
7
  import "../chunk-TPBI6TOU.js";
8
8
  import "../chunk-6FNC3XMI.js";
@@ -0,0 +1,227 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
2
+
3
+ var _chunkT5QVCXVBcjs = require('./chunk-T5QVCXVB.cjs');
4
+
5
+ // src/ops/index.ts
6
+ var _http = require('http');
7
+ var _client = require('@prometheus-io/client');
8
+
9
+ // src/ops/metrics.ts
10
+
11
+
12
+ // src/ops/runtime.ts
13
+ var _cluster = require('cluster'); var _cluster2 = _interopRequireDefault(_cluster);
14
+
15
+ function collectRuntime(registry, appName) {
16
+ registry.setDefaultLabels({ app: appName, process_role: _cluster2.default.isWorker ? "worker" : "main" });
17
+ _client.collectDefaultMetrics.call(void 0, { register: registry });
18
+ let previousCpu = process.cpuUsage();
19
+ let previousTime = performance.now();
20
+ new (0, _client.Gauge)({
21
+ aggregator: "sum",
22
+ collect() {
23
+ const currentCpu = process.cpuUsage();
24
+ const currentTime = performance.now();
25
+ const elapsed = currentTime - previousTime;
26
+ this.set(
27
+ elapsed > 0 ? (currentCpu.user - previousCpu.user + currentCpu.system - previousCpu.system) / (elapsed * 1e3) : 0
28
+ );
29
+ previousCpu = currentCpu;
30
+ previousTime = currentTime;
31
+ },
32
+ help: "CPU time divided by elapsed wall time since the last scrape",
33
+ name: "process_cpu_utilization_ratio",
34
+ registers: [registry]
35
+ });
36
+ new (0, _client.Gauge)({
37
+ collect() {
38
+ this.set(process.uptime());
39
+ },
40
+ help: "Process uptime in seconds",
41
+ name: "process_uptime_seconds",
42
+ registers: [registry]
43
+ });
44
+ new (0, _client.Gauge)({
45
+ collect() {
46
+ this.set(process.memoryUsage().arrayBuffers);
47
+ },
48
+ help: "Memory used by ArrayBuffers in bytes",
49
+ name: "nodejs_arraybuffer_memory_bytes",
50
+ registers: [registry]
51
+ });
52
+ new (0, _client.Gauge)({
53
+ aggregator: "max",
54
+ collect() {
55
+ this.set(process.resourceUsage().maxRSS * 1024);
56
+ },
57
+ help: "Maximum resident memory in bytes",
58
+ name: "process_max_resident_memory_bytes",
59
+ registers: [registry]
60
+ });
61
+ for (const [name, field] of [
62
+ ["minor_page_faults", "minorPageFault"],
63
+ ["major_page_faults", "majorPageFault"],
64
+ ["filesystem_reads", "fsRead"],
65
+ ["filesystem_writes", "fsWrite"],
66
+ ["voluntary_context_switches", "voluntaryContextSwitches"],
67
+ ["involuntary_context_switches", "involuntaryContextSwitches"]
68
+ ]) {
69
+ new (0, _client.Gauge)({
70
+ collect() {
71
+ this.set(process.resourceUsage()[field]);
72
+ },
73
+ help: `Process ${name} since startup`,
74
+ name: `process_${name}_total`,
75
+ registers: [registry]
76
+ });
77
+ }
78
+ }
79
+
80
+ // src/ops/metrics.ts
81
+ var HTTP_LABELS = ["method", "route", "status"];
82
+ var SECOND_BUCKETS = [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30];
83
+ var ProcessMetrics = class {
84
+ constructor() {
85
+ this.registry = new (0, _client.Registry)();
86
+ this.runtimeStarted = false;
87
+ }
88
+ counter(name, labelNames = []) {
89
+ const existing = this.registry.getSingleMetric(name);
90
+ if (existing) {
91
+ return existing;
92
+ }
93
+ return new (0, _client.Counter)({ help: name, labelNames, name, registers: [this.registry] });
94
+ }
95
+ histogram(name, labelNames = []) {
96
+ const existing = this.registry.getSingleMetric(name);
97
+ if (existing) {
98
+ return existing;
99
+ }
100
+ return new (0, _client.Histogram)({ buckets: SECOND_BUCKETS, help: name, labelNames, name, registers: [this.registry] });
101
+ }
102
+ /** Полный штатный набор Node/process-метрик клиента и дополнительные данные процесса. */
103
+ startRuntime(appName) {
104
+ if (this.runtimeStarted) {
105
+ return;
106
+ }
107
+ this.runtimeStarted = true;
108
+ collectRuntime(this.registry, appName);
109
+ }
110
+ /** HTTP-измерения с ограниченными метками маршрута; вызывается один раз на запрос. */
111
+ trackHttp(req, res, route) {
112
+ const active = this.registry.getSingleMetric("http_requests_in_flight");
113
+ const gauge = active != null ? active : new (0, _client.Gauge)({
114
+ help: "HTTP requests currently being served",
115
+ name: "http_requests_in_flight",
116
+ registers: [this.registry]
117
+ });
118
+ const started = performance.now();
119
+ gauge.inc();
120
+ let finished = false;
121
+ const finish = () => {
122
+ var _a;
123
+ if (finished) {
124
+ return;
125
+ }
126
+ finished = true;
127
+ gauge.dec();
128
+ const labels = { method: (_a = req.method) != null ? _a : "UNKNOWN", route: route(), status: String(res.statusCode) };
129
+ this.counter("http_requests_total", HTTP_LABELS).inc(labels);
130
+ this.histogram("http_request_duration_seconds", HTTP_LABELS).observe(labels, (performance.now() - started) / 1e3);
131
+ };
132
+ res.once("finish", finish);
133
+ res.once("close", finish);
134
+ }
135
+ };
136
+ var PROCESS_METRICS = /* @__PURE__ */ Symbol.for("@foxford/den/process-metrics");
137
+ var storage = globalThis;
138
+ function getProcessMetrics() {
139
+ var _a;
140
+ return (_a = storage[PROCESS_METRICS]) != null ? _a : storage[PROCESS_METRICS] = new ProcessMetrics();
141
+ }
142
+
143
+ // src/ops/index.ts
144
+ var RESERVED_PATHS = /* @__PURE__ */ new Set(["/healthz", "/livez", "/metrics"]);
145
+ function isOpsPath(url = "/") {
146
+ var _a;
147
+ return RESERVED_PATHS.has((_a = url.split("?")[0]) != null ? _a : "/");
148
+ }
149
+ function clusterMetrics(metrics = getProcessMetrics()) {
150
+ const aggregator = new (0, _client.ClusterRegistry)();
151
+ _client.ClusterRegistry.setRegistries(metrics.registry);
152
+ const lifecycle = aggregator;
153
+ return {
154
+ collect: () => aggregator.clusterMetrics(),
155
+ contentType: metrics.registry.contentType,
156
+ shutdown: () => lifecycle.shutdown()
157
+ };
158
+ }
159
+ function startOpsServer() {
160
+ return _chunkT5QVCXVBcjs.__async.call(void 0, this, arguments, function* (options = {}) {
161
+ var _a;
162
+ const metrics = (_a = options.metrics) != null ? _a : getProcessMetrics();
163
+ const server = _http.createServer.call(void 0, (req, res) => {
164
+ var _a2, _b, _c, _d;
165
+ const path = (_a2 = req.url) == null ? void 0 : _a2.split("?")[0];
166
+ res.setHeader("cache-control", "no-store");
167
+ if (req.method !== "GET" || !["/healthz", "/livez", "/metrics"].includes(path != null ? path : "")) {
168
+ res.writeHead(404).end();
169
+ return;
170
+ }
171
+ if (path !== "/metrics") {
172
+ res.writeHead(path === "/livez" && ((_b = options.isLive) == null ? void 0 : _b.call(options)) === false ? 503 : 200, {
173
+ "content-type": "text/plain; charset=utf-8"
174
+ }).end("ok");
175
+ return;
176
+ }
177
+ void ((_d = (_c = options.collect) == null ? void 0 : _c.call(options)) != null ? _d : metrics.registry.metrics()).then((body) => {
178
+ var _a3;
179
+ if (!res.destroyed) {
180
+ res.writeHead(200, { "content-type": (_a3 = options.contentType) != null ? _a3 : metrics.registry.contentType }).end(body);
181
+ }
182
+ }).catch(() => {
183
+ if (!res.destroyed) {
184
+ res.writeHead(503).end("metrics unavailable");
185
+ }
186
+ });
187
+ });
188
+ yield new Promise((resolve, reject) => {
189
+ var _a2, _b;
190
+ server.once("error", reject);
191
+ server.listen((_a2 = options.port) != null ? _a2 : 9090, (_b = options.host) != null ? _b : "0.0.0.0", () => {
192
+ server.removeListener("error", reject);
193
+ resolve();
194
+ });
195
+ });
196
+ return server;
197
+ });
198
+ }
199
+ function closeHttpServer(server, timeoutMs = 5e3) {
200
+ return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
201
+ if (!server.listening) {
202
+ return;
203
+ }
204
+ yield new Promise((resolve, reject) => {
205
+ const timer = setTimeout(() => server.closeAllConnections(), timeoutMs);
206
+ timer.unref();
207
+ server.close((error) => {
208
+ clearTimeout(timer);
209
+ if (error) {
210
+ reject(error);
211
+ } else {
212
+ resolve();
213
+ }
214
+ });
215
+ server.closeIdleConnections();
216
+ });
217
+ });
218
+ }
219
+
220
+
221
+
222
+
223
+
224
+
225
+
226
+
227
+ exports.ProcessMetrics = ProcessMetrics; exports.getProcessMetrics = getProcessMetrics; exports.isOpsPath = isOpsPath; exports.clusterMetrics = clusterMetrics; exports.startOpsServer = startOpsServer; exports.closeHttpServer = closeHttpServer;
@@ -0,0 +1,227 @@
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 { collectDefaultMetrics, Gauge } from "@prometheus-io/client";
15
+ function collectRuntime(registry, appName) {
16
+ registry.setDefaultLabels({ app: appName, process_role: cluster.isWorker ? "worker" : "main" });
17
+ collectDefaultMetrics({ register: registry });
18
+ let previousCpu = process.cpuUsage();
19
+ let previousTime = performance.now();
20
+ new Gauge({
21
+ aggregator: "sum",
22
+ collect() {
23
+ const currentCpu = process.cpuUsage();
24
+ const currentTime = performance.now();
25
+ const elapsed = currentTime - previousTime;
26
+ this.set(
27
+ elapsed > 0 ? (currentCpu.user - previousCpu.user + currentCpu.system - previousCpu.system) / (elapsed * 1e3) : 0
28
+ );
29
+ previousCpu = currentCpu;
30
+ previousTime = currentTime;
31
+ },
32
+ help: "CPU time divided by elapsed wall time since the last scrape",
33
+ name: "process_cpu_utilization_ratio",
34
+ registers: [registry]
35
+ });
36
+ new Gauge({
37
+ collect() {
38
+ this.set(process.uptime());
39
+ },
40
+ help: "Process uptime in seconds",
41
+ name: "process_uptime_seconds",
42
+ registers: [registry]
43
+ });
44
+ new Gauge({
45
+ collect() {
46
+ this.set(process.memoryUsage().arrayBuffers);
47
+ },
48
+ help: "Memory used by ArrayBuffers in bytes",
49
+ name: "nodejs_arraybuffer_memory_bytes",
50
+ registers: [registry]
51
+ });
52
+ new Gauge({
53
+ aggregator: "max",
54
+ collect() {
55
+ this.set(process.resourceUsage().maxRSS * 1024);
56
+ },
57
+ help: "Maximum resident memory in bytes",
58
+ name: "process_max_resident_memory_bytes",
59
+ registers: [registry]
60
+ });
61
+ for (const [name, field] of [
62
+ ["minor_page_faults", "minorPageFault"],
63
+ ["major_page_faults", "majorPageFault"],
64
+ ["filesystem_reads", "fsRead"],
65
+ ["filesystem_writes", "fsWrite"],
66
+ ["voluntary_context_switches", "voluntaryContextSwitches"],
67
+ ["involuntary_context_switches", "involuntaryContextSwitches"]
68
+ ]) {
69
+ new Gauge({
70
+ collect() {
71
+ this.set(process.resourceUsage()[field]);
72
+ },
73
+ help: `Process ${name} since startup`,
74
+ name: `process_${name}_total`,
75
+ registers: [registry]
76
+ });
77
+ }
78
+ }
79
+
80
+ // src/ops/metrics.ts
81
+ var HTTP_LABELS = ["method", "route", "status"];
82
+ var SECOND_BUCKETS = [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30];
83
+ var ProcessMetrics = class {
84
+ constructor() {
85
+ this.registry = new Registry();
86
+ this.runtimeStarted = false;
87
+ }
88
+ counter(name, labelNames = []) {
89
+ const existing = this.registry.getSingleMetric(name);
90
+ if (existing) {
91
+ return existing;
92
+ }
93
+ return new Counter({ help: name, labelNames, name, registers: [this.registry] });
94
+ }
95
+ histogram(name, labelNames = []) {
96
+ const existing = this.registry.getSingleMetric(name);
97
+ if (existing) {
98
+ return existing;
99
+ }
100
+ return new Histogram({ buckets: SECOND_BUCKETS, help: name, labelNames, name, registers: [this.registry] });
101
+ }
102
+ /** Полный штатный набор Node/process-метрик клиента и дополнительные данные процесса. */
103
+ startRuntime(appName) {
104
+ if (this.runtimeStarted) {
105
+ return;
106
+ }
107
+ this.runtimeStarted = true;
108
+ collectRuntime(this.registry, appName);
109
+ }
110
+ /** HTTP-измерения с ограниченными метками маршрута; вызывается один раз на запрос. */
111
+ trackHttp(req, res, route) {
112
+ const active = this.registry.getSingleMetric("http_requests_in_flight");
113
+ const gauge = active != null ? active : new Gauge2({
114
+ help: "HTTP requests currently being served",
115
+ name: "http_requests_in_flight",
116
+ registers: [this.registry]
117
+ });
118
+ const started = performance.now();
119
+ gauge.inc();
120
+ let finished = false;
121
+ const finish = () => {
122
+ var _a;
123
+ if (finished) {
124
+ return;
125
+ }
126
+ finished = true;
127
+ gauge.dec();
128
+ const labels = { method: (_a = req.method) != null ? _a : "UNKNOWN", route: route(), status: String(res.statusCode) };
129
+ this.counter("http_requests_total", HTTP_LABELS).inc(labels);
130
+ this.histogram("http_request_duration_seconds", HTTP_LABELS).observe(labels, (performance.now() - started) / 1e3);
131
+ };
132
+ res.once("finish", finish);
133
+ res.once("close", finish);
134
+ }
135
+ };
136
+ var PROCESS_METRICS = /* @__PURE__ */ Symbol.for("@foxford/den/process-metrics");
137
+ var storage = globalThis;
138
+ function getProcessMetrics() {
139
+ var _a;
140
+ return (_a = storage[PROCESS_METRICS]) != null ? _a : storage[PROCESS_METRICS] = new ProcessMetrics();
141
+ }
142
+
143
+ // src/ops/index.ts
144
+ var RESERVED_PATHS = /* @__PURE__ */ new Set(["/healthz", "/livez", "/metrics"]);
145
+ function isOpsPath(url = "/") {
146
+ var _a;
147
+ return RESERVED_PATHS.has((_a = url.split("?")[0]) != null ? _a : "/");
148
+ }
149
+ function clusterMetrics(metrics = getProcessMetrics()) {
150
+ const aggregator = new ClusterRegistry();
151
+ ClusterRegistry.setRegistries(metrics.registry);
152
+ const lifecycle = aggregator;
153
+ return {
154
+ collect: () => aggregator.clusterMetrics(),
155
+ contentType: metrics.registry.contentType,
156
+ shutdown: () => lifecycle.shutdown()
157
+ };
158
+ }
159
+ function startOpsServer() {
160
+ return __async(this, arguments, function* (options = {}) {
161
+ var _a;
162
+ const metrics = (_a = options.metrics) != null ? _a : getProcessMetrics();
163
+ const server = createServer((req, res) => {
164
+ var _a2, _b, _c, _d;
165
+ const path = (_a2 = req.url) == null ? void 0 : _a2.split("?")[0];
166
+ res.setHeader("cache-control", "no-store");
167
+ if (req.method !== "GET" || !["/healthz", "/livez", "/metrics"].includes(path != null ? path : "")) {
168
+ res.writeHead(404).end();
169
+ return;
170
+ }
171
+ if (path !== "/metrics") {
172
+ res.writeHead(path === "/livez" && ((_b = options.isLive) == null ? void 0 : _b.call(options)) === false ? 503 : 200, {
173
+ "content-type": "text/plain; charset=utf-8"
174
+ }).end("ok");
175
+ return;
176
+ }
177
+ void ((_d = (_c = options.collect) == null ? void 0 : _c.call(options)) != null ? _d : metrics.registry.metrics()).then((body) => {
178
+ var _a3;
179
+ if (!res.destroyed) {
180
+ res.writeHead(200, { "content-type": (_a3 = options.contentType) != null ? _a3 : metrics.registry.contentType }).end(body);
181
+ }
182
+ }).catch(() => {
183
+ if (!res.destroyed) {
184
+ res.writeHead(503).end("metrics unavailable");
185
+ }
186
+ });
187
+ });
188
+ yield new Promise((resolve, reject) => {
189
+ var _a2, _b;
190
+ server.once("error", reject);
191
+ server.listen((_a2 = options.port) != null ? _a2 : 9090, (_b = options.host) != null ? _b : "0.0.0.0", () => {
192
+ server.removeListener("error", reject);
193
+ resolve();
194
+ });
195
+ });
196
+ return server;
197
+ });
198
+ }
199
+ function closeHttpServer(server, timeoutMs = 5e3) {
200
+ return __async(this, null, function* () {
201
+ if (!server.listening) {
202
+ return;
203
+ }
204
+ yield new Promise((resolve, reject) => {
205
+ const timer = setTimeout(() => server.closeAllConnections(), timeoutMs);
206
+ timer.unref();
207
+ server.close((error) => {
208
+ clearTimeout(timer);
209
+ if (error) {
210
+ reject(error);
211
+ } else {
212
+ resolve();
213
+ }
214
+ });
215
+ server.closeIdleConnections();
216
+ });
217
+ });
218
+ }
219
+
220
+ export {
221
+ ProcessMetrics,
222
+ getProcessMetrics,
223
+ isOpsPath,
224
+ clusterMetrics,
225
+ startOpsServer,
226
+ closeHttpServer
227
+ };
@@ -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);
@@ -145,7 +145,8 @@ loadAppEnv(fileURLToPath(new URL('..', import.meta.url)))
145
145
  await serveApp(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)}),
149
150
  })
150
151
  `;
151
152
  }
@@ -201,7 +202,8 @@ function loadAppConfig(root) {
201
202
  configFile: false,
202
203
  logLevel: "silent",
203
204
  root,
204
- server: { middlewareMode: true }
205
+ // Этот сервер только загружает конфиг; пересборкой в dev управляет watchSources.
206
+ server: { middlewareMode: true, watch: null }
205
207
  });
206
208
  try {
207
209
  const loaded = yield server.ssrLoadModule(configPath);
@@ -324,7 +326,7 @@ import { pathToFileURL } from "url";
324
326
  import os from "os";
325
327
  var TOOL = "den";
326
328
  var PAD = 11;
327
- var ENDPOINTS = ["/_render?url=<\u043F\u0443\u0442\u044C>", "/_routes", "/_health"];
329
+ var ENDPOINTS = ["/_render?url=<\u043F\u0443\u0442\u044C>", "/_routes"];
328
330
  var OUTPUTS = ["build/client", "build/server", "build/server.mjs"];
329
331
  var SHORTCUTS = [
330
332
  { key: "r", title: "\u043F\u0435\u0440\u0435\u0441\u043E\u0431\u0440\u0430\u0442\u044C" },