@application-kit/observability 0.3.1 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @application-kit/observability
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0f930bf: `metric-names.json` в пакете (вход `@application-kit/observability/metric-names.json`): имена и типы всех метрик
8
+ реестра — гистограмм HTTP и GraphQL и метрик процесса. По нему чарт платформы сверяет VMRule и дашборды, не
9
+ запуская код пакета; совпадение файла с реестром держит тест пакета.
10
+
11
+ ## 0.4.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 29b4378: Вход `/tracing` (только ESM): `startTracing({ esmHookedModules, instrumentations, resolveClientIp })` — OpenTelemetry
16
+ NodeSDK для `node --import` с OTLP gRPC-экспортёром, спанами HTTP-сервера без служебных путей и с `client.address`,
17
+ ESM-хуком строго по списку модулей и флашем на `beforeExit`; без endpoint'а коллектора не делает ничего.
18
+
3
19
  ## 0.3.1
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -24,6 +24,7 @@ Peer-зависимости: `@nestjs/common`, `@prometheus-io/client`; оста
24
24
  | `@application-kit/observability/http` | `resolveClientIp` — IP клиента за прокси | ничего |
25
25
  | `@application-kit/observability/logging` | опции nestjs-pino, request-id, support-id, служебные пути, токен логгера | `@opentelemetry/api`, `class-validator`, `class-transformer`; типы — `nestjs-pino`, `pino` |
26
26
  | `@application-kit/observability/metrics` | реестр, гистограммы HTTP и GraphQL, `GET /metrics`, хуки Fastify, сервер `/metrics` | `@nestjs/common`, `@prometheus-io/client`; типы — `fastify`, `@nestjs/platform-fastify` |
27
+ | `@application-kit/observability/tracing` | `startTracing` — OpenTelemetry NodeSDK для `node --import`, только ESM | `@opentelemetry/sdk-node`, `@opentelemetry/instrumentation`, `@opentelemetry/instrumentation-http`, `@opentelemetry/exporter-trace-otlp-grpc`, `@opentelemetry/semantic-conventions` |
27
28
 
28
29
  Корневого входа нет: каждый вход несёт свои peer-зависимости, и лишнего потребитель не ставит.
29
30
 
@@ -38,6 +39,9 @@ Peer-зависимости: `@nestjs/common`, `@prometheus-io/client`; оста
38
39
  `start(key)` → `describe(key, document)` → `finish(key, hasErrors)`, где `key` — объект контекста запроса;
39
40
  `asMercuriusHook` оборачивает синхронный обработчик в async-хук Mercurius.
40
41
  - `DURATION_BUCKETS_SECONDS` — общие границы гистограмм, от 5 мс до 10 с.
42
+ - `@application-kit/observability/metric-names.json` — имена и типы всех метрик реестра (обе гистограммы и метрики
43
+ процесса на Linux) для тех, кто строит по ним алерты и дашборды, не запуская код: чарт платформы сверяет с ним
44
+ VMRule и дашборды. Файл совпадает с реестром — это держит тест пакета; переименование метрики — major.
41
45
 
42
46
  Web-процесс подключает `MetricsHttpModule.forRoot({ publicRoute })` — реестр, обе гистограммы и `GET /metrics`
43
47
  (путь — `METRICS_ROUTE` входа `/logging`), — и вешает хуки после создания приложения: HTTP — `onResponse` Fastify
@@ -137,3 +141,38 @@ export class WebModule {}
137
141
  ```ts
138
142
  const clientIp = resolveClientIp(request, { trustedHeaders: ['cf-connecting-ip'] })
139
143
  ```
