hotelme-nuxt-logger 1.4.0 → 1.6.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.md CHANGED
@@ -151,6 +151,74 @@ const response = await $fetch('/api/hotels', {
151
151
  })
152
152
  ```
153
153
 
154
+ ## OpenTelemetry (з 1.5.0)
155
+
156
+ Трейсинг вимкнено за замовчуванням. Вмикається в nuxt.config — SDK, OTLP gRPC
157
+ експортер і `@opentelemetry/api` вже є в залежностях модуля, ставити окремо не треба.
158
+
159
+ ```typescript
160
+ logger: {
161
+ projectName: 'widgets-ssr',
162
+ otel: {
163
+ enabled: true,
164
+ ignorePaths: ['/nuxt_img/'], // + типові /_nuxt/, /_ipx/, /__nuxt, clientErrorEndpoint
165
+ ignoreUserAgents: ['^Slackbot-LinkExpanding ', '^kube-probe/'],
166
+ slowMs: 1000, // порятунок повільних, env OTEL_TRACES_SLOW_MS
167
+ },
168
+ }
169
+ ```
170
+
171
+ Env (читає SDK):
172
+
173
+ | Змінна | Типово |
174
+ |---|---|
175
+ | `OTEL_SERVICE_NAME` | `projectName` |
176
+ | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_ENDPOINT` | — (обов'язкова: без неї трейсинг не вмикається, лог `otel_disabled`) |
177
+ | `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG` | `parentbased_always_on` |
178
+
179
+ Env модуля (перекриває `slowMs` з nuxt.config):
180
+
181
+ | Змінна | Типово |
182
+ |---|---|
183
+ | `OTEL_TRACES_SLOW_MS` | — (0 — без порятунку) |
184
+
185
+ Напр. 25% трейсів + повільні ≥ 1 с з решти:
186
+
187
+ ```yaml
188
+ OTEL_TRACES_SAMPLER: parentbased_traceidratio
189
+ OTEL_TRACES_SAMPLER_ARG: "0.25"
190
+ OTEL_TRACES_SLOW_MS: "1000"
191
+ ```
192
+
193
+ Що робить модуль:
194
+
195
+ - **Активний контекст на весь запит.** Плагін обгортає `nitroApp.h3App.handler`, тож middleware, handler'и та SSR-рендер виконуються всередині `context.with(serverSpan)` — span'и з коду застосунку стають дочірніми без обгорток.
196
+ - **Server span** на кожен запит, батько — вхідний `traceparent`. Без нього trace id = `X-Request-ID` без дефісів (`2e285875-f20c-…` → `2e285875f20c…`, як у libwts3), якщо це UUID / 32 hex; інакше випадковий. У `event.context`: `otelSpan`; у відповіді — `X-Trace-Id`. 5xx → статус ERROR, 4xx — ні.
197
+ - **Вихідні fetch** (native `fetch`, `$fetch`, ofetch на зовнішній URL) — `@opentelemetry/instrumentation-undici` через diagnostics_channel: CLIENT span `GET <host>` (повний URL — в `url.full`), `traceparent` і `X-Request-ID` сторінки, якщо виклик не поставив свій. Поза запитом span'и не пишуться.
198
+ - **Внутрішні SSR-виклики** (`$fetch('/api/…')` → Nitro localFetch) ідуть у тому ж async-ланцюжку, тож span роуту стає дочірнім span'а сторінки, а logger-middleware бере reqid сторінки. Заголовки руками не потрібні.
199
+ - **Назва span'а** — шаблон, ніколи сирий шлях (кожна назва — окремий stream у VictoriaTraces), визначається сама після роутингу:
200
+ - сторінка → `route.name` з Vue Router без i18n-суфікса: `GET weather-citynameid`, `GET index` (аліаси теж). Локаль — в атрибуті `i18n.locale`;
201
+ - Nitro-хендлер → шаблон з файлової структури: `GET /api/city_info`, `GET /record/**`;
202
+ - решта (відповідь з server middleware, невідомий шлях) → `GET /api/*` / `GET /*`; точний шлях — в `url.path`, статус — в `http.response.status_code`.
203
+ - **Фільтри** (статика, `ignorePaths`, `ignoreUserAgents`) не міняють поведінку запиту: він обробляється як завжди, але в невибраному (sampled=0) контексті — без span'ів у нього, його внутрішніх викликів і бекенда (`traceparent …-00`; libwts3 з `OTEL_TRACES_SLOW_MS` все ж відправить свій повільний запит — без батька з цього боку). Потрібен parent-based семплер (типовий).
204
+ - **Вибірка «head + порятунок повільних»** (`slowMs` > 0 і семплер вибирає < 100%):
205
+ - частка з `OTEL_TRACES_SAMPLER_ARG` вибирається на старті root span'а за trace id, як у `parentbased_traceidratio` — `traceparent …-01` іде в бекенд, трейс повний. Підтримуються `always_on` / `always_off` / `traceidratio` (з `parentbased_` чи без — семплер завжди parent-based); інший семплер (`jaeger_remote` тощо) → warn `otel_config_invalid`, порятунок вимкнено;
206
+ - решта пишеться в пам'ять (`traceparent …-00`) і буферизується до кінця свого локального root span'а — окремо для кожного запиту, навіть з одним trace id. Root тривав ≥ `slowMs` → експортуються він (`sampling.kept_by: "slow"`) і всі його локальні дочірні span'и (`/api/*`, CLIENT span'и вихідних fetch з тривалістю); інакше буфер скидається. Так само працює `OTEL_TRACES_SLOW_MS` у libwts3;
207
+ - span'и бекенда в такому трейсі є, лише якщо бекенд врятував їх сам: libwts3 на `…-00` теж пише в пам'ять і відправляє запит, що тривав ≥ свого `OTEL_TRACES_SLOW_MS` (типово 1000). Повільний бекенд робить повільним і root тут, тож трейс зв'язний; швидкого бекенда в ньому немає;
208
+ - вхідний `traceparent …-01` — вибраний, `…-00` — та сама логіка порятунку; фільтри (статика, `ignorePaths`, `ignoreUserAgents`) не пишуться взагалі;
209
+ - буфер обмежено: 2000 трейсів у польоті, 512 span'ів на трейс.
210
+ - **`?force_otel=1`** у запиті (як у libwts3) — трейс вибраний завжди: попри частку семплера, фільтри (`ignorePaths`, `ignoreUserAgents`, статика) і вхідний `traceparent …-00`. Позначка в контексті, тож вибрані й дочірні span'и (внутрішні `/api/*`, вихідні fetch), а бекенд отримує `traceparent …-01` і пише свою частину повністю. Root — з `sampling.kept_by: "force"`. Працює з `always_*` / `traceidratio` (з `parentbased_` чи без); з іншим семплером (`jaeger_remote`) параметр ігнорується, `otel_initialized` пише `force_otel: false`. Параметр публічний: будь-хто може форсувати трейс — як і в libwts3.
211
+ - **Flush при зупинці**: span'и батчуються до 5 с, на `close` provider робить shutdown.
212
+ - Помилки експорту (`diag`) пишуться JSON-логером як `otel_diag`.
213
+
214
+ Хелпер (auto-import у Nitro або `import … from 'hotelme-nuxt-logger/runtime/otel'`), працює і з вимкненим трейсингом:
215
+
216
+ ```typescript
217
+ // X-Request-ID сторінки (явна подія або поточна) — для бекендів, коли OTel вимкнено
218
+ const headers = traceHeaders(event)
219
+
220
+ ```
221
+
154
222
  ## API
