hotelme-nuxt-logger 1.3.0 → 1.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/README.md CHANGED
@@ -117,6 +117,8 @@ logger.warn('Rate limit approaching', { remaining: 10 })
117
117
 
118
118
  ### В API handlers
119
119
 
120
+ `logger` та `getRequestId` auto-import'яться і в серверному коді Nitro (з 1.4.0). Явний імпорт, якщо треба: `import { logger } from 'hotelme-nuxt-logger/runtime/logger'`.
121
+
120
122
  ```typescript
121
123
  // server/api/hotels.get.ts
122
124
  export default defineEventHandler((event) => {
@@ -149,6 +151,52 @@ const response = await $fetch('/api/hotels', {
149
151
  })
150
152
  ```
151
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
+ },
167
+ }
168
+ ```
169
+
170
+ Env (читає SDK):
171
+
172
+ | Змінна | Типово |
173
+ |---|---|
174
+ | `OTEL_SERVICE_NAME` | `projectName` |
175
+ | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_ENDPOINT` | — (обов'язкова: без неї трейсинг не вмикається, лог `otel_disabled`) |
176
+ | `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG` | `parentbased_always_on` |
177
+
178
+ Що робить модуль:
179
+
180
+ - **Активний контекст на весь запит.** Плагін обгортає `nitroApp.h3App.handler`, тож middleware, handler'и та SSR-рендер виконуються всередині `context.with(serverSpan)` — span'и з коду застосунку стають дочірніми без обгорток.
181
+ - **Server span** на кожен запит, батько — вхідний `traceparent`. Без нього trace id = `X-Request-ID` без дефісів (`2e285875-f20c-…` → `2e285875f20c…`, як у libwts3), якщо це UUID / 32 hex; інакше випадковий. У `event.context`: `otelSpan`; у відповіді — `X-Trace-Id`. 5xx → статус ERROR, 4xx — ні.
182
+ - **Вихідні fetch** (native `fetch`, `$fetch`, ofetch на зовнішній URL) — `@opentelemetry/instrumentation-undici` через diagnostics_channel: CLIENT span `GET <host>` (повний URL — в `url.full`), `traceparent` і `X-Request-ID` сторінки, якщо виклик не поставив свій. Поза запитом span'и не пишуться.
183
+ - **Внутрішні SSR-виклики** (`$fetch('/api/…')` → Nitro localFetch) ідуть у тому ж async-ланцюжку, тож span роуту стає дочірнім span'а сторінки, а logger-middleware бере reqid сторінки. Заголовки руками не потрібні.
184
+ - **Назва span'а** — шаблон, ніколи сирий шлях (кожна назва — окремий stream у VictoriaTraces), визначається сама після роутингу:
185
+ - сторінка → `route.name` з Vue Router без i18n-суфікса: `GET weather-citynameid`, `GET index` (аліаси теж). Локаль — в атрибуті `i18n.locale`;
186
+ - Nitro-хендлер → шаблон з файлової структури: `GET /api/city_info`, `GET /record/**`;
187
+ - решта (відповідь з server middleware, невідомий шлях) → `GET /api/*` / `GET /*`; точний шлях — в `url.path`, статус — в `http.response.status_code`.
188
+ - **Фільтри** (статика, `ignorePaths`, `ignoreUserAgents`) не міняють поведінку запиту: він обробляється як завжди, але в невибраному (sampled=0) контексті — без span'ів у нього, його внутрішніх викликів і бекенда (`traceparent …-00`). Потрібен parent-based семплер (типовий).
189
+ - **Flush при зупинці**: span'и батчуються до 5 с, на `close` provider робить shutdown.
190
+ - Помилки експорту (`diag`) пишуться JSON-логером як `otel_diag`.
191
+
192
+ Хелпер (auto-import у Nitro або `import … from 'hotelme-nuxt-logger/runtime/otel'`), працює і з вимкненим трейсингом:
193
+
194
+ ```typescript
195
+ // X-Request-ID сторінки (явна подія або поточна) — для бекендів, коли OTel вимкнено
196
+ const headers = traceHeaders(event)
197
+
198
+ ```
199
+
152
200
  ## API
153
201
 
154
202
  ### 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))));
@@ -26,6 +32,16 @@ const module$1 = kit.defineNuxtModule({
26
32
  const clientErrorEndpoint = options.clientErrorEndpoint || "/_logger/client-error";
27
33
  nuxt.options.runtimeConfig.public.loggerClientErrorEndpoint = clientErrorEndpoint;
28
34
  kit.addImportsDir(resolver.resolve("./runtime"));
35
+ kit.addServerImports([
36
+ { name: "logger", from: resolver.resolve("./runtime/logger") },
37
+ { name: "getRequestId", from: resolver.resolve("./runtime/logger") },
38
+ { name: "traceHeaders", from: resolver.resolve("./runtime/server/otel/tracing") }
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
+ };
29
45
  if (options.enableRequestLogging) {
30
46
  kit.addServerHandler({
31
47
  handler: resolver.resolve("./runtime/middleware/logger.server"),
@@ -46,6 +62,14 @@ const module$1 = kit.defineNuxtModule({
46
62
  handler: resolver.resolve("./runtime/server/routes/client-error.post")
47
63
  });
48
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"));
49
73
  }
50
74
  });
51
75
 
package/dist/module.d.cts CHANGED
@@ -27,8 +27,37 @@ 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[];
30
59
  }
31
60
  declare const _default: any;
32
61
 
33
62
  export { _default as default };
34
- export type { ModuleOptions };
63
+ export type { ModuleOptions, OtelOptions };
package/dist/module.d.mts CHANGED
@@ -27,8 +27,37 @@ 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[];
30
59
  }
31
60
  declare const _default: any;
32
61
 
33
62
  export { _default as default };
34
- export type { ModuleOptions };
63
+ export type { ModuleOptions, OtelOptions };
package/dist/module.d.ts CHANGED
@@ -27,8 +27,37 @@ 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[];
30
59
  }
31
60
  declare const _default: any;
32
61
 
33
62
  export { _default as default };
34
- export type { ModuleOptions };
63
+ 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.3.0",
7
+ "version": "1.5.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addImportsDir, addServerHandler, addServerPlugin, addPlugin } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addImportsDir, addServerImports, addServerHandler, addServerPlugin, addPlugin } from '@nuxt/kit';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname } from 'path';
4
4
 
@@ -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));
@@ -32,6 +38,16 @@ const module = defineNuxtModule({
32
38
  const clientErrorEndpoint = options.clientErrorEndpoint || "/_logger/client-error";
33
39
  nuxt.options.runtimeConfig.public.loggerClientErrorEndpoint = clientErrorEndpoint;
34
40
  addImportsDir(resolver.resolve("./runtime"));
41
+ addServerImports([
42
+ { name: "logger", from: resolver.resolve("./runtime/logger") },
43
+ { name: "getRequestId", from: resolver.resolve("./runtime/logger") },
44
+ { name: "traceHeaders", from: resolver.resolve("./runtime/server/otel/tracing") }
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
+ };
35
51
  if (options.enableRequestLogging) {
36
52
  addServerHandler({
37
53
  handler: resolver.resolve("./runtime/middleware/logger.server"),
@@ -52,6 +68,14 @@ const module = defineNuxtModule({
52
68
  handler: resolver.resolve("./runtime/server/routes/client-error.post")
53
69
  });
54
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"));
55
79
  }
56
80
  });
57
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,15 @@
1
+ /** Серіалізована форма `logger.otel` з nuxt.config (див. module.ts). */
2
+ export interface OtelRuntimeConfig {
3
+ ignorePaths?: string[];
4
+ ignoreStaticAssets?: boolean;
5
+ ignoreUserAgents?: string[];
6
+ }
7
+ export interface OtelSettings {
8
+ ignorePaths: string[];
9
+ ignoreStaticAssets: boolean;
10
+ ignoreUserAgent?: RegExp;
11
+ }
12
+ export declare const TRACER_NAME = "hotelme-nuxt-logger";
13
+ export declare function otelRuntimeConfig(): OtelRuntimeConfig;
14
+ /** Регекспи компілюються один раз, на першому запиті. */
15
+ export declare function otelSettings(): OtelSettings;
@@ -0,0 +1,17 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ export const TRACER_NAME = "hotelme-nuxt-logger";
3
+ export function otelRuntimeConfig() {
4
+ return useRuntimeConfig().logger?.otel ?? {};
5
+ }
6
+ let settings;
7
+ export function otelSettings() {
8
+ if (settings) return settings;
9
+ const config = otelRuntimeConfig();
10
+ const userAgents = (config.ignoreUserAgents ?? []).filter(Boolean);
11
+ settings = {
12
+ ignorePaths: config.ignorePaths ?? [],
13
+ ignoreStaticAssets: config.ignoreStaticAssets ?? true,
14
+ ignoreUserAgent: userAgents.length ? new RegExp(userAgents.map((ua) => `(?:${ua})`).join("|"), "i") : void 0
15
+ };
16
+ return settings;
17
+ }
@@ -0,0 +1,15 @@
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
+ * Root trace id — з `X-Request-ID`, див. requestTraceId.ts.
11
+ *
12
+ * service.name: env OTEL_SERVICE_NAME → `logger.projectName`.
13
+ */
14
+ declare const _default: import("nitropack").NitroAppPlugin;
15
+ export default _default;
@@ -0,0 +1,81 @@
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
+ const diagLogger = {
15
+ error: (message, ...args) => logger.error("otel_diag", { error: message, args }),
16
+ warn: (message, ...args) => logger.warn("otel_diag", { error: message, args }),
17
+ info: () => {
18
+ },
19
+ debug: () => {
20
+ },
21
+ verbose: () => {
22
+ }
23
+ };
24
+ function hasHeader(request, name) {
25
+ const { headers } = request;
26
+ if (typeof headers === "string") return new RegExp(`(^|\\r\\n)${name}:`, "i").test(headers);
27
+ for (let i = 0; i < headers.length; i += 2) {
28
+ if (String(headers[i]).toLowerCase() === name) return true;
29
+ }
30
+ return false;
31
+ }
32
+ function undiciInstrumentation() {
33
+ return new UndiciInstrumentation({
34
+ requireParentforSpans: true,
35
+ requestHook: (span, request) => {
36
+ span.updateName(`${request.method} ${new URL(request.origin).hostname}`);
37
+ const reqid = requestIdOf(currentRequestEvent());
38
+ if (reqid && !hasHeader(request, "x-request-id")) request.addHeader("x-request-id", reqid);
39
+ }
40
+ });
41
+ }
42
+ export default defineNitroPlugin((nitroApp) => {
43
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
44
+ if (!endpoint) {
45
+ logger.warn("otel_disabled", { error: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT not set" });
46
+ return;
47
+ }
48
+ diag.setLogger(diagLogger, DiagLogLevel.WARN);
49
+ const serviceName = process.env.OTEL_SERVICE_NAME || String(useRuntimeConfig().public.projectName || "hotelme");
50
+ const spanProcessor = new BatchSpanProcessor(new OTLPTraceExporter(), {
51
+ maxQueueSize: 2048,
52
+ maxExportBatchSize: 512,
53
+ scheduledDelayMillis: 5e3,
54
+ exportTimeoutMillis: 1e4
55
+ });
56
+ const provider = new NodeTracerProvider({
57
+ resource: resourceFromAttributes({ "service.name": serviceName }),
58
+ spanProcessors: [spanProcessor],
59
+ idGenerator: requestIdGenerator
60
+ });
61
+ provider.register();
62
+ const unregisterInstrumentations = registerInstrumentations({
63
+ tracerProvider: provider,
64
+ instrumentations: [undiciInstrumentation()]
65
+ });
66
+ const handler = nitroApp.h3App.handler;
67
+ nitroApp.h3App.handler = (event) => context.with(serverSpanContext(event), () => handler(event));
68
+ nitroApp.hooks.hook("close", async () => {
69
+ unregisterInstrumentations();
70
+ try {
71
+ await provider.shutdown();
72
+ } catch (error) {
73
+ logger.error("otel_shutdown_failed", { error: error instanceof Error ? error.message : String(error) });
74
+ } finally {
75
+ trace.disable();
76
+ propagation.disable();
77
+ context.disable();
78
+ }
79
+ });
80
+ logger.info("otel_initialized", { service: serviceName, endpoint });
81
+ });
@@ -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,26 @@
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
+ * Кладе в event.context `otelSpan` (назва сторінки, `i18n.locale`), віддає
24
+ * `X-Trace-Id` у відповіді.
25
+ */
26
+ export declare function serverSpanContext(event: H3Event): Context;
@@ -0,0 +1,64 @@
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 { requestIdGenerator } from "./requestTraceId.js";
7
+ const STATIC_ASSET = /\.(css|m?js|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|map|json|txt|xml)$/i;
8
+ function unsampledContext(active) {
9
+ return trace.setSpanContext(active, {
10
+ traceId: requestIdGenerator.generateTraceId(),
11
+ spanId: requestIdGenerator.generateSpanId(),
12
+ traceFlags: TraceFlags.NONE
13
+ });
14
+ }
15
+ function resolvedRouteName(event) {
16
+ if (event.context.otelPageName) return event.context.otelPageName;
17
+ const route = event.context.matchedRoute?.path;
18
+ return route && route !== "/**" ? route : void 0;
19
+ }
20
+ export function serverSpanContext(event) {
21
+ const active = context.active();
22
+ const settings = otelSettings();
23
+ const userAgent = getHeader(event, "user-agent") ?? "";
24
+ const [pathname = "", query = ""] = event.path.split("?");
25
+ if (isTracingSuppressed(active) || settings.ignoreUserAgent?.test(userAgent) || settings.ignorePaths.some((prefix) => pathname.startsWith(prefix)) || settings.ignoreStaticAssets && STATIC_ASSET.test(pathname)) {
26
+ return isTracingSuppressed(active) ? active : unsampledContext(active);
27
+ }
28
+ const carrier = {};
29
+ const traceparent = getHeader(event, "traceparent");
30
+ const tracestate = getHeader(event, "tracestate");
31
+ if (traceparent) carrier.traceparent = traceparent;
32
+ if (tracestate) carrier.tracestate = tracestate;
33
+ const parentCtx = propagation.extract(active, carrier);
34
+ const span = trace.getTracer(TRACER_NAME).startSpan(
35
+ `${event.method} ${pathname.startsWith("/api/") ? "/api/*" : "/*"}`,
36
+ {
37
+ kind: SpanKind.SERVER,
38
+ attributes: {
39
+ "http.request.method": event.method,
40
+ "url.path": pathname,
41
+ "url.query": query,
42
+ "server.address": getHeader(event, "host") ?? "",
43
+ "user_agent.original": userAgent
44
+ }
45
+ },
46
+ parentCtx
47
+ );
48
+ event.context.otelSpan = span;
49
+ if (isSpanContextValid(span.spanContext())) {
50
+ setResponseHeader(event, "X-Trace-Id", span.spanContext().traceId);
51
+ }
52
+ const res = event.node.res;
53
+ res.once("close", () => {
54
+ const route = resolvedRouteName(event);
55
+ if (route) span.updateName(`${event.method} ${route}`);
56
+ span.setAttribute("request_id", requestIdOf(event) ?? "");
57
+ const statusSent = res.headersSent || res.writableFinished;
58
+ if (statusSent) span.setAttribute("http.response.status_code", res.statusCode);
59
+ if (!res.writableFinished) span.setAttribute("error.type", "aborted");
60
+ if (statusSent && res.statusCode >= 500) span.setStatus({ code: SpanStatusCode.ERROR });
61
+ span.end();
62
+ });
63
+ return trace.setSpan(parentCtx, span);
64
+ }
@@ -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.3.0",
3
+ "version": "1.5.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",
@@ -20,6 +21,14 @@
20
21
  "types": "./dist/types.d.ts",
21
22
  "import": "./dist/module.mjs",
22
23
  "require": "./dist/module.cjs"
24
+ },
25
+ "./runtime/logger": {
26
+ "types": "./dist/runtime/logger.d.ts",
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"
23
32
  }
24
33
  },
25
34
  "main": "./dist/module.cjs",
@@ -37,7 +46,15 @@
37
46
  "test": "echo \"No tests yet\""
38
47
  },
39
48
  "dependencies": {
40
- "@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"
41
58
  },
42
59
  "devDependencies": {
43
60
  "@nuxt/module-builder": "^0.8.0",