144
+
145
+ ## Трейсинг (`/tracing`)
146
+
147
+ `startTracing({ esmHookedModules, instrumentations, resolveClientIp })` поднимает OpenTelemetry NodeSDK: OTLP gRPC-экспортёр,
148
+ спаны HTTP-сервера без служебных путей (`isInfrastructurePath` входа `/logging`) и с `client.address` корневого спана,
149
+ хук import-in-the-middle строго по списку модулей, флаш спанов на `beforeExit`. Endpoint, sampling, `service.name` и
150
+ атрибуты ресурса SDK читает из стандартных `OTEL_*`; без `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` (или
151
+ `OTEL_EXPORTER_OTLP_ENDPOINT`) функция не делает ничего — ни хука, ни инструментаций — и отдаёт `undefined`.
152
+
153
+ Вызов — в отдельном файле, который бандлер собирает в свой выход и который процесс грузит флагом рантайма
154
+ `node --import ./tracing.mjs`: инструментации патчат модули в момент загрузки, а ESM выполняет все `import` бандла до
155
+ его первой строки, поэтому импорт из кода приложения опоздал бы. Вход только ESM — `module.register` есть лишь у
156
+ ESM-загрузчика.
157
+
158
+ - `esmHookedModules` — пакеты, которые бандл импортирует как ESM (например `kafkajs`): до них не дотягивается
159
+ require-in-the-middle. Хук без списка обернул бы и чанки самого бандла и порвал бы им live-binding.
160
+ - `instrumentations` — фабрика инструментаций продукта (фреймворк, драйвер БД, брокер): конструктор инструментации
161
+ сразу патчит модуль, поэтому вызывается фабрика, только когда трейсинг включён.
162
+ - `resolveClientIp` — резолвер адреса клиента; дефолт — `resolveClientIp` входа `/http` без доверенных заголовков.
163
+
164
+ ```ts
165
+ // src/tracing.ts → vite build --ssr src/tracing.ts → node --import ./tracing.mjs ./web.mjs
166
+ startTracing({
167
+ esmHookedModules: ['kafkajs'],
168
+ instrumentations: () => [
169
+ new FastifyOtelInstrumentation({ registerOnInitialization: true }),
170
+ new PgInstrumentation(),
171
+ new KafkaJsInstrumentation(),
172
+ ],
173
+ resolveClientIp: (request) => resolveClientIp(request, { trustedHeaders: ['cf-connecting-ip'] }),
174
+ })
175
+ ```
176
+
177
+ Версии `@opentelemetry/*` с номером `0.x` выходят одним релизом и между собой совместимы только в пределах одного минора:
178
+ `sdk-node`, `instrumentation`, `instrumentation-http` и `exporter-trace-otlp-grpc` ставятся одной версией `0.221.x`.
@@ -0,0 +1,2 @@
1
+ export * from './start-tracing.js';
2
+ export * from './tracing-options.interface.js';
@@ -0,0 +1,2 @@
1
+ export * from './start-tracing.js';
2
+ export * from './tracing-options.interface.js';
@@ -0,0 +1,3 @@
1
+ import { NodeSDK } from '@opentelemetry/sdk-node';
2
+ import type { TracingOptionsInterface } from './tracing-options.interface.js';
3
+ export declare const startTracing: (options: TracingOptionsInterface) => NodeSDK | undefined;
@@ -0,0 +1,55 @@
1
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
2
+ import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
3
+ import { NodeSDK } from '@opentelemetry/sdk-node';
4
+ import { ATTR_CLIENT_ADDRESS } from '@opentelemetry/semantic-conventions';
5
+ import { register } from 'node:module';
6
+ import nodeProcess from 'node:process';
7
+ import { resolveClientIp } from '../http/resolve-client-ip.js';
8
+ import { isInfrastructurePath } from '../logging/infrastructure-paths.js';
9
+ const withoutQuery = (url) => {
10
+ const queryStart = url.indexOf('?');
11
+ return queryStart === -1 ? url : url.slice(0, queryStart);
12
+ };
13
+ const isServiceRequest = (request) => request.url !== undefined
14
+ && isInfrastructurePath(withoutQuery(request.url));
15
+ const createHttpInstrumentation = (clientIp = resolveClientIp) => {
16
+ const clientAddressAttributes = (request) => {
17
+ const address = clientIp(request);
18
+ return address === '' ? {} : { [ATTR_CLIENT_ADDRESS]: address };
19
+ };
20
+ return new HttpInstrumentation({
21
+ ignoreIncomingRequestHook: isServiceRequest,
22
+ startIncomingSpanHook: clientAddressAttributes,
23
+ });
24
+ };
25
+ const flushSpans = async (sdk) => {
26
+ try {
27
+ await sdk.shutdown();
28
+ }
29
+ catch {
30
+ }
31
+ };
32
+ const tracesEndpoint = () => {
33
+ const { env } = nodeProcess;
34
+ return (env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? env.OTEL_EXPORTER_OTLP_ENDPOINT ?? '').trim();
35
+ };
36
+ export const startTracing = (options) => {
37
+ if (tracesEndpoint() === '') {
38
+ return undefined;
39
+ }
40
+ register('@opentelemetry/instrumentation/hook.mjs', import.meta.url, {
41
+ data: { include: options.esmHookedModules },
42
+ });
43
+ const sdk = new NodeSDK({
44
+ instrumentations: [
45
+ createHttpInstrumentation(options.resolveClientIp),
46
+ ...options.instrumentations?.() ?? [],
47
+ ],
48
+ traceExporter: new OTLPTraceExporter(),
49
+ });
50
+ sdk.start();
51
+ nodeProcess.once('beforeExit', () => {
52
+ void flushSpans(sdk);
53
+ });
54
+ return sdk;
55
+ };
@@ -0,0 +1,8 @@
1
+ import type { Instrumentation } from '@opentelemetry/instrumentation';
2
+ import type { ClientIpResolver } from '../logging/pino-logger-options.interface.js';
3
+ export type InstrumentationsFactory = () => Instrumentation[];
4
+ export interface TracingOptionsInterface {
5
+ readonly esmHookedModules: readonly string[];
6
+ readonly instrumentations?: InstrumentationsFactory;
7
+ readonly resolveClientIp?: ClientIpResolver;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,142 @@
1
+ [
2
+ {
3
+ "name": "graphql_operation_duration_seconds",
4
+ "type": "histogram"
5
+ },
6
+ {
7
+ "name": "http_request_duration_seconds",
8
+ "type": "histogram"
9
+ },
10
+ {
11
+ "name": "nodejs_active_handles",
12
+ "type": "gauge"
13
+ },
14
+ {
15
+ "name": "nodejs_active_handles_total",
16
+ "type": "gauge"
17
+ },
18
+ {
19
+ "name": "nodejs_active_requests",
20
+ "type": "gauge"
21
+ },
22
+ {
23
+ "name": "nodejs_active_requests_total",
24
+ "type": "gauge"
25
+ },
26
+ {
27
+ "name": "nodejs_active_resources",
28
+ "type": "gauge"
29
+ },
30
+ {
31
+ "name": "nodejs_active_resources_total",
32
+ "type": "gauge"
33
+ },
34
+ {
35
+ "name": "nodejs_eventloop_lag_max_seconds",
36
+ "type": "gauge"
37
+ },
38
+ {
39
+ "name": "nodejs_eventloop_lag_mean_seconds",
40
+ "type": "gauge"
41
+ },
42
+ {
43
+ "name": "nodejs_eventloop_lag_min_seconds",
44
+ "type": "gauge"
45
+ },
46
+ {
47
+ "name": "nodejs_eventloop_lag_p50_seconds",
48
+ "type": "gauge"
49
+ },
50
+ {
51
+ "name": "nodejs_eventloop_lag_p90_seconds",
52
+ "type": "gauge"
53
+ },
54
+ {
55
+ "name": "nodejs_eventloop_lag_p99_seconds",
56
+ "type": "gauge"
57
+ },
58
+ {
59
+ "name": "nodejs_eventloop_lag_seconds",
60
+ "type": "gauge"
61
+ },
62
+ {
63
+ "name": "nodejs_eventloop_lag_stddev_seconds",
64
+ "type": "gauge"
65
+ },
66
+ {
67
+ "name": "nodejs_eventloop_utilization_histogram",
68
+ "type": "histogram"
69
+ },
70
+ {
71
+ "name": "nodejs_eventloop_utilization_summary",
72
+ "type": "summary"
73
+ },
74
+ {
75
+ "name": "nodejs_external_memory_bytes",
76
+ "type": "gauge"
77
+ },
78
+ {
79
+ "name": "nodejs_gc_duration_seconds",
80
+ "type": "histogram"
81
+ },
82
+ {
83
+ "name": "nodejs_heap_size_total_bytes",
84
+ "type": "gauge"
85
+ },
86
+ {
87
+ "name": "nodejs_heap_size_used_bytes",
88
+ "type": "gauge"
89
+ },
90
+ {
91
+ "name": "nodejs_heap_space_size_available_bytes",
92
+ "type": "gauge"
93
+ },
94
+ {
95
+ "name": "nodejs_heap_space_size_total_bytes",
96
+ "type": "gauge"
97
+ },
98
+ {
99
+ "name": "nodejs_heap_space_size_used_bytes",
100
+ "type": "gauge"
101
+ },
102
+ {
103
+ "name": "nodejs_version_info",
104
+ "type": "gauge"
105
+ },
106
+ {
107
+ "name": "process_cpu_seconds_total",
108
+ "type": "counter"
109
+ },
110
+ {
111
+ "name": "process_cpu_system_seconds_total",
112
+ "type": "counter"
113
+ },
114
+ {
115
+ "name": "process_cpu_user_seconds_total",
116
+ "type": "counter"
117
+ },
118
+ {
119
+ "name": "process_heap_bytes",
120
+ "type": "gauge"
121
+ },
122
+ {
123
+ "name": "process_max_fds",
124
+ "type": "gauge"
125
+ },
126
+ {
127
+ "name": "process_open_fds",
128
+ "type": "gauge"
129
+ },
130
+ {
131
+ "name": "process_resident_memory_bytes",
132
+ "type": "gauge"
133
+ },
134
+ {
135
+ "name": "process_start_time_seconds",
136
+ "type": "gauge"
137
+ },
138
+ {
139
+ "name": "process_virtual_memory_bytes",
140
+ "type": "gauge"
141
+ }
142
+ ]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@application-kit/observability",
3
- "version": "0.3.1",
4
- "description": "Наблюдаемость NestJS-процесса: метрики Prometheus и RED-гистограммы, health-чеки и пробы, опции pino с request-id и trace-контекстом, логирование CQRS-шин, реальный IP клиента за прокси",
3
+ "version": "0.5.0",
4
+ "description": "Наблюдаемость NestJS-процесса: метрики Prometheus и RED-гистограммы, health-чеки и пробы, опции pino с request-id и trace-контекстом, логирование CQRS-шин, реальный IP клиента за прокси, трейсинг OpenTelemetry",
5
5
  "keywords": [
6
6
  "cqrs",
7
7
  "health",
@@ -9,9 +9,11 @@
9
9
  "metrics",
10
10
  "nestjs",
11
11
  "observability",
12
+ "opentelemetry",
12
13
  "pino",
13
14
  "prometheus",
14
- "terminus"
15
+ "terminus",
16
+ "tracing"
15
17
  ],
16
18
  "homepage": "https://gitlab.com/application-kit/application-kit/-/tree/main/packages/observability",
17
19
  "bugs": "https://gitlab.com/application-kit/application-kit/-/issues",
@@ -87,6 +89,7 @@
87
89
  "default": "./dist/cjs/logging/index.js"
88
90
  }