155
223
 
156
224
  ### Logger методи
package/dist/module.cjs CHANGED
@@ -17,7 +17,13 @@ const module$1 = kit.defineNuxtModule({
17
17
  enableRequestLogging: true,
18
18
  enableErrorLogging: true,
19
19
  enableClientErrorLogging: true,
20
- clientErrorEndpoint: "/_logger/client-error"
20
+ clientErrorEndpoint: "/_logger/client-error",
21
+ otel: {
22
+ enabled: false,
23
+ ignorePaths: ["/_nuxt/", "/_ipx/", "/__nuxt"],
24
+ ignoreStaticAssets: true,
25
+ ignoreUserAgents: []
26
+ }
21
27
  },
22
28
  setup(options, nuxt) {
23
29
  const moduleDir = typeof __dirname !== "undefined" ? __dirname : path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('module.cjs', document.baseURI).href))));
@@ -28,8 +34,14 @@ const module$1 = kit.defineNuxtModule({
28
34
  kit.addImportsDir(resolver.resolve("./runtime"));
29
35
  kit.addServerImports([
30
36
  { name: "logger", from: resolver.resolve("./runtime/logger") },
31
- { name: "getRequestId", from: resolver.resolve("./runtime/logger") }
37
+ { name: "getRequestId", from: resolver.resolve("./runtime/logger") },
38
+ { name: "traceHeaders", from: resolver.resolve("./runtime/server/otel/tracing") }
32
39
  ]);
40
+ const { enabled: otelEnabled, ignorePaths = [], ...otel } = options.otel ?? {};
41
+ nuxt.options.runtimeConfig.logger = {
42
+ ...nuxt.options.runtimeConfig.logger,
43
+ otel: { ...otel, ignorePaths: [.../* @__PURE__ */ new Set([...ignorePaths, clientErrorEndpoint])] }
44
+ };
33
45
  if (options.enableRequestLogging) {
34
46
  kit.addServerHandler({
35
47
  handler: resolver.resolve("./runtime/middleware/logger.server"),
@@ -50,6 +62,14 @@ const module$1 = kit.defineNuxtModule({
50
62
  handler: resolver.resolve("./runtime/server/routes/client-error.post")
51
63
  });
52
64
  }
65
+ if (otelEnabled) {
66
+ kit.addServerPlugin(resolver.resolve("./runtime/server/otel/plugin"));
67
+ kit.addPlugin({
68
+ src: resolver.resolve("./runtime/plugins/otelPageName.server"),
69
+ mode: "server"
70
+ });
71
+ }
72
+ kit.addServerPlugin(resolver.resolve("./runtime/server/contextPlugin"));
53
73
  }
54
74
  });
55
75
 
package/dist/module.d.cts CHANGED
@@ -27,8 +27,45 @@ interface ModuleOptions {
27
27
  * @default '/_logger/client-error'
28
28
  */
29
29
  clientErrorEndpoint?: string;
30
+ /**
31
+ * OpenTelemetry-трейсинг (OTLP gRPC). Вимкнено за замовчуванням.
32
+ * Хелпер `traceHeaders` доступний й з вимкненим трейсингом.
33
+ */
34
+ otel?: OtelOptions;
35
+ }
36
+ interface OtelOptions {
37
+ /**
38
+ * Tracer provider, OTLP-експортер і server span на кожен запит.
39
+ * @default false
40
+ */
41
+ enabled?: boolean;
42
+ /**
43
+ * Префікси шляхів без span'ів (разом з дочірніми). Додаються до типових.
44
+ * @default ['/_nuxt/', '/_ipx/', '/__nuxt', clientErrorEndpoint]
45
+ */
46
+ ignorePaths?: string[];
47
+ /**
48
+ * Без span'ів для статики (css, js, картинки, шрифти, json, txt, xml, map).
49
+ * @default true
50
+ */
51
+ ignoreStaticAssets?: boolean;
52
+ /**
53
+ * Regex-рядки User-Agent без span'ів (k8s probe, боти прев'ю посилань), напр.
54
+ * `'^kube-probe/'`, `'^Slackbot-LinkExpanding '`. Трейсинг вимикається на
55
+ * весь запит, разом із його внутрішніми /api-викликами та вихідними fetch.
56
+ * @default []
57
+ */
58
+ ignoreUserAgents?: string[];
59
+ /**
60
+ * Трейси поза вибіркою семплера з `OTEL_TRACES_SAMPLER*` (`always_*`,
61
+ * `traceidratio`, з `parentbased_` чи без), чий server span тривав ≥ `slowMs`,
62
+ * теж експортуються — увесь запит з локальними дочірніми span'ами, як
63
+ * `OTEL_TRACES_SLOW_MS` у libwts3. Бекенд рятує свою частину сам. Перекривається env
64
+ * `OTEL_TRACES_SLOW_MS`. 0 — вимкнено.
65
+ */
66
+ slowMs?: number;
30
67
  }
31
68
  declare const _default: any;
32
69
 
33
70
  export { _default as default };
34
- export type { ModuleOptions };
71
+ export type { ModuleOptions, OtelOptions };
package/dist/module.d.mts CHANGED
@@ -27,8 +27,45 @@ interface ModuleOptions {
27
27
  * @default '/_logger/client-error'
28
28
  */
29
29
  clientErrorEndpoint?: string;
30
+ /**
31
+ * OpenTelemetry-трейсинг (OTLP gRPC). Вимкнено за замовчуванням.
32
+ * Хелпер `traceHeaders` доступний й з вимкненим трейсингом.
33
+ */
34
+ otel?: OtelOptions;
35
+ }
36
+ interface OtelOptions {
37
+ /**
38
+ * Tracer provider, OTLP-експортер і server span на кожен запит.
39
+ * @default false
40
+ */
41
+ enabled?: boolean;
42
+ /**
43
+ * Префікси шляхів без span'ів (разом з дочірніми). Додаються до типових.
44
+ * @default ['/_nuxt/', '/_ipx/', '/__nuxt', clientErrorEndpoint]
45
+ */
46
+ ignorePaths?: string[];
47
+ /**
48
+ * Без span'ів для статики (css, js, картинки, шрифти, json, txt, xml, map).
49
+ * @default true
50
+ */
51
+ ignoreStaticAssets?: boolean;
52
+ /**
53
+ * Regex-рядки User-Agent без span'ів (k8s probe, боти прев'ю посилань), напр.
54
+ * `'^kube-probe/'`, `'^Slackbot-LinkExpanding '`. Трейсинг вимикається на
55
+ * весь запит, разом із його внутрішніми /api-викликами та вихідними fetch.
56
+ * @default []
57
+ */
58
+ ignoreUserAgents?: string[];
59
+ /**
60
+ * Трейси поза вибіркою семплера з `OTEL_TRACES_SAMPLER*` (`always_*`,
61
+ * `traceidratio`, з `parentbased_` чи без), чий server span тривав ≥ `slowMs`,
62
+ * теж експортуються — увесь запит з локальними дочірніми span'ами, як
63
+ * `OTEL_TRACES_SLOW_MS` у libwts3. Бекенд рятує свою частину сам. Перекривається env
64
+ * `OTEL_TRACES_SLOW_MS`. 0 — вимкнено.
65
+ */
66
+ slowMs?: number;
30
67
  }
31
68
  declare const _default: any;
32
69
 
33
70
  export { _default as default };
34
- export type { ModuleOptions };
71
+ export type { ModuleOptions, OtelOptions };
package/dist/module.d.ts CHANGED
@@ -27,8 +27,45 @@ interface ModuleOptions {
27
27
  * @default '/_logger/client-error'
28
28
  */
29
29
  clientErrorEndpoint?: string;
30
+ /**
31
+ * OpenTelemetry-трейсинг (OTLP gRPC). Вимкнено за замовчуванням.
32
+ * Хелпер `traceHeaders` доступний й з вимкненим трейсингом.
33
+ */
34
+ otel?: OtelOptions;
35
+ }
36
+ interface OtelOptions {
37
+ /**
38
+ * Tracer provider, OTLP-експортер і server span на кожен запит.
39
+ * @default false
40
+ */
41
+ enabled?: boolean;
42
+ /**
43
+ * Префікси шляхів без span'ів (разом з дочірніми). Додаються до типових.
44
+ * @default ['/_nuxt/', '/_ipx/', '/__nuxt', clientErrorEndpoint]
45
+ */
46
+ ignorePaths?: string[];
47
+ /**
48
+ * Без span'ів для статики (css, js, картинки, шрифти, json, txt, xml, map).
49
+ * @default true
50
+ */
51
+ ignoreStaticAssets?: boolean;
52
+ /**
53
+ * Regex-рядки User-Agent без span'ів (k8s probe, боти прев'ю посилань), напр.
54
+ * `'^kube-probe/'`, `'^Slackbot-LinkExpanding '`. Трейсинг вимикається на
55
+ * весь запит, разом із його внутрішніми /api-викликами та вихідними fetch.
56
+ * @default []
57
+ */
58
+ ignoreUserAgents?: string[];
59
+ /**
60
+ * Трейси поза вибіркою семплера з `OTEL_TRACES_SAMPLER*` (`always_*`,
61
+ * `traceidratio`, з `parentbased_` чи без), чий server span тривав ≥ `slowMs`,
62
+ * теж експортуються — увесь запит з локальними дочірніми span'ами, як
63
+ * `OTEL_TRACES_SLOW_MS` у libwts3. Бекенд рятує свою частину сам. Перекривається env
64
+ * `OTEL_TRACES_SLOW_MS`. 0 — вимкнено.
65
+ */
66
+ slowMs?: number;
30
67
  }
31
68
  declare const _default: any;
32
69
 
33
70
  export { _default as default };
34
- export type { ModuleOptions };
71
+ export type { ModuleOptions, OtelOptions };
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "1.4.0",
7
+ "version": "1.6.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -23,7 +23,13 @@ const module = defineNuxtModule({
23
23
  enableRequestLogging: true,
24
24
  enableErrorLogging: true,
25
25
  enableClientErrorLogging: true,
26
- clientErrorEndpoint: "/_logger/client-error"
26
+ clientErrorEndpoint: "/_logger/client-error",
27
+ otel: {
28
+ enabled: false,
29
+ ignorePaths: ["/_nuxt/", "/_ipx/", "/__nuxt"],
30
+ ignoreStaticAssets: true,
31
+ ignoreUserAgents: []
32
+ }
27
33
  },
