@application-kit/observability 0.3.1 → 0.4.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,13 @@
1
1
  # @application-kit/observability
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 29b4378: Вход `/tracing` (только ESM): `startTracing({ esmHookedModules, instrumentations, resolveClientIp })` — OpenTelemetry
8
+ NodeSDK для `node --import` с OTLP gRPC-экспортёром, спанами HTTP-сервера без служебных путей и с `client.address`,
9
+ ESM-хуком строго по списку модулей и флашем на `beforeExit`; без endpoint'а коллектора не делает ничего.
10
+
3
11
  ## 0.3.1
4
12
 
5
13
  ### 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
 
@@ -137,3 +138,38 @@ export class WebModule {}
137
138
  ```ts
138
139
  const clientIp = resolveClientIp(request, { trustedHeaders: ['cf-connecting-ip'] })
139
140
  ```
141
+
142
+ ## Трейсинг (`/tracing`)
143
+
144
+ `startTracing({ esmHookedModules, instrumentations, resolveClientIp })` поднимает OpenTelemetry NodeSDK: OTLP gRPC-экспортёр,
145
+ спаны HTTP-сервера без служебных путей (`isInfrastructurePath` входа `/logging`) и с `client.address` корневого спана,
146
+ хук import-in-the-middle строго по списку модулей, флаш спанов на `beforeExit`. Endpoint, sampling, `service.name` и
147
+ атрибуты ресурса SDK читает из стандартных `OTEL_*`; без `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` (или
148
+ `OTEL_EXPORTER_OTLP_ENDPOINT`) функция не делает ничего — ни хука, ни инструментаций — и отдаёт `undefined`.
149
+
150
+ Вызов — в отдельном файле, который бандлер собирает в свой выход и который процесс грузит флагом рантайма
151
+ `node --import ./tracing.mjs`: инструментации патчат модули в момент загрузки, а ESM выполняет все `import` бандла до
152
+ его первой строки, поэтому импорт из кода приложения опоздал бы. Вход только ESM — `module.register` есть лишь у
153
+ ESM-загрузчика.
154
+
155
+ - `esmHookedModules` — пакеты, которые бандл импортирует как ESM (например `kafkajs`): до них не дотягивается
156
+ require-in-the-middle. Хук без списка обернул бы и чанки самого бандла и порвал бы им live-binding.
157
+ - `instrumentations` — фабрика инструментаций продукта (фреймворк, драйвер БД, брокер): конструктор инструментации
158
+ сразу патчит модуль, поэтому вызывается фабрика, только когда трейсинг включён.
159
+ - `resolveClientIp` — резолвер адреса клиента; дефолт — `resolveClientIp` входа `/http` без доверенных заголовков.
160
+
161
+ ```ts
162
+ // src/tracing.ts → vite build --ssr src/tracing.ts → node --import ./tracing.mjs ./web.mjs
163
+ startTracing({
164
+ esmHookedModules: ['kafkajs'],
165
+ instrumentations: () => [
166
+ new FastifyOtelInstrumentation({ registerOnInitialization: true }),
167
+ new PgInstrumentation(),
168
+ new KafkaJsInstrumentation(),
169
+ ],
170
+ resolveClientIp: (request) => resolveClientIp(request, { trustedHeaders: ['cf-connecting-ip'] }),
171
+ })
172
+ ```
173
+
174
+ Версии `@opentelemetry/*` с номером `0.x` выходят одним релизом и между собой совместимы только в пределах одного минора:
175
+ `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 {};
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.4.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",
@@ -97,7 +99,13 @@
97
99
  "default": "./dist/cjs/metrics/index.js"
98
100
  }
99
101
  },
100
- "./package.json": "./package.json"
102
+ "./package.json": "./package.json",
103
+ "./tracing": {
104
+ "import": {
105
+ "types": "./dist/esm/tracing/index.d.ts",
106
+ "default": "./dist/esm/tracing/index.js"
107
+ }
108
+ }
101
109
  },
102
110
  "main": "./dist/cjs/metrics/index.js",
103
111
  "module": "./dist/esm/metrics/index.js",
@@ -106,7 +114,7 @@
106
114
  "dist"
107
115
  ],
108
116
  "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",
117
+ "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
118
  "lint:tsc": "tsc --noEmit --pretty --project .",
111
119
  "test:mutation": "stryker run",
112
120
  "test:unit": "vitest run"
@@ -124,6 +132,11 @@
124
132
  "@nestjs/testing": "^11.2.7",
125
133
  "@nestjs/typeorm": "^11.0.3",
126
134
  "@opentelemetry/api": "^1.9.1",
135
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0",
136
+ "@opentelemetry/instrumentation": "^0.221.0",
137
+ "@opentelemetry/instrumentation-http": "^0.221.0",
138
+ "@opentelemetry/sdk-node": "^0.221.0",
139
+ "@opentelemetry/semantic-conventions": "^1.43.0",
127
140
  "@prometheus-io/client": "^0.16.0",
128
141
  "@stryker-mutator/core": "^10.0.0",
129
142
  "@stryker-mutator/vitest-runner": "^10.0.0",
@@ -149,6 +162,11 @@
149
162
  "@nestjs/terminus": "^11.0.0",
150
163
  "@nestjs/typeorm": "^11.0.0",
151
164
  "@opentelemetry/api": "^1.9.0",
165
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0",
166
+ "@opentelemetry/instrumentation": "^0.221.0",
167
+ "@opentelemetry/instrumentation-http": "^0.221.0",
168
+ "@opentelemetry/sdk-node": "^0.221.0",
169
+ "@opentelemetry/semantic-conventions": "^1.43.0",
152
170
  "@prometheus-io/client": "^0.16.0",
153
171
  "class-transformer": "^0.5.1",
154
172
  "class-validator": "^0.15.1",
@@ -178,6 +196,21 @@
178
196
  "@opentelemetry/api": {
179
197
  "optional": true
180
198
  },
199
+ "@opentelemetry/exporter-trace-otlp-grpc": {
200
+ "optional": true
201
+ },
202
+ "@opentelemetry/instrumentation": {
203
+ "optional": true
204
+ },
205
+ "@opentelemetry/instrumentation-http": {
206
+ "optional": true
207
+ },
208
+ "@opentelemetry/sdk-node": {
209
+ "optional": true
210
+ },
211
+ "@opentelemetry/semantic-conventions": {
212
+ "optional": true
213
+ },
181
214
  "class-transformer": {
182
215
  "optional": true
183
216
  },