@foxford/den-traces 0.1.0-beta-3355e5f3-20260907
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 +55 -0
- package/chunk-F4ITEBDQ.cjs +73 -0
- package/chunk-HN4BBSRG.js +73 -0
- package/index.cjs +97 -0
- package/index.d.cts +90 -0
- package/index.d.ts +90 -0
- package/index.js +97 -0
- package/package.json +64 -0
- package/timings/index.cjs +14 -0
- package/timings/index.d.cts +48 -0
- package/timings/index.d.ts +48 -0
- package/timings/index.js +14 -0
package/README.mdx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Den Traces
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# @foxford/den-traces
|
|
6
|
+
|
|
7
|
+
Метки времени, походы наружу и дев-трасса для den-приложений.
|
|
8
|
+
|
|
9
|
+
**Спека пакета — [SPEC.md](SPEC.md)** (обязательна при правках). Ниже — быстрый старт.
|
|
10
|
+
|
|
11
|
+
## Подключение
|
|
12
|
+
|
|
13
|
+
1. Зависимость в приложении:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
"@foxford/den-traces": "^0.1.0"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. Вызов в `den.config.ts`:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { defineAppConfig } from '@foxford/den/build'
|
|
23
|
+
import { describeDeclarations, installServerTrace } from '@foxford/den-traces'
|
|
24
|
+
import { installServerSentry } from '@foxford/den-sentry'
|
|
25
|
+
|
|
26
|
+
export default defineAppConfig({
|
|
27
|
+
server: {
|
|
28
|
+
configure: (instance) => {
|
|
29
|
+
installServerTrace(instance, {
|
|
30
|
+
describe: async () => {
|
|
31
|
+
const { declarations } = await import('./src/declarations')
|
|
32
|
+
return describeDeclarations(declarations)
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
installServerSentry(instance, { appName: 'my-app', dsn: process.env.SENTRY_DSN })
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. Импорты меток и походов — из подпути `./timings` (без серверных зависимостей):
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { measure, trackFetch } from '@foxford/den-traces/timings'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`installServerTrace` ставит `setTimingSink` / `setRequestSink`. Приёмник upstream-ошибок
|
|
48
|
+
(`setUpstreamErrorSink`) цепляет `installServerSentry`.
|
|
49
|
+
|
|
50
|
+
## Экспорты
|
|
51
|
+
|
|
52
|
+
| Путь | Содержимое |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `@foxford/den-traces` | `installServerTrace`, `recordServerTiming`, `describeDeclarations`, sinks |
|
|
55
|
+
| `@foxford/den-traces/timings` | `measure`, `trackFetch`, `setTimingSink`, `setRequestSink`, `setUpstreamErrorSink` |
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/timings/index.ts
|
|
2
|
+
var sink = null;
|
|
3
|
+
var requestSink = null;
|
|
4
|
+
var upstreamErrorSink = null;
|
|
5
|
+
function setTimingSink(next) {
|
|
6
|
+
sink = next;
|
|
7
|
+
}
|
|
8
|
+
function setRequestSink(next) {
|
|
9
|
+
requestSink = next;
|
|
10
|
+
}
|
|
11
|
+
function setUpstreamErrorSink(next) {
|
|
12
|
+
upstreamErrorSink = next;
|
|
13
|
+
}
|
|
14
|
+
async function measure(name, work) {
|
|
15
|
+
if (sink === null) {
|
|
16
|
+
return work();
|
|
17
|
+
}
|
|
18
|
+
const startedAt = performance.now();
|
|
19
|
+
try {
|
|
20
|
+
return await work();
|
|
21
|
+
} finally {
|
|
22
|
+
sink(name, performance.now() - startedAt, startedAt);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function trackFetch(name, url, init) {
|
|
26
|
+
const startedAt = performance.now();
|
|
27
|
+
try {
|
|
28
|
+
const response = await measure(name, () => fetch(url, init));
|
|
29
|
+
const length = response.headers.get("content-length");
|
|
30
|
+
_optionalChain([requestSink, 'optionalCall', _ => _({
|
|
31
|
+
bytes: length === null ? null : Number(length),
|
|
32
|
+
ms: performance.now() - startedAt,
|
|
33
|
+
name,
|
|
34
|
+
startedAt,
|
|
35
|
+
status: response.status,
|
|
36
|
+
url
|
|
37
|
+
})]);
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
_optionalChain([upstreamErrorSink, 'optionalCall', _2 => _2({
|
|
40
|
+
name,
|
|
41
|
+
reason: `http_${String(response.status)}`,
|
|
42
|
+
status: response.status,
|
|
43
|
+
url
|
|
44
|
+
})]);
|
|
45
|
+
}
|
|
46
|
+
return response;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
_optionalChain([requestSink, 'optionalCall', _3 => _3({
|
|
49
|
+
bytes: null,
|
|
50
|
+
error: String(error),
|
|
51
|
+
ms: performance.now() - startedAt,
|
|
52
|
+
name,
|
|
53
|
+
startedAt,
|
|
54
|
+
status: null,
|
|
55
|
+
url
|
|
56
|
+
})]);
|
|
57
|
+
_optionalChain([upstreamErrorSink, 'optionalCall', _4 => _4({
|
|
58
|
+
name,
|
|
59
|
+
reason: "network",
|
|
60
|
+
status: null,
|
|
61
|
+
url
|
|
62
|
+
})]);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
exports.setTimingSink = setTimingSink; exports.setRequestSink = setRequestSink; exports.setUpstreamErrorSink = setUpstreamErrorSink; exports.measure = measure; exports.trackFetch = trackFetch;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/timings/index.ts
|
|
2
|
+
var sink = null;
|
|
3
|
+
var requestSink = null;
|
|
4
|
+
var upstreamErrorSink = null;
|
|
5
|
+
function setTimingSink(next) {
|
|
6
|
+
sink = next;
|
|
7
|
+
}
|
|
8
|
+
function setRequestSink(next) {
|
|
9
|
+
requestSink = next;
|
|
10
|
+
}
|
|
11
|
+
function setUpstreamErrorSink(next) {
|
|
12
|
+
upstreamErrorSink = next;
|
|
13
|
+
}
|
|
14
|
+
async function measure(name, work) {
|
|
15
|
+
if (sink === null) {
|
|
16
|
+
return work();
|
|
17
|
+
}
|
|
18
|
+
const startedAt = performance.now();
|
|
19
|
+
try {
|
|
20
|
+
return await work();
|
|
21
|
+
} finally {
|
|
22
|
+
sink(name, performance.now() - startedAt, startedAt);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function trackFetch(name, url, init) {
|
|
26
|
+
const startedAt = performance.now();
|
|
27
|
+
try {
|
|
28
|
+
const response = await measure(name, () => fetch(url, init));
|
|
29
|
+
const length = response.headers.get("content-length");
|
|
30
|
+
requestSink?.({
|
|
31
|
+
bytes: length === null ? null : Number(length),
|
|
32
|
+
ms: performance.now() - startedAt,
|
|
33
|
+
name,
|
|
34
|
+
startedAt,
|
|
35
|
+
status: response.status,
|
|
36
|
+
url
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
upstreamErrorSink?.({
|
|
40
|
+
name,
|
|
41
|
+
reason: `http_${String(response.status)}`,
|
|
42
|
+
status: response.status,
|
|
43
|
+
url
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return response;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
requestSink?.({
|
|
49
|
+
bytes: null,
|
|
50
|
+
error: String(error),
|
|
51
|
+
ms: performance.now() - startedAt,
|
|
52
|
+
name,
|
|
53
|
+
startedAt,
|
|
54
|
+
status: null,
|
|
55
|
+
url
|
|
56
|
+
});
|
|
57
|
+
upstreamErrorSink?.({
|
|
58
|
+
name,
|
|
59
|
+
reason: "network",
|
|
60
|
+
status: null,
|
|
61
|
+
url
|
|
62
|
+
});
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
setTimingSink,
|
|
69
|
+
setRequestSink,
|
|
70
|
+
setUpstreamErrorSink,
|
|
71
|
+
measure,
|
|
72
|
+
trackFetch
|
|
73
|
+
};
|
package/index.cjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
var _chunkF4ITEBDQcjs = require('./chunk-F4ITEBDQ.cjs');
|
|
8
|
+
|
|
9
|
+
// src/server/install.ts
|
|
10
|
+
var _async_hooks = require('async_hooks');
|
|
11
|
+
|
|
12
|
+
// src/server/trace.ts
|
|
13
|
+
function nameOf(named) {
|
|
14
|
+
return _nullishCoalesce(named.token.description, () => ( "?"));
|
|
15
|
+
}
|
|
16
|
+
function describeDeclarations(app) {
|
|
17
|
+
return {
|
|
18
|
+
guards: (_nullishCoalesce(app.guards, () => ( []))).map(nameOf),
|
|
19
|
+
mountsInto: app.mountsInto,
|
|
20
|
+
name: app.name,
|
|
21
|
+
provides: (_nullishCoalesce(app.provides, () => ( []))).map(nameOf),
|
|
22
|
+
requires: (_nullishCoalesce(app.requires, () => ( []))).map((token) => token.description),
|
|
23
|
+
routes: (_nullishCoalesce(app.routes, () => ( []))).map(
|
|
24
|
+
(route) => typeof route === "string" ? { guards: [], source: route } : { guards: (_nullishCoalesce(route.guards, () => ( []))).map(nameOf), source: route.source }
|
|
25
|
+
),
|
|
26
|
+
slots: Object.keys(_nullishCoalesce(app.slots, () => ( {}))),
|
|
27
|
+
units: (_nullishCoalesce(app.units, () => ( []))).map(nameOf),
|
|
28
|
+
viewModels: (_nullishCoalesce(app.viewModels, () => ( []))).map(nameOf)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
var TRACE_LIMIT_BYTES = 8192;
|
|
32
|
+
function traceHeader(requests, startedAt) {
|
|
33
|
+
const shift = (request) => ({
|
|
34
|
+
bytes: request.bytes,
|
|
35
|
+
ms: Number(request.ms.toFixed(1)),
|
|
36
|
+
name: request.name,
|
|
37
|
+
startMs: Number((request.startedAt - startedAt).toFixed(1)),
|
|
38
|
+
status: request.status,
|
|
39
|
+
url: request.url
|
|
40
|
+
});
|
|
41
|
+
let kept = requests.map(shift);
|
|
42
|
+
let encoded = Buffer.from(JSON.stringify({ requests: kept })).toString("base64");
|
|
43
|
+
while (encoded.length > TRACE_LIMIT_BYTES && kept.length > 0) {
|
|
44
|
+
kept = kept.slice(1);
|
|
45
|
+
encoded = Buffer.from(JSON.stringify({ requests: kept, truncated: true })).toString("base64");
|
|
46
|
+
}
|
|
47
|
+
return encoded;
|
|
48
|
+
}
|
|
49
|
+
function serverTimingHeader(marks) {
|
|
50
|
+
return marks.map(({ name, ms, startMs }) => `${name};dur=${ms.toFixed(1)};desc="@${startMs.toFixed(1)}"`).join(", ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/server/install.ts
|
|
54
|
+
var TRACE_HEADER = "x-den-trace";
|
|
55
|
+
var storage = new (0, _async_hooks.AsyncLocalStorage)();
|
|
56
|
+
function recordServerTiming(name, ms, startedAt) {
|
|
57
|
+
const timings = storage.getStore();
|
|
58
|
+
_optionalChain([timings, 'optionalAccess', _ => _.marks, 'access', _2 => _2.push, 'call', _3 => _3({ ms, name, startMs: (_nullishCoalesce(startedAt, () => ( performance.now() - ms))) - timings.startedAt })]);
|
|
59
|
+
}
|
|
60
|
+
function asked(headers) {
|
|
61
|
+
return headers[TRACE_HEADER] !== void 0;
|
|
62
|
+
}
|
|
63
|
+
function installServerTrace(instance, options = {}) {
|
|
64
|
+
_chunkF4ITEBDQcjs.setTimingSink.call(void 0, recordServerTiming);
|
|
65
|
+
_chunkF4ITEBDQcjs.setRequestSink.call(void 0, (request) => _optionalChain([storage, 'access', _4 => _4.getStore, 'call', _5 => _5(), 'optionalAccess', _6 => _6.requests, 'access', _7 => _7.push, 'call', _8 => _8(request)]));
|
|
66
|
+
instance.addHook("onRequest", (_request, _reply, done) => {
|
|
67
|
+
storage.run({ marks: [], requests: [], startedAt: performance.now() }, done);
|
|
68
|
+
});
|
|
69
|
+
instance.addHook("onSend", (request, reply, payload, done) => {
|
|
70
|
+
const timings = storage.getStore();
|
|
71
|
+
if (timings !== void 0 && timings.marks.length > 0) {
|
|
72
|
+
reply.header("server-timing", serverTimingHeader(timings.marks));
|
|
73
|
+
}
|
|
74
|
+
if (timings !== void 0 && asked(request.headers)) {
|
|
75
|
+
reply.header(TRACE_HEADER, traceHeader(timings.requests, timings.startedAt));
|
|
76
|
+
}
|
|
77
|
+
done(null, payload);
|
|
78
|
+
});
|
|
79
|
+
if (options.describe !== void 0 && instance.get !== void 0) {
|
|
80
|
+
const describe = options.describe;
|
|
81
|
+
instance.get(
|
|
82
|
+
"/_describe",
|
|
83
|
+
async (request, reply) => asked(request.headers) ? reply.code(200).send(await describe()) : reply.code(404).send()
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
exports.describeDeclarations = describeDeclarations; exports.installServerTrace = installServerTrace; exports.measure = _chunkF4ITEBDQcjs.measure; exports.recordServerTiming = recordServerTiming; exports.setRequestSink = _chunkF4ITEBDQcjs.setRequestSink; exports.setTimingSink = _chunkF4ITEBDQcjs.setTimingSink; exports.setUpstreamErrorSink = _chunkF4ITEBDQcjs.setUpstreamErrorSink; exports.traceHeader = traceHeader; exports.trackFetch = _chunkF4ITEBDQcjs.trackFetch;
|
package/index.d.cts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { TrackedRequest } from './timings/index.cjs';
|
|
2
|
+
export { RequestSink, TimingSink, UpstreamErrorContext, UpstreamErrorSink, measure, setRequestSink, setTimingSink, setUpstreamErrorSink, trackFetch } from './timings/index.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Хуки инстанса, которыми пользуется `@foxford/den`.
|
|
6
|
+
*
|
|
7
|
+
* Форма минимальная, а не `FastifyInstance`: сервер приложению даёт den, и fastify в
|
|
8
|
+
* зависимостях пакета не нужен.
|
|
9
|
+
*/
|
|
10
|
+
interface DenHookHost {
|
|
11
|
+
addHook(name: 'onRequest', hook: (request: unknown, reply: unknown, done: () => void) => void): unknown;
|
|
12
|
+
addHook(name: 'onSend', hook: (request: {
|
|
13
|
+
headers: Record<string, string | string[] | undefined>;
|
|
14
|
+
}, reply: {
|
|
15
|
+
header: (name: string, value: string) => unknown;
|
|
16
|
+
}, payload: unknown, done: (error: null, payload?: unknown) => void) => void): unknown;
|
|
17
|
+
get?(path: string, handler: (request: {
|
|
18
|
+
headers: Record<string, string | string[] | undefined>;
|
|
19
|
+
}, reply: {
|
|
20
|
+
code: (status: number) => {
|
|
21
|
+
send: (body?: unknown) => unknown;
|
|
22
|
+
};
|
|
23
|
+
}) => unknown): unknown;
|
|
24
|
+
}
|
|
25
|
+
interface InstallServerTraceOptions {
|
|
26
|
+
/** Ленивая отдача деклараций для `/_describe`; без неё ручка не регистрируется. */
|
|
27
|
+
describe?: () => Promise<Record<string, unknown>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Именованная штука дескриптора: единица, VM, возможность, guard — все различаются токеном. */
|
|
31
|
+
interface Named {
|
|
32
|
+
token: {
|
|
33
|
+
description?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Часть дескриптора, которую пересказывает ручка деклараций.
|
|
38
|
+
*
|
|
39
|
+
* Форма структурная, а не `AppDescriptor`: тип дескриптора параметризован узлом view-фреймворка,
|
|
40
|
+
* и, сославшись на него, серверный модуль потащил бы за собой рендерер. Пересказываются только
|
|
41
|
+
* ОБЪЯВЛЕНИЯ — ни одного поля с поведением здесь нет, и это же граница того, что вправе уехать
|
|
42
|
+
* наружу.
|
|
43
|
+
*/
|
|
44
|
+
interface Declarations {
|
|
45
|
+
name: string;
|
|
46
|
+
mountsInto?: string;
|
|
47
|
+
routes?: ReadonlyArray<string | {
|
|
48
|
+
source: string;
|
|
49
|
+
guards?: ReadonlyArray<Named>;
|
|
50
|
+
}>;
|
|
51
|
+
slots?: Record<string, unknown>;
|
|
52
|
+
units?: ReadonlyArray<Named>;
|
|
53
|
+
viewModels?: ReadonlyArray<Named>;
|
|
54
|
+
requires?: ReadonlyArray<{
|
|
55
|
+
description?: string;
|
|
56
|
+
}>;
|
|
57
|
+
provides?: ReadonlyArray<Named>;
|
|
58
|
+
guards?: ReadonlyArray<Named>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Декларации дескриптора данными — то, чего у хоста нет и быть не может.
|
|
62
|
+
*
|
|
63
|
+
* @param app - Дескриптор приложения
|
|
64
|
+
*/
|
|
65
|
+
declare function describeDeclarations(app: Declarations): Record<string, unknown>;
|
|
66
|
+
/**
|
|
67
|
+
* Дев-трасса запроса для заголовка: base64 от JSON.
|
|
68
|
+
*
|
|
69
|
+
* @param requests - Походы приложения за время запроса
|
|
70
|
+
* @param startedAt - Начало запроса по часам процесса
|
|
71
|
+
*/
|
|
72
|
+
declare function traceHeader(requests: readonly TrackedRequest[], startedAt: number): string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Записывает метку синхронной работы — рендера и всего, что меряется чужими часами.
|
|
76
|
+
*
|
|
77
|
+
* @param name - Имя метки
|
|
78
|
+
* @param ms - Длительность
|
|
79
|
+
* @param startedAt - Момент начала по часам процесса; по умолчанию — конец измеряемого куска
|
|
80
|
+
*/
|
|
81
|
+
declare function recordServerTiming(name: string, ms: number, startedAt?: number): void;
|
|
82
|
+
/**
|
|
83
|
+
* Заводит сбор на инстансе приложения: метки, походы наружу и ручку деклараций.
|
|
84
|
+
*
|
|
85
|
+
* @param instance - Инстанс, на котором подняты ручки приложения
|
|
86
|
+
* @param options - Ленивая отдача деклараций для `/_describe`
|
|
87
|
+
*/
|
|
88
|
+
declare function installServerTrace(instance: DenHookHost, options?: InstallServerTraceOptions): void;
|
|
89
|
+
|
|
90
|
+
export { type Declarations, type DenHookHost, type InstallServerTraceOptions, TrackedRequest, describeDeclarations, installServerTrace, recordServerTiming, traceHeader };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { TrackedRequest } from './timings/index.js';
|
|
2
|
+
export { RequestSink, TimingSink, UpstreamErrorContext, UpstreamErrorSink, measure, setRequestSink, setTimingSink, setUpstreamErrorSink, trackFetch } from './timings/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Хуки инстанса, которыми пользуется `@foxford/den`.
|
|
6
|
+
*
|
|
7
|
+
* Форма минимальная, а не `FastifyInstance`: сервер приложению даёт den, и fastify в
|
|
8
|
+
* зависимостях пакета не нужен.
|
|
9
|
+
*/
|
|
10
|
+
interface DenHookHost {
|
|
11
|
+
addHook(name: 'onRequest', hook: (request: unknown, reply: unknown, done: () => void) => void): unknown;
|
|
12
|
+
addHook(name: 'onSend', hook: (request: {
|
|
13
|
+
headers: Record<string, string | string[] | undefined>;
|
|
14
|
+
}, reply: {
|
|
15
|
+
header: (name: string, value: string) => unknown;
|
|
16
|
+
}, payload: unknown, done: (error: null, payload?: unknown) => void) => void): unknown;
|
|
17
|
+
get?(path: string, handler: (request: {
|
|
18
|
+
headers: Record<string, string | string[] | undefined>;
|
|
19
|
+
}, reply: {
|
|
20
|
+
code: (status: number) => {
|
|
21
|
+
send: (body?: unknown) => unknown;
|
|
22
|
+
};
|
|
23
|
+
}) => unknown): unknown;
|
|
24
|
+
}
|
|
25
|
+
interface InstallServerTraceOptions {
|
|
26
|
+
/** Ленивая отдача деклараций для `/_describe`; без неё ручка не регистрируется. */
|
|
27
|
+
describe?: () => Promise<Record<string, unknown>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Именованная штука дескриптора: единица, VM, возможность, guard — все различаются токеном. */
|
|
31
|
+
interface Named {
|
|
32
|
+
token: {
|
|
33
|
+
description?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Часть дескриптора, которую пересказывает ручка деклараций.
|
|
38
|
+
*
|
|
39
|
+
* Форма структурная, а не `AppDescriptor`: тип дескриптора параметризован узлом view-фреймворка,
|
|
40
|
+
* и, сославшись на него, серверный модуль потащил бы за собой рендерер. Пересказываются только
|
|
41
|
+
* ОБЪЯВЛЕНИЯ — ни одного поля с поведением здесь нет, и это же граница того, что вправе уехать
|
|
42
|
+
* наружу.
|
|
43
|
+
*/
|
|
44
|
+
interface Declarations {
|
|
45
|
+
name: string;
|
|
46
|
+
mountsInto?: string;
|
|
47
|
+
routes?: ReadonlyArray<string | {
|
|
48
|
+
source: string;
|
|
49
|
+
guards?: ReadonlyArray<Named>;
|
|
50
|
+
}>;
|
|
51
|
+
slots?: Record<string, unknown>;
|
|
52
|
+
units?: ReadonlyArray<Named>;
|
|
53
|
+
viewModels?: ReadonlyArray<Named>;
|
|
54
|
+
requires?: ReadonlyArray<{
|
|
55
|
+
description?: string;
|
|
56
|
+
}>;
|
|
57
|
+
provides?: ReadonlyArray<Named>;
|
|
58
|
+
guards?: ReadonlyArray<Named>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Декларации дескриптора данными — то, чего у хоста нет и быть не может.
|
|
62
|
+
*
|
|
63
|
+
* @param app - Дескриптор приложения
|
|
64
|
+
*/
|
|
65
|
+
declare function describeDeclarations(app: Declarations): Record<string, unknown>;
|
|
66
|
+
/**
|
|
67
|
+
* Дев-трасса запроса для заголовка: base64 от JSON.
|
|
68
|
+
*
|
|
69
|
+
* @param requests - Походы приложения за время запроса
|
|
70
|
+
* @param startedAt - Начало запроса по часам процесса
|
|
71
|
+
*/
|
|
72
|
+
declare function traceHeader(requests: readonly TrackedRequest[], startedAt: number): string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Записывает метку синхронной работы — рендера и всего, что меряется чужими часами.
|
|
76
|
+
*
|
|
77
|
+
* @param name - Имя метки
|
|
78
|
+
* @param ms - Длительность
|
|
79
|
+
* @param startedAt - Момент начала по часам процесса; по умолчанию — конец измеряемого куска
|
|
80
|
+
*/
|
|
81
|
+
declare function recordServerTiming(name: string, ms: number, startedAt?: number): void;
|
|
82
|
+
/**
|
|
83
|
+
* Заводит сбор на инстансе приложения: метки, походы наружу и ручку деклараций.
|
|
84
|
+
*
|
|
85
|
+
* @param instance - Инстанс, на котором подняты ручки приложения
|
|
86
|
+
* @param options - Ленивая отдача деклараций для `/_describe`
|
|
87
|
+
*/
|
|
88
|
+
declare function installServerTrace(instance: DenHookHost, options?: InstallServerTraceOptions): void;
|
|
89
|
+
|
|
90
|
+
export { type Declarations, type DenHookHost, type InstallServerTraceOptions, TrackedRequest, describeDeclarations, installServerTrace, recordServerTiming, traceHeader };
|
package/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
measure,
|
|
3
|
+
setRequestSink,
|
|
4
|
+
setTimingSink,
|
|
5
|
+
setUpstreamErrorSink,
|
|
6
|
+
trackFetch
|
|
7
|
+
} from "./chunk-HN4BBSRG.js";
|
|
8
|
+
|
|
9
|
+
// src/server/install.ts
|
|
10
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
11
|
+
|
|
12
|
+
// src/server/trace.ts
|
|
13
|
+
function nameOf(named) {
|
|
14
|
+
return named.token.description ?? "?";
|
|
15
|
+
}
|
|
16
|
+
function describeDeclarations(app) {
|
|
17
|
+
return {
|
|
18
|
+
guards: (app.guards ?? []).map(nameOf),
|
|
19
|
+
mountsInto: app.mountsInto,
|
|
20
|
+
name: app.name,
|
|
21
|
+
provides: (app.provides ?? []).map(nameOf),
|
|
22
|
+
requires: (app.requires ?? []).map((token) => token.description),
|
|
23
|
+
routes: (app.routes ?? []).map(
|
|
24
|
+
(route) => typeof route === "string" ? { guards: [], source: route } : { guards: (route.guards ?? []).map(nameOf), source: route.source }
|
|
25
|
+
),
|
|
26
|
+
slots: Object.keys(app.slots ?? {}),
|
|
27
|
+
units: (app.units ?? []).map(nameOf),
|
|
28
|
+
viewModels: (app.viewModels ?? []).map(nameOf)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
var TRACE_LIMIT_BYTES = 8192;
|
|
32
|
+
function traceHeader(requests, startedAt) {
|
|
33
|
+
const shift = (request) => ({
|
|
34
|
+
bytes: request.bytes,
|
|
35
|
+
ms: Number(request.ms.toFixed(1)),
|
|
36
|
+
name: request.name,
|
|
37
|
+
startMs: Number((request.startedAt - startedAt).toFixed(1)),
|
|
38
|
+
status: request.status,
|
|
39
|
+
url: request.url
|
|
40
|
+
});
|
|
41
|
+
let kept = requests.map(shift);
|
|
42
|
+
let encoded = Buffer.from(JSON.stringify({ requests: kept })).toString("base64");
|
|
43
|
+
while (encoded.length > TRACE_LIMIT_BYTES && kept.length > 0) {
|
|
44
|
+
kept = kept.slice(1);
|
|
45
|
+
encoded = Buffer.from(JSON.stringify({ requests: kept, truncated: true })).toString("base64");
|
|
46
|
+
}
|
|
47
|
+
return encoded;
|
|
48
|
+
}
|
|
49
|
+
function serverTimingHeader(marks) {
|
|
50
|
+
return marks.map(({ name, ms, startMs }) => `${name};dur=${ms.toFixed(1)};desc="@${startMs.toFixed(1)}"`).join(", ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/server/install.ts
|
|
54
|
+
var TRACE_HEADER = "x-den-trace";
|
|
55
|
+
var storage = new AsyncLocalStorage();
|
|
56
|
+
function recordServerTiming(name, ms, startedAt) {
|
|
57
|
+
const timings = storage.getStore();
|
|
58
|
+
timings?.marks.push({ ms, name, startMs: (startedAt ?? performance.now() - ms) - timings.startedAt });
|
|
59
|
+
}
|
|
60
|
+
function asked(headers) {
|
|
61
|
+
return headers[TRACE_HEADER] !== void 0;
|
|
62
|
+
}
|
|
63
|
+
function installServerTrace(instance, options = {}) {
|
|
64
|
+
setTimingSink(recordServerTiming);
|
|
65
|
+
setRequestSink((request) => storage.getStore()?.requests.push(request));
|
|
66
|
+
instance.addHook("onRequest", (_request, _reply, done) => {
|
|
67
|
+
storage.run({ marks: [], requests: [], startedAt: performance.now() }, done);
|
|
68
|
+
});
|
|
69
|
+
instance.addHook("onSend", (request, reply, payload, done) => {
|
|
70
|
+
const timings = storage.getStore();
|
|
71
|
+
if (timings !== void 0 && timings.marks.length > 0) {
|
|
72
|
+
reply.header("server-timing", serverTimingHeader(timings.marks));
|
|
73
|
+
}
|
|
74
|
+
if (timings !== void 0 && asked(request.headers)) {
|
|
75
|
+
reply.header(TRACE_HEADER, traceHeader(timings.requests, timings.startedAt));
|
|
76
|
+
}
|
|
77
|
+
done(null, payload);
|
|
78
|
+
});
|
|
79
|
+
if (options.describe !== void 0 && instance.get !== void 0) {
|
|
80
|
+
const describe = options.describe;
|
|
81
|
+
instance.get(
|
|
82
|
+
"/_describe",
|
|
83
|
+
async (request, reply) => asked(request.headers) ? reply.code(200).send(await describe()) : reply.code(404).send()
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
describeDeclarations,
|
|
89
|
+
installServerTrace,
|
|
90
|
+
measure,
|
|
91
|
+
recordServerTiming,
|
|
92
|
+
setRequestSink,
|
|
93
|
+
setTimingSink,
|
|
94
|
+
setUpstreamErrorSink,
|
|
95
|
+
traceHeader,
|
|
96
|
+
trackFetch
|
|
97
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@foxford/den-traces",
|
|
3
|
+
"version": "0.1.0-beta-3355e5f3-20260907",
|
|
4
|
+
"description": "Метки времени, походы наружу и дев-трасса для den-приложений",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"foxford",
|
|
7
|
+
"den",
|
|
8
|
+
"traces",
|
|
9
|
+
"server-timing",
|
|
10
|
+
"observability"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/foxford/frontend-platform",
|
|
13
|
+
"bugs": "https://github.com/foxford/frontend-platform/issues",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/foxford/frontend-platform.git",
|
|
17
|
+
"directory": "packages/den-traces"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Foxford Co",
|
|
21
|
+
"maintainers": [
|
|
22
|
+
{
|
|
23
|
+
"name": "Denis Riabov",
|
|
24
|
+
"email": "d.a.riabov@foxford.ru",
|
|
25
|
+
"team": "core-frontend"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "Roman Olin",
|
|
29
|
+
"email": "r.v.olin@foxford.ru",
|
|
30
|
+
"team": "core-frontend"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@foxford/den": "^3.3.0"
|
|
36
|
+
},
|
|
37
|
+
"main": "./index.cjs",
|
|
38
|
+
"module": "./index.js",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"import": "./index.js",
|
|
42
|
+
"require": "./index.cjs",
|
|
43
|
+
"types": "./index.d.ts"
|
|
44
|
+
},
|
|
45
|
+
"./timings": {
|
|
46
|
+
"import": "./timings/index.js",
|
|
47
|
+
"require": "./timings/index.cjs",
|
|
48
|
+
"types": "./timings/index.d.ts"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"README.mdx",
|
|
53
|
+
"chunk-F4ITEBDQ.cjs",
|
|
54
|
+
"chunk-HN4BBSRG.js",
|
|
55
|
+
"index.cjs",
|
|
56
|
+
"index.d.cts",
|
|
57
|
+
"index.d.ts",
|
|
58
|
+
"index.js",
|
|
59
|
+
"package.json",
|
|
60
|
+
"timings"
|
|
61
|
+
],
|
|
62
|
+
"sha": "3355e5f3",
|
|
63
|
+
"scripts": {}
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
var _chunkF4ITEBDQcjs = require('../chunk-F4ITEBDQ.cjs');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
exports.measure = _chunkF4ITEBDQcjs.measure; exports.setRequestSink = _chunkF4ITEBDQcjs.setRequestSink; exports.setTimingSink = _chunkF4ITEBDQcjs.setTimingSink; exports.setUpstreamErrorSink = _chunkF4ITEBDQcjs.setUpstreamErrorSink; exports.trackFetch = _chunkF4ITEBDQcjs.trackFetch;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Контекст сбоя upstream для приёмника ошибок. */
|
|
2
|
+
interface UpstreamErrorContext {
|
|
3
|
+
name: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
status?: number | null;
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Куда уходят метки: имя куска работы, его длительность и момент начала.
|
|
10
|
+
*
|
|
11
|
+
* Начало нужно потому, что куски вложены друг в друга и идут внахлёст: по одним длительностям
|
|
12
|
+
* «ручка страницы 80 мс» и «ручка меню 187 мс» читаются как 267 мс работы, тогда как второй
|
|
13
|
+
* запрос эти 80 мс просто ждал своей очереди.
|
|
14
|
+
*/
|
|
15
|
+
type TimingSink = (name: string, ms: number, startedAt: number) => void;
|
|
16
|
+
/** Поход приложения наружу: куда ходили и чем кончилось. */
|
|
17
|
+
interface TrackedRequest {
|
|
18
|
+
/** Имя куска работы — то же, что у метки времени. */
|
|
19
|
+
name: string;
|
|
20
|
+
url: string;
|
|
21
|
+
status: number | null;
|
|
22
|
+
ms: number;
|
|
23
|
+
startedAt: number;
|
|
24
|
+
/** Размер ответа по `content-length`; `null` — бэкенд его не прислал. */
|
|
25
|
+
bytes: number | null;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Куда уходят походы наружу.
|
|
30
|
+
*
|
|
31
|
+
* Отдельно от меток, хотя момент один: метка отвечает «сколько заняло», поход — «куда ходили
|
|
32
|
+
* и что ответили». Первое уезжает хосту заголовком `Server-Timing` всегда, второе — только
|
|
33
|
+
* когда хост попросил, и только в разработке.
|
|
34
|
+
*/
|
|
35
|
+
type RequestSink = (request: TrackedRequest) => void;
|
|
36
|
+
type UpstreamErrorSink = (context: UpstreamErrorContext) => void;
|
|
37
|
+
/** Ставит приёмник меток; `null` — снимает. */
|
|
38
|
+
declare function setTimingSink(next: TimingSink | null): void;
|
|
39
|
+
/** Ставит приёмник походов наружу; `null` — снимает. */
|
|
40
|
+
declare function setRequestSink(next: RequestSink | null): void;
|
|
41
|
+
/** Ставит приёмник upstream-ошибок; `null` — снимает. */
|
|
42
|
+
declare function setUpstreamErrorSink(next: UpstreamErrorSink | null): void;
|
|
43
|
+
/** Измеряет работу и отдаёт её длительность приёмнику. */
|
|
44
|
+
declare function measure<T>(name: string, work: () => Promise<T>): Promise<T>;
|
|
45
|
+
/** Ходит в ручку и рассказывает о походе: длительность — меткой, адрес и исход — приёмнику. */
|
|
46
|
+
declare function trackFetch(name: string, url: string, init?: RequestInit): Promise<Response>;
|
|
47
|
+
|
|
48
|
+
export { type RequestSink, type TimingSink, type TrackedRequest, type UpstreamErrorContext, type UpstreamErrorSink, measure, setRequestSink, setTimingSink, setUpstreamErrorSink, trackFetch };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Контекст сбоя upstream для приёмника ошибок. */
|
|
2
|
+
interface UpstreamErrorContext {
|
|
3
|
+
name: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
status?: number | null;
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Куда уходят метки: имя куска работы, его длительность и момент начала.
|
|
10
|
+
*
|
|
11
|
+
* Начало нужно потому, что куски вложены друг в друга и идут внахлёст: по одним длительностям
|
|
12
|
+
* «ручка страницы 80 мс» и «ручка меню 187 мс» читаются как 267 мс работы, тогда как второй
|
|
13
|
+
* запрос эти 80 мс просто ждал своей очереди.
|
|
14
|
+
*/
|
|
15
|
+
type TimingSink = (name: string, ms: number, startedAt: number) => void;
|
|
16
|
+
/** Поход приложения наружу: куда ходили и чем кончилось. */
|
|
17
|
+
interface TrackedRequest {
|
|
18
|
+
/** Имя куска работы — то же, что у метки времени. */
|
|
19
|
+
name: string;
|
|
20
|
+
url: string;
|
|
21
|
+
status: number | null;
|
|
22
|
+
ms: number;
|
|
23
|
+
startedAt: number;
|
|
24
|
+
/** Размер ответа по `content-length`; `null` — бэкенд его не прислал. */
|
|
25
|
+
bytes: number | null;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Куда уходят походы наружу.
|
|
30
|
+
*
|
|
31
|
+
* Отдельно от меток, хотя момент один: метка отвечает «сколько заняло», поход — «куда ходили
|
|
32
|
+
* и что ответили». Первое уезжает хосту заголовком `Server-Timing` всегда, второе — только
|
|
33
|
+
* когда хост попросил, и только в разработке.
|
|
34
|
+
*/
|
|
35
|
+
type RequestSink = (request: TrackedRequest) => void;
|
|
36
|
+
type UpstreamErrorSink = (context: UpstreamErrorContext) => void;
|
|
37
|
+
/** Ставит приёмник меток; `null` — снимает. */
|
|
38
|
+
declare function setTimingSink(next: TimingSink | null): void;
|
|
39
|
+
/** Ставит приёмник походов наружу; `null` — снимает. */
|
|
40
|
+
declare function setRequestSink(next: RequestSink | null): void;
|
|
41
|
+
/** Ставит приёмник upstream-ошибок; `null` — снимает. */
|
|
42
|
+
declare function setUpstreamErrorSink(next: UpstreamErrorSink | null): void;
|
|
43
|
+
/** Измеряет работу и отдаёт её длительность приёмнику. */
|
|
44
|
+
declare function measure<T>(name: string, work: () => Promise<T>): Promise<T>;
|
|
45
|
+
/** Ходит в ручку и рассказывает о походе: длительность — меткой, адрес и исход — приёмнику. */
|
|
46
|
+
declare function trackFetch(name: string, url: string, init?: RequestInit): Promise<Response>;
|
|
47
|
+
|
|
48
|
+
export { type RequestSink, type TimingSink, type TrackedRequest, type UpstreamErrorContext, type UpstreamErrorSink, measure, setRequestSink, setTimingSink, setUpstreamErrorSink, trackFetch };
|
package/timings/index.js
ADDED