28
34
  setup(options, nuxt) {
29
35
  const moduleDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
@@ -34,8 +40,14 @@ const module = defineNuxtModule({
34
40
  addImportsDir(resolver.resolve("./runtime"));
35
41
  addServerImports([
36
42
  { name: "logger", from: resolver.resolve("./runtime/logger") },
37
- { name: "getRequestId", from: resolver.resolve("./runtime/logger") }
43
+ { name: "getRequestId", from: resolver.resolve("./runtime/logger") },
44
+ { name: "traceHeaders", from: resolver.resolve("./runtime/server/otel/tracing") }
38
45
  ]);
46
+ const { enabled: otelEnabled, ignorePaths = [], ...otel } = options.otel ?? {};
47
+ nuxt.options.runtimeConfig.logger = {
48
+ ...nuxt.options.runtimeConfig.logger,
49
+ otel: { ...otel, ignorePaths: [.../* @__PURE__ */ new Set([...ignorePaths, clientErrorEndpoint])] }
50
+ };
39
51
  if (options.enableRequestLogging) {
40
52
  addServerHandler({
41
53
  handler: resolver.resolve("./runtime/middleware/logger.server"),
@@ -56,6 +68,14 @@ const module = defineNuxtModule({
56
68
  handler: resolver.resolve("./runtime/server/routes/client-error.post")
57
69
  });
58
70
  }
71
+ if (otelEnabled) {
72
+ addServerPlugin(resolver.resolve("./runtime/server/otel/plugin"));
73
+ addPlugin({
74
+ src: resolver.resolve("./runtime/plugins/otelPageName.server"),
75
+ mode: "server"
76
+ });
77
+ }
78
+ addServerPlugin(resolver.resolve("./runtime/server/contextPlugin"));
59
79
  }
60
80
  });
61
81
 
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Назва роута сторінки для server span'а (див. resolvedRouteName у serverSpan).
3
+ *
4
+ * `router.resolve`, а не `afterEach`: початкова навігація SSR відбувається ще в
5
+ * плагіні роутера Nuxt, до цього плагіна, і може впасти в route middleware
6
+ * (404, редирект) — тоді `currentRoute` лишається порожнім. Resolve лише
7
+ * зіставляє URL з таблицею роутів, незалежно від результату навігації.
8
+ * Аліаси (`/widgets/weather` → `index`) дають назву основного роута.
9
+ */
10
+ declare const _default: any;
11
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { defineNuxtPlugin, useRequestEvent } from "#imports";
2
+ const I18N_SEPARATOR = "___";
3
+ export default defineNuxtPlugin((nuxtApp) => {
4
+ const event = useRequestEvent();
5
+ const url = nuxtApp.ssrContext?.url;
6
+ if (!event?.context.otelSpan || !url) return;
7
+ const name = nuxtApp.$router.resolve(url).name;
8
+ if (typeof name !== "string") return;
9
+ const [baseName, locale] = name.split(I18N_SEPARATOR);
10
+ event.context.otelPageName = baseName;
11
+ if (locale) event.context.otelSpan.setAttribute("i18n.locale", locale);
12
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Весь pipeline запиту (middleware, handler, SSR-рендер) — всередині
3
+ * AsyncLocalStorage з його подією, див. requestContext.ts. Той самий прийом,
4
+ * що й `experimental.asyncContext` у Nitro: toNodeListener і localFetch читають
5
+ * `h3App.handler` на кожен запит.
6
+ */
7
+ declare const _default: import("nitropack").NitroAppPlugin;
8
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { defineNitroPlugin } from "nitropack/runtime";
2
+ import { runInRequestContext } from "./requestContext.js";
3
+ export default defineNitroPlugin((nitroApp) => {
4
+ const handler = nitroApp.h3App.handler;
5
+ nitroApp.h3App.handler = (event) => runInRequestContext(event, () => handler(event));
6
+ });
@@ -0,0 +1,39 @@
1
+ import { type Sampler } from '@opentelemetry/sdk-trace-base';
2
+ /** Серіалізована форма `logger.otel` з nuxt.config (див. module.ts). */
3
+ export interface OtelRuntimeConfig {
4
+ ignorePaths?: string[];
5
+ ignoreStaticAssets?: boolean;
6
+ ignoreUserAgents?: string[];
7
+ slowMs?: number;
8
+ }
9
+ export interface OtelSettings {
10
+ ignorePaths: string[];
11
+ ignoreStaticAssets: boolean;
12
+ ignoreUserAgent?: RegExp;
13
+ }
14
+ export declare const TRACER_NAME = "hotelme-nuxt-logger";
15
+ export declare function otelRuntimeConfig(): OtelRuntimeConfig;
16
+ /** Регекспи компілюються один раз, на першому запиті. */
17
+ export declare function otelSettings(): OtelSettings;
18
+ export interface OtelSampling {
19
+ /** Частка head-вибірки з OTEL_TRACES_SAMPLER*, 0–1 */
20
+ ratio: number;
21
+ /** Поріг порятунку повільних трейсів, мс */
22
+ slowMs: number;
23
+ }
24
+ /**
25
+ * Той самий семплер, що SDK зібрав би з env сам (`buildSamplerFromEnv` не
26
+ * експортується) — щоб обгорнути його ForceSampler'ом. undefined — семплер,
27
+ * якого тут немає: лишається на SDK.
28
+ */
29
+ export declare function envSampler(): Sampler | undefined;
30
+ /**
31
+ * Порятунок повільних: `OTEL_TRACES_SLOW_MS` (перекриває `logger.otel.slowMs`)
32
+ * > 0 і семплер з `OTEL_TRACES_SAMPLER*` вибирає менше 100%. Інакше undefined —
33
+ * семплер з env без порятунку (envSampler), як до 1.6.0.
34
+ *
35
+ * Семплер з порятунком завжди parent-based: `traceidratio` без префікса
36
+ * поводиться як `parentbased_traceidratio` — фільтри serverSpan.ts без цього
37
+ * не працюють.
38
+ */
39
+ export declare function otelSampling(): OtelSampling | undefined;
@@ -0,0 +1,59 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { AlwaysOffSampler, AlwaysOnSampler, ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
3
+ import { logger } from "../../logger.js";
4
+ export const TRACER_NAME = "hotelme-nuxt-logger";
5
+ export function otelRuntimeConfig() {
6
+ return useRuntimeConfig().logger?.otel ?? {};
7
+ }
8
+ let settings;
9
+ export function otelSettings() {
10
+ if (settings) return settings;
11
+ const config = otelRuntimeConfig();
12
+ const userAgents = (config.ignoreUserAgents ?? []).filter(Boolean);
13
+ settings = {
14
+ ignorePaths: config.ignorePaths ?? [],
15
+ ignoreStaticAssets: config.ignoreStaticAssets ?? true,
16
+ ignoreUserAgent: userAgents.length ? new RegExp(userAgents.map((ua) => `(?:${ua})`).join("|"), "i") : void 0
17
+ };
18
+ return settings;
19
+ }
20
+ function envNumber(name) {
21
+ const raw = process.env[name]?.trim();
22
+ if (!raw) return void 0;
23
+ const value = Number(raw);
24
+ if (Number.isFinite(value)) return value;
25
+ logger.warn("otel_config_invalid", { error: `${name}=${raw} is not a number, ignored` });
26
+ return void 0;
27
+ }
28
+ function envSamplerConfig() {
29
+ const sampler = process.env.OTEL_TRACES_SAMPLER?.trim().toLowerCase() || "parentbased_always_on";
30
+ const parentBased = sampler.startsWith("parentbased_");
31
+ switch (parentBased ? sampler.slice("parentbased_".length) : sampler) {
32
+ case "always_on":
33
+ return { ratio: 1, parentBased };
34
+ case "always_off":
35
+ return { ratio: 0, parentBased };
36
+ case "traceidratio": {
37
+ const ratio = envNumber("OTEL_TRACES_SAMPLER_ARG");
38
+ return { ratio: ratio !== void 0 && ratio >= 0 && ratio <= 1 ? ratio : 1, parentBased };
39
+ }
40
+ default:
41
+ return void 0;
42
+ }
43
+ }
44
+ export function envSampler() {
45
+ const config = envSamplerConfig();
46
+ if (!config) return void 0;
47
+ const root = config.ratio >= 1 ? new AlwaysOnSampler() : config.ratio <= 0 ? new AlwaysOffSampler() : new TraceIdRatioBasedSampler(config.ratio);
48
+ return config.parentBased ? new ParentBasedSampler({ root }) : root;
49
+ }
50
+ export function otelSampling() {
51
+ const slowMs = envNumber("OTEL_TRACES_SLOW_MS") ?? otelRuntimeConfig().slowMs ?? 0;
52
+ if (slowMs <= 0) return void 0;
53
+ const ratio = envSamplerConfig()?.ratio;
54
+ if (ratio === void 0) {
55
+ logger.warn("otel_config_invalid", { error: `OTEL_TRACES_SLOW_MS ignored: unsupported OTEL_TRACES_SAMPLER=${process.env.OTEL_TRACES_SAMPLER}` });
56
+ return void 0;
57
+ }
58
+ return ratio < 1 ? { ratio, slowMs } : void 0;
59
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Tracer provider з OTLP gRPC експортером.
3
+ *
4
+ * Env, які SDK читає сам:
5
+ * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT
6
+ * (insecure чи TLS — за схемою URL). Без них трейсинг не вмикається:
7
+ * експортер SDK інакше мовчки слав би на localhost:4317.
8
+ * - OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG (типово parentbased_always_on).
9
+ *
10
+ * OTEL_TRACES_SLOW_MS (config.ts → otelSampling) — порятунок трейсів поза
11
+ * вибіркою, чий root тривав довше порогу (див. sampling.ts). Тоді семплер —
12
+ * HeadSampler з тією ж часткою, що в OTEL_TRACES_SAMPLER_ARG.
13
+ *
14
+ * `?force_otel=1` у запиті — трейс вибраний завжди (ForceSampler, як у libwts3).
15
+ *
16
+ * Root trace id — з `X-Request-ID`, див. requestTraceId.ts.
17
+ *
18
+ * service.name: env OTEL_SERVICE_NAME → `logger.projectName`.
19
+ */
20
+ declare const _default: import("nitropack").NitroAppPlugin;
21
+ export default _default;
@@ -0,0 +1,94 @@
1
+ import { defineNitroPlugin } from "nitropack/runtime";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { context, diag, DiagLogLevel, propagation, trace } from "@opentelemetry/api";
4
+ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
5
+ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
6
+ import { resourceFromAttributes } from "@opentelemetry/resources";
7
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
8
+ import { registerInstrumentations } from "@opentelemetry/instrumentation";
9
+ import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
10
+ import { logger } from "../../logger.js";
11
+ import { currentRequestEvent, requestIdOf } from "../requestContext.js";
12
+ import { serverSpanContext } from "./serverSpan.js";
13
+ import { requestIdGenerator } from "./requestTraceId.js";
14
+ import { envSampler, otelSampling } from "./config.js";
15
+ import { ForceSampler, HeadSampler, SlowTraceSpanProcessor } from "./sampling.js";
16
+ const diagLogger = {
17
+ error: (message, ...args) => logger.error("otel_diag", { error: message, args }),
18
+ warn: (message, ...args) => logger.warn("otel_diag", { error: message, args }),
19
+ info: () => {
20
+ },
21
+ debug: () => {
22
+ },
23
+ verbose: () => {
24
+ }
25
+ };
26
+ function hasHeader(request, name) {
27
+ const { headers } = request;
28
+ if (typeof headers === "string") return new RegExp(`(^|\\r\\n)${name}:`, "i").test(headers);
29
+ for (let i = 0; i < headers.length; i += 2) {
30
+ if (String(headers[i]).toLowerCase() === name) return true;
31
+ }
32
+ return false;
33
+ }
34
+ function undiciInstrumentation() {
35
+ return new UndiciInstrumentation({
36
+ requireParentforSpans: true,
37
+ requestHook: (span, request) => {
38
+ span.updateName(`${request.method} ${new URL(request.origin).hostname}`);
39
+ const reqid = requestIdOf(currentRequestEvent());
40
+ if (reqid && !hasHeader(request, "x-request-id")) request.addHeader("x-request-id", reqid);
41
+ }
42
+ });
43
+ }
44
+ export default defineNitroPlugin((nitroApp) => {
45
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
46
+ if (!endpoint) {
47
+ logger.warn("otel_disabled", { error: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT not set" });
48
+ return;
49
+ }
50
+ diag.setLogger(diagLogger, DiagLogLevel.WARN);
51
+ const serviceName = process.env.OTEL_SERVICE_NAME || String(useRuntimeConfig().public.projectName || "hotelme");
52
+ const batchProcessor = new BatchSpanProcessor(new OTLPTraceExporter(), {
53
+ maxQueueSize: 2048,
54
+ maxExportBatchSize: 512,
55
+ scheduledDelayMillis: 5e3,
56
+ exportTimeoutMillis: 1e4
57
+ });
58
+ const sampling = otelSampling();
59
+ const spanProcessor = sampling ? new SlowTraceSpanProcessor(batchProcessor, sampling.slowMs) : batchProcessor;
60
+ const baseSampler = sampling ? new HeadSampler(sampling.ratio) : envSampler();
61
+ const provider = new NodeTracerProvider({
62
+ resource: resourceFromAttributes({ "service.name": serviceName }),
63
+ ...baseSampler && { sampler: new ForceSampler(baseSampler) },
64
+ spanProcessors: [spanProcessor],
65
+ idGenerator: requestIdGenerator
66
+ });
67
+ provider.register();
68
+ const unregisterInstrumentations = registerInstrumentations({
69
+ tracerProvider: provider,
70
+ instrumentations: [undiciInstrumentation()]
71
+ });
72
+ const handler = nitroApp.h3App.handler;
73
+ nitroApp.h3App.handler = (event) => context.with(serverSpanContext(event), () => handler(event));
74
+ nitroApp.hooks.hook("close", async () => {
75
+ unregisterInstrumentations();
76
+ try {
77
+ await provider.shutdown();
78
+ } catch (error) {
79
+ logger.error("otel_shutdown_failed", { error: error instanceof Error ? error.message : String(error) });
80
+ } finally {
81
+ trace.disable();
82
+ propagation.disable();
83
+ context.disable();
84
+ }
85
+ });
86
+ logger.info("otel_initialized", {
87
+ service: serviceName,
88
+ endpoint,
89
+ sampler: process.env.OTEL_TRACES_SAMPLER || "parentbased_always_on",
90
+ sampler_arg: process.env.OTEL_TRACES_SAMPLER_ARG,
91
+ slow_ms: sampling?.slowMs,
92
+ force_otel: !!baseSampler
93
+ });
94
+ });
@@ -0,0 +1,10 @@
1
+ import { type IdGenerator } from '@opentelemetry/sdk-trace-base';
2
+ /**
3
+ * Trace id як у libwts3: `X-Request-ID` поточного запиту без дефісів
4
+ * (`2e285875-f20c-4c86-b6c0-dead22613ca9` → `2e285875f20c4c86b6c0dead22613ca9`),
5
+ * щоб reqid з логів напряму відкривав трейс. Не UUID / 32 hex — випадковий.
6
+ *
7
+ * Tracer викликає generateTraceId лише для span'а без батька, тобто server
8
+ * span'а запиту без вхідного `traceparent`.
9
+ */
10
+ export declare const requestIdGenerator: IdGenerator;
@@ -0,0 +1,11 @@
1
+ import { isValidTraceId } from "@opentelemetry/api";
2
+ import { RandomIdGenerator } from "@opentelemetry/sdk-trace-base";
3
+ import { currentRequestEvent, requestIdOf } from "../requestContext.js";
4
+ const random = new RandomIdGenerator();
5
+ export const requestIdGenerator = {
6
+ generateTraceId: () => {
7
+ const traceId = requestIdOf(currentRequestEvent())?.replace(/-/g, "").toLowerCase();
8
+ return traceId && /^[0-9a-f]{32}$/.test(traceId) && isValidTraceId(traceId) ? traceId : random.generateTraceId();
9
+ },
10
+ generateSpanId: () => random.generateSpanId()
11
+ };
@@ -0,0 +1,64 @@
1
+ import { type Attributes, type Context, type Link, type SpanKind } from '@opentelemetry/api';
2
+ import { type ReadableSpan, type Sampler, type SamplingResult, type Span, type SpanProcessor } from '@opentelemetry/sdk-trace-base';
3
+ /** Контекст, у якому ForceSampler вибирає кожен span — його й усіх дочірніх. */
4
+ export declare function forceSampled(context: Context): Context;
5
+ /**
6
+ * `?force_otel=1` як у libwts3: запит вибраний попри частку семплера, фільтри
7
+ * й вхідний `traceparent …-00`. Позначка живе в контексті, тож дочірні span'и
8
+ * (внутрішні `/api/*`, вихідні fetch) теж вибрані, а бекенд отримує
9
+ * `traceparent …-01` і пише свою частину повністю. Решта — `base`.
10
+ */
11
+ export declare class ForceSampler implements Sampler {
12
+ private readonly base;
13
+ constructor(base: Sampler);
14
+ shouldSample(context: Context, traceId: string, spanName: string, spanKind: SpanKind, attributes: Attributes, links: Link[]): SamplingResult;
15
+ toString(): string;
16
+ }
17
+ /**
18
+ * `parentbased_traceidratio`, у якого решта трейсів — RECORD замість
19
+ * NOT_RECORD: span'и пишуться в пам'ять для SlowTraceSpanProcessor, але
20
+ * назовні йде `traceparent …-00`: бекенд вирішує сам (libwts3 теж рятує
21
+ * повільні).
22
+ *
23
+ * Батько вибраний → вибраний. Невибраний батько:
24
+ * - власний record-only span або вхідний `traceparent …-00` → RECORD;
25
+ * - unsampledContext із serverSpan.ts (статика, ignorePaths, ignoreUserAgents) —
26
+ * локальний non-recording span → NOT_RECORD, фільтри працюють як раніше.
27
+ */
28
+ export declare class HeadSampler implements Sampler {
29
+ private readonly ratio;
30
+ constructor(ratio: number);
31
+ shouldSample(context: Context, traceId: string): SamplingResult;
32
+ toString(): string;
33
+ }
34
+ /**
35
+ * Порятунок повільних трейсів, які не потрапили в head-вибірку — як
36
+ * OTEL_TRACES_SLOW_MS у libwts3: рішення на весь запит, а не на окремий span.
37
+ *
38
+ * Вибрані span'и йдуть у `next` одразу. Record-only (див. HeadSampler)
39
+ * буферизуються до кінця свого локального root'а — server span'а без батька
40
+ * або з віддаленим батьком. Root тривав ≥ `slowMs` → увесь запит іде в `next`
41
+ * з прапорцем sampled, root — з `sampling.kept_by: "slow"`; інакше буфер
42
+ * скидається. Бекенд отримав `traceparent …-00`, тож його span'и в трейсі є,
43
+ * лише якщо він сам їх врятував (libwts3: тривав ≥ свого OTEL_TRACES_SLOW_MS).
44
+ *
45
+ * Ключ буфера — span id локального root'а, не trace id: кілька запитів з одним
46
+ * trace id (один вхідний `traceparent`, повтор `X-Request-ID`) вирішуються
47
+ * кожен за своєю тривалістю.
48
+ *
49
+ * Рішення пам'ятається, щоб span'и, які закінчились після root'а
50
+ * (fire-and-forget fetch), пішли за ним, а не зависли в буфері.
51
+ */
52
+ export declare class SlowTraceSpanProcessor implements SpanProcessor {
53
+ private readonly next;
54
+ private readonly slowMs;
55
+ /** Record-only span → span id його локального root'а */
56
+ private readonly roots;
57
+ private readonly pending;
58
+ private readonly decisions;
59
+ constructor(next: SpanProcessor, slowMs: number);
60
+ onStart(span: Span, parentContext: Context): void;
61
+ onEnd(span: ReadableSpan): void;
62
+ forceFlush(): Promise<void>;
63
+ shutdown(): Promise<void>;
64
+ }
@@ -0,0 +1,126 @@
1
+ import { createContextKey, isSpanContextValid, trace, TraceFlags } from "@opentelemetry/api";
2
+ import { hrTimeToMilliseconds } from "@opentelemetry/core";
3
+ import {
4
+ SamplingDecision,
5
+ TraceIdRatioBasedSampler
6
+ } from "@opentelemetry/sdk-trace-base";
7
+ const SAMPLED = { decision: SamplingDecision.RECORD_AND_SAMPLED };
8
+ const RECORDED = { decision: SamplingDecision.RECORD };
9
+ const DROPPED = { decision: SamplingDecision.NOT_RECORD };
10
+ const FORCED = createContextKey("hotelme-nuxt-logger.force_otel");
11
+ export function forceSampled(context) {
12
+ return context.setValue(FORCED, true);
13
+ }
14
+ export class ForceSampler {
15
+ constructor(base) {
16
+ this.base = base;
17
+ }
18
+ shouldSample(context, traceId, spanName, spanKind, attributes, links) {
19
+ if (context.getValue(FORCED)) return SAMPLED;
20
+ return this.base.shouldSample(context, traceId, spanName, spanKind, attributes, links);
21
+ }
22
+ toString() {
23
+ return `ForceSampler{${this.base}}`;
24
+ }
25
+ }
26
+ export class HeadSampler {
27
+ ratio;
28
+ constructor(ratio) {
29
+ this.ratio = new TraceIdRatioBasedSampler(ratio);
30
+ }
31
+ shouldSample(context, traceId) {
32
+ const parentSpan = trace.getSpan(context);
33
+ const parent = parentSpan?.spanContext();
34
+ if (!parentSpan || !parent || !isSpanContextValid(parent)) {
35
+ return this.ratio.shouldSample(context, traceId).decision === SamplingDecision.RECORD_AND_SAMPLED ? SAMPLED : RECORDED;
36
+ }
37
+ if (parent.traceFlags & TraceFlags.SAMPLED) return SAMPLED;
38
+ return parent.isRemote || parentSpan.isRecording() ? RECORDED : DROPPED;
39
+ }
40
+ toString() {
41
+ return `HeadSampler{${this.ratio}}`;
42
+ }
43
+ }
44
+ const MAX_PENDING_TRACES = 2e3;
45
+ const MAX_SPANS_PER_TRACE = 512;
46
+ const MAX_DECISIONS = 2e3;
47
+ function rememberBounded(map, key, value, max) {
48
+ if (map.size >= max) map.delete(map.keys().next().value);
49
+ map.set(key, value);
50
+ }
51
+ function isLocalRoot(span) {
52
+ return !span.parentSpanContext || !!span.parentSpanContext.isRemote;
53
+ }
54
+ function asSampled(span, extraAttributes) {
55
+ const spanContext = { ...span.spanContext(), traceFlags: span.spanContext().traceFlags | TraceFlags.SAMPLED };
56
+ return {
57
+ name: span.name,
58
+ kind: span.kind,
59
+ spanContext: () => spanContext,
60
+ parentSpanContext: span.parentSpanContext,
61
+ startTime: span.startTime,
62
+ endTime: span.endTime,
63
+ status: span.status,
64
+ attributes: extraAttributes ? { ...span.attributes, ...extraAttributes } : span.attributes,
65
+ links: span.links,
66
+ events: span.events,
67
+ duration: span.duration,
68
+ ended: span.ended,
69
+ resource: span.resource,
70
+ instrumentationScope: span.instrumentationScope,
71
+ droppedAttributesCount: span.droppedAttributesCount,
72
+ droppedEventsCount: span.droppedEventsCount,
73
+ droppedLinksCount: span.droppedLinksCount
74
+ };
75
+ }
76
+ export class SlowTraceSpanProcessor {
77
+ constructor(next, slowMs) {
78
+ this.next = next;
79
+ this.slowMs = slowMs;
80
+ }
81
+ /** Record-only span → span id його локального root'а */
82
+ roots = /* @__PURE__ */ new WeakMap();
83
+ pending = /* @__PURE__ */ new Map();
84
+ decisions = /* @__PURE__ */ new Map();
85
+ onStart(span, parentContext) {
86
+ if (!(span.spanContext().traceFlags & TraceFlags.SAMPLED)) {
87
+ const parent = trace.getSpan(parentContext);
88
+ const root = !isLocalRoot(span) && parent ? this.roots.get(parent) : void 0;
89
+ this.roots.set(span, root ?? span.spanContext().spanId);
90
+ }
91
+ this.next.onStart(span, parentContext);
92
+ }
93
+ onEnd(span) {
94
+ if (span.spanContext().traceFlags & TraceFlags.SAMPLED) return this.next.onEnd(span);
95
+ const root = this.roots.get(span) ?? span.spanContext().spanId;
96
+ const decision = this.decisions.get(root);
97
+ if (decision !== void 0) {
98
+ if (decision) this.next.onEnd(asSampled(span));
99
+ return;
100
+ }
101
+ if (!isLocalRoot(span)) {
102
+ let spans2 = this.pending.get(root);
103
+ if (!spans2) {
104
+ spans2 = [];
105
+ rememberBounded(this.pending, root, spans2, MAX_PENDING_TRACES);
106
+ }
107
+ if (spans2.length < MAX_SPANS_PER_TRACE) spans2.push(span);
108
+ return;
109
+ }
110
+ const spans = this.pending.get(root) ?? [];
111
+ this.pending.delete(root);
112
+ const keep = hrTimeToMilliseconds(span.duration) >= this.slowMs;
113
+ rememberBounded(this.decisions, root, keep, MAX_DECISIONS);
114
+ if (!keep) return;
115
+ for (const child of spans) this.next.onEnd(asSampled(child));
116
+ this.next.onEnd(asSampled(span, { "sampling.kept_by": "slow" }));
117
+ }
118
+ forceFlush() {
119
+ return this.next.forceFlush();
120
+ }
121
+ shutdown() {
122
+ this.pending.clear();
123
+ this.decisions.clear();
124
+ return this.next.shutdown();
125
+ }
126
+ }
@@ -0,0 +1,29 @@
1
+ import { type H3Event } from 'h3';
2
+ import { type Context } from '@opentelemetry/api';
3
+ declare module 'h3' {
4
+ interface H3EventContext {
5
+ /** Назва роута сторінки з Vue Router, ставить plugins/otelPageName.server */
6
+ otelPageName?: string;
7
+ }
8
+ }
9
+ /**
10
+ * Контекст, у якому виконується весь pipeline запиту (див. plugin.ts).
11
+ *
12
+ * Server span з батьком із вхідного W3C `traceparent`, а для внутрішнього
13
+ * SSR-виклику (`$fetch('/api/…')` → localFetch) — з активного контексту
14
+ * сторінки: localFetch іде в тому ж async-ланцюжку, тож span роуту стає
15
+ * дочірнім без жодних заголовків. Без батька trace id — з `X-Request-ID`
16
+ * (requestIdGenerator).
17
+ *
18
+ * Відфільтровані запити (статика, ignorePaths, ignoreUserAgents) отримують
19
+ * невибраний контекст, а не просто відсутність span'а: інакше їхні внутрішні
20
+ * /api-виклики, вихідні fetch і span'и бекенда стали б осиротілими
21
+ * root-трейсами. Дочірні запити успадковують його через parent-based семплер.
22
+ *
23
+ * `?force_otel=1` (як у libwts3) — трейс вибраний попри фільтри, частку
24
+ * семплера й вхідний `traceparent …-00`, root з `sampling.kept_by: "force"`.
25
+ *
26
+ * Кладе в event.context `otelSpan` (назва сторінки, `i18n.locale`), віддає
27
+ * `X-Trace-Id` у відповіді.
28
+ */
29
+ export declare function serverSpanContext(event: H3Event): Context;
@@ -0,0 +1,69 @@
1
+ import { getHeader, setResponseHeader } from "h3";
2
+ import { context, isSpanContextValid, propagation, trace, SpanKind, SpanStatusCode, TraceFlags } from "@opentelemetry/api";
3
+ import { isTracingSuppressed } from "@opentelemetry/core";
4
+ import { requestIdOf } from "../requestContext.js";
5
+ import { otelSettings, TRACER_NAME } from "./config.js";
6
+ import { forceSampled } from "./sampling.js";
7
+ import { requestIdGenerator } from "./requestTraceId.js";
8
+ const STATIC_ASSET = /\.(css|m?js|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|map|json|txt|xml)$/i;
9
+ function unsampledContext(active) {
10
+ return trace.setSpanContext(active, {
11
+ traceId: requestIdGenerator.generateTraceId(),
12
+ spanId: requestIdGenerator.generateSpanId(),
13
+ traceFlags: TraceFlags.NONE
14
+ });
15
+ }
16
+ function resolvedRouteName(event) {
17
+ if (event.context.otelPageName) return event.context.otelPageName;
18
+ const route = event.context.matchedRoute?.path;
19
+ return route && route !== "/**" ? route : void 0;
20
+ }
21
+ export function serverSpanContext(event) {
22
+ const active = context.active();
23
+ const settings = otelSettings();
24
+ const userAgent = getHeader(event, "user-agent") ?? "";
25
+ const [pathname = "", query = ""] = event.path.split("?");
26
+ if (isTracingSuppressed(active)) return active;
27
+ const forced = new URLSearchParams(query).get("force_otel") === "1";
28
+ if (!forced && (settings.ignoreUserAgent?.test(userAgent) || settings.ignorePaths.some((prefix) => pathname.startsWith(prefix)) || settings.ignoreStaticAssets && STATIC_ASSET.test(pathname))) {
29
+ return unsampledContext(active);
30
+ }
31
+ const carrier = {};
32
+ const traceparent = getHeader(event, "traceparent");
33
+ const tracestate = getHeader(event, "tracestate");
34
+ if (traceparent) carrier.traceparent = traceparent;
35
+ if (tracestate) carrier.tracestate = tracestate;
36
+ const extracted = propagation.extract(active, carrier);
37
+ const parentCtx = forced ? forceSampled(extracted) : extracted;
38
+ const span = trace.getTracer(TRACER_NAME).startSpan(
39
+ `${event.method} ${pathname.startsWith("/api/") ? "/api/*" : "/*"}`,
40
+ {
41
+ kind: SpanKind.SERVER,
42
+ attributes: {
43
+ "http.request.method": event.method,
44
+ "url.path": pathname,
45
+ "url.query": query,
46
+ "server.address": getHeader(event, "host") ?? "",
47
+ "user_agent.original": userAgent,
48
+ ...forced && { "sampling.kept_by": "force" }
49
+ }
50
+ },
51
+ parentCtx
52
+ );
53
+ event.context.otelSpan = span;
54
+ if (isSpanContextValid(span.spanContext())) {
55
+ setResponseHeader(event, "X-Trace-Id", span.spanContext().traceId);
56
+ }
57
+ const res = event.node.res;
58
+ res.once("close", () => {
59
+ const route = resolvedRouteName(event);
60
+ if (route) span.updateName(`${event.method} ${route}`);
61
+ span.setAttribute("request_id", requestIdOf(event) ?? "");
62
+ const statusSent = res.headersSent || res.writableFinished;
63
+ if (statusSent) span.setAttribute("http.response.status_code", res.statusCode);
64
+ if (!res.writableFinished) span.setAttribute("error.type", "aborted");
65
+ if (statusSent && res.statusCode >= 500) span.setStatus({ code: SpanStatusCode.ERROR });
66
+ span.end();
67
+ });
68
+ return trace.setSpan(parentCtx, span);
69
+ }
@@ -0,0 +1,14 @@
1
+ import type { H3Event } from 'h3';
2
+ import type { Span } from '@opentelemetry/api';
3
+ declare module 'h3' {
4
+ interface H3EventContext {
5
+ otelSpan?: Span;
6
+ }
7
+ }
8
+ /**
9
+ * `X-Request-ID` сторінки для вихідного запиту — щоб access-log бекенда мав
10
+ * той самий id, що й `stats`/`api request` цього pod'а. Подія — явна або
11
+ * поточна з AsyncLocalStorage. `traceparent` тут немає: його в кожен fetch
12
+ * додає instrumentation-undici, другий такий заголовок зламав би зв'язок.
13
+ */
14
+ export declare function traceHeaders(event?: H3Event | undefined): Record<string, string>;
@@ -0,0 +1,5 @@
1
+ import { currentRequestEvent, requestIdOf } from "../requestContext.js";
2
+ export function traceHeaders(event = currentRequestEvent()) {
3
+ const reqid = requestIdOf(event);
4
+ return reqid ? { "X-Request-ID": reqid } : {};
5
+ }
@@ -0,0 +1,15 @@
1
+ import type { H3Event } from 'h3';
2
+ /**
3
+ * H3-подія запиту, всередині якого виконується поточний код (middleware,
4
+ * handler, SSR-рендер, їхні await'и). `undefined` поза запитом.
5
+ */
6
+ export declare function currentRequestEvent(): H3Event | undefined;
7
+ /** reqid події: від logger-middleware або сирий `X-Request-ID`. */
8
+ export declare function requestIdOf(event: H3Event | undefined): string | undefined;
9
+ /**
10
+ * Запускає `handler` з подією в AsyncLocalStorage. Внутрішній SSR-виклик
11
+ * (`$fetch('/api/…')` → Nitro localFetch) іде в тому ж async-ланцюжку, тож тут
12
+ * видно батьківську подію: її reqid стає `X-Request-ID` дочірнього запиту, і
13
+ * logger-middleware не генерує новий.
14
+ */
15
+ export declare function runInRequestContext<T>(event: H3Event, handler: () => T): T;
@@ -0,0 +1,16 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const storage = new AsyncLocalStorage();
3
+ export function currentRequestEvent() {
4
+ return storage.getStore();
5
+ }
6
+ export function requestIdOf(event) {
7
+ const reqid = event?.context.reqid || event?.node.req.headers["x-request-id"];
8
+ return typeof reqid === "string" && reqid ? reqid : void 0;
9
+ }
10
+ export function runInRequestContext(event, handler) {
11
+ const parentReqid = requestIdOf(storage.getStore());
12
+ if (parentReqid && !event.node.req.headers["x-request-id"]) {
13
+ event.node.req.headers["x-request-id"] = parentReqid;
14
+ }
15
+ return storage.run(event, handler);
16
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "hotelme-nuxt-logger",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Стандартний logger для Nuxt проектів з JSON виводом сумісним з бекенд-логами",
5
5
  "keywords": [
6
6
  "nuxt",
7
7
  "nuxt-module",
8
8
  "logger",
9
9
  "logging",
10
- "json-logger"
10
+ "json-logger",
11
+ "opentelemetry"
11
12
  ],
12
13
  "repository": {
13
14
  "type": "git",
@@ -24,6 +25,10 @@
24
25
  "./runtime/logger": {
25
26
  "types": "./dist/runtime/logger.d.ts",
26
27
  "import": "./dist/runtime/logger.js"
28
+ },
29
+ "./runtime/otel": {
30
+ "types": "./dist/runtime/server/otel/tracing.d.ts",
31
+ "import": "./dist/runtime/server/otel/tracing.js"
27
32
  }
28
33
  },
29
34
  "main": "./dist/module.cjs",
@@ -41,7 +46,15 @@
41
46
  "test": "echo \"No tests yet\""
42
47
  },
43
48
  "dependencies": {
44
- "@nuxt/kit": "^3.0.0"
49
+ "@nuxt/kit": "^3.0.0",
50
+ "@opentelemetry/api": "^1.9.1",
51
+ "@opentelemetry/core": "^2.11.0",
52
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
53
+ "@opentelemetry/instrumentation": "^0.222.0",
54
+ "@opentelemetry/instrumentation-undici": "^0.32.0",
55
+ "@opentelemetry/resources": "^2.11.0",
56
+ "@opentelemetry/sdk-trace-base": "^2.11.0",
57
+ "@opentelemetry/sdk-trace-node": "^2.11.0"
45
58
  },
46
59
  "devDependencies": {
47
60
  "@nuxt/module-builder": "^0.8.0",