hotelme-nuxt-logger 1.4.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 +46 -0
- package/dist/module.cjs +22 -2
- package/dist/module.d.cts +30 -1
- package/dist/module.d.mts +30 -1
- package/dist/module.d.ts +30 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +22 -2
- package/dist/runtime/plugins/otelPageName.server.d.ts +11 -0
- package/dist/runtime/plugins/otelPageName.server.js +12 -0
- package/dist/runtime/server/contextPlugin.d.ts +8 -0
- package/dist/runtime/server/contextPlugin.js +6 -0
- package/dist/runtime/server/otel/config.d.ts +15 -0
- package/dist/runtime/server/otel/config.js +17 -0
- package/dist/runtime/server/otel/plugin.d.ts +15 -0
- package/dist/runtime/server/otel/plugin.js +81 -0
- package/dist/runtime/server/otel/requestTraceId.d.ts +10 -0
- package/dist/runtime/server/otel/requestTraceId.js +11 -0
- package/dist/runtime/server/otel/serverSpan.d.ts +26 -0
- package/dist/runtime/server/otel/serverSpan.js +64 -0
- package/dist/runtime/server/otel/tracing.d.ts +14 -0
- package/dist/runtime/server/otel/tracing.js +5 -0
- package/dist/runtime/server/requestContext.d.ts +15 -0
- package/dist/runtime/server/requestContext.js +16 -0
- package/package.json +16 -3
package/README.md
CHANGED
|
@@ -151,6 +151,52 @@ 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
|
+
},
|
|
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
|
+
|
|
154
200
|
## API
|
|
155
201
|
|
|
156
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))));
|
|
@@ -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,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
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,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,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
|
+
"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",
|
|
@@ -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",
|