89
91
  },
92
+ "./metric-names.json": "./metric-names.json",
90
93
  "./metrics": {
91
94
  "import": {
92
95
  "types": "./dist/esm/metrics/index.d.ts",
@@ -97,16 +100,23 @@
97
100
  "default": "./dist/cjs/metrics/index.js"
98
101
  }
99
102
  },
100
- "./package.json": "./package.json"
103
+ "./package.json": "./package.json",
104
+ "./tracing": {
105
+ "import": {
106
+ "types": "./dist/esm/tracing/index.d.ts",
107
+ "default": "./dist/esm/tracing/index.js"
108
+ }
109
+ }
101
110
  },
102
111
  "main": "./dist/cjs/metrics/index.js",
103
112
  "module": "./dist/esm/metrics/index.js",
104
113
  "types": "./dist/cjs/metrics/index.d.ts",
105
114
  "files": [
106
- "dist"
115
+ "dist",
116
+ "metric-names.json"
107
117
  ],
108
118
  "scripts": {
109
- "build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src/cqrs && node ../../scripts/barrels.mjs create ./src/health --entry-only=cli,graphql && node ../../scripts/barrels.mjs create ./src/http && node ../../scripts/barrels.mjs create ./src/logging && node ../../scripts/barrels.mjs create ./src/metrics && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
119
+ "build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src/cqrs && node ../../scripts/barrels.mjs create ./src/health --entry-only=cli,graphql && node ../../scripts/barrels.mjs create ./src/http && node ../../scripts/barrels.mjs create ./src/logging && node ../../scripts/barrels.mjs create ./src/metrics && node ../../scripts/barrels.mjs create ./src/tracing && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
110
120
  "lint:tsc": "tsc --noEmit --pretty --project .",
111
121
  "test:mutation": "stryker run",
112
122
  "test:unit": "vitest run"
@@ -124,6 +134,11 @@
124
134
  "@nestjs/testing": "^11.2.7",
125
135
  "@nestjs/typeorm": "^11.0.3",
126
136
  "@opentelemetry/api": "^1.9.1",
137
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0",
138
+ "@opentelemetry/instrumentation": "^0.221.0",
139
+ "@opentelemetry/instrumentation-http": "^0.221.0",
140
+ "@opentelemetry/sdk-node": "^0.221.0",
141
+ "@opentelemetry/semantic-conventions": "^1.43.0",
127
142
  "@prometheus-io/client": "^0.16.0",
128
143
  "@stryker-mutator/core": "^10.0.0",
129
144
  "@stryker-mutator/vitest-runner": "^10.0.0",
@@ -149,6 +164,11 @@
149
164
  "@nestjs/terminus": "^11.0.0",
150
165
  "@nestjs/typeorm": "^11.0.0",
151
166
  "@opentelemetry/api": "^1.9.0",
167
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0",
168
+ "@opentelemetry/instrumentation": "^0.221.0",
169
+ "@opentelemetry/instrumentation-http": "^0.221.0",
170
+ "@opentelemetry/sdk-node": "^0.221.0",
171
+ "@opentelemetry/semantic-conventions": "^1.43.0",
152
172
  "@prometheus-io/client": "^0.16.0",
153
173
  "class-transformer": "^0.5.1",
154
174
  "class-validator": "^0.15.1",
@@ -178,6 +198,21 @@
178
198
  "@opentelemetry/api": {
179
199
  "optional": true
180
200
  },
201
+ "@opentelemetry/exporter-trace-otlp-grpc": {
202
+ "optional": true
203
+ },
204
+ "@opentelemetry/instrumentation": {
205
+ "optional": true
206
+ },
207
+ "@opentelemetry/instrumentation-http": {
208
+ "optional": true
209
+ },
210
+ "@opentelemetry/sdk-node": {
211
+ "optional": true
212
+ },
213
+ "@opentelemetry/semantic-conventions": {
214
+ "optional": true
215
+ },
181
216
  "class-transformer": {
182
217
  "optional": true
183
218
  },