@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.
- package/README.mdx +31 -6
- 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-2P5TAWRW.cjs +227 -0
- package/chunk-DUU4MQ4B.js +227 -0
- package/{chunk-2GTQK2PZ.js → chunk-FMRORLLE.js} +6 -4
- package/{chunk-P4J4M2WZ.cjs → chunk-OC4LQ2CK.cjs} +6 -4
- 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 +70 -16
- package/serve/index.d.cts +10 -5
- package/serve/index.d.ts +10 -5
- package/serve/index.js +71 -17
- 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
|
```
|
|
@@ -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 /
|
|
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
|
-
|
|
206
|
-
`
|
|
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
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 _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
|
|
73
|
+
yield _chunkOC4LQ2CKcjs.buildApp.call(void 0, target);
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
if (command === "dev") {
|
|
77
|
-
yield
|
|
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
package/builder/index.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
var
|
|
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 =
|
|
13
|
+
exports.buildApp = _chunkOC4LQ2CKcjs.buildApp; exports.defineAppConfig = _chunkOC4LQ2CKcjs.defineAppConfig; exports.devApp = _chunkOC4LQ2CKcjs.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
|
@@ -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 =
|
|
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.
|
|
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
|
-
|
|
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"
|
|
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" },
|