hotelme-nuxt-logger 1.5.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 +23 -1
- package/dist/module.d.cts +8 -0
- package/dist/module.d.mts +8 -0
- package/dist/module.d.ts +8 -0
- package/dist/module.json +1 -1
- package/dist/runtime/server/otel/config.d.ts +24 -0
- package/dist/runtime/server/otel/config.js +42 -0
- package/dist/runtime/server/otel/plugin.d.ts +7 -1
- package/dist/runtime/server/otel/plugin.js +15 -2
- package/dist/runtime/server/otel/sampling.d.ts +64 -0
- package/dist/runtime/server/otel/sampling.js +126 -0
- package/dist/runtime/server/otel/serverSpan.d.ts +3 -0
- package/dist/runtime/server/otel/serverSpan.js +9 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,7 @@ logger: {
|
|
|
163
163
|
enabled: true,
|
|
164
164
|
ignorePaths: ['/nuxt_img/'], // + типові /_nuxt/, /_ipx/, /__nuxt, clientErrorEndpoint
|
|
165
165
|
ignoreUserAgents: ['^Slackbot-LinkExpanding ', '^kube-probe/'],
|
|
166
|
+
slowMs: 1000, // порятунок повільних, env OTEL_TRACES_SLOW_MS
|
|
166
167
|
},
|
|
167
168
|
}
|
|
168
169
|
```
|
|
@@ -175,6 +176,20 @@ Env (читає SDK):
|
|
|
175
176
|
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_ENDPOINT` | — (обов'язкова: без неї трейсинг не вмикається, лог `otel_disabled`) |
|
|
176
177
|
| `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG` | `parentbased_always_on` |
|
|
177
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
|
+
|
|
178
193
|
Що робить модуль:
|
|
179
194
|
|
|
180
195
|
- **Активний контекст на весь запит.** Плагін обгортає `nitroApp.h3App.handler`, тож middleware, handler'и та SSR-рендер виконуються всередині `context.with(serverSpan)` — span'и з коду застосунку стають дочірніми без обгорток.
|
|
@@ -185,7 +200,14 @@ Env (читає SDK):
|
|
|
185
200
|
- сторінка → `route.name` з Vue Router без i18n-суфікса: `GET weather-citynameid`, `GET index` (аліаси теж). Локаль — в атрибуті `i18n.locale`;
|
|
186
201
|
- Nitro-хендлер → шаблон з файлової структури: `GET /api/city_info`, `GET /record/**`;
|
|
187
202
|
- решта (відповідь з server middleware, невідомий шлях) → `GET /api/*` / `GET /*`; точний шлях — в `url.path`, статус — в `http.response.status_code`.
|
|
188
|
-
- **Фільтри** (статика, `ignorePaths`, `ignoreUserAgents`) не міняють поведінку запиту: він обробляється як завжди, але в невибраному (sampled=0) контексті — без span'ів у нього, його внутрішніх викликів і бекенда (`traceparent …-00`). Потрібен parent-based семплер (типовий).
|
|
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.
|
|
189
211
|
- **Flush при зупинці**: span'и батчуються до 5 с, на `close` provider робить shutdown.
|
|
190
212
|
- Помилки експорту (`diag`) пишуться JSON-логером як `otel_diag`.
|
|
191
213
|
|
package/dist/module.d.cts
CHANGED
|
@@ -56,6 +56,14 @@ interface OtelOptions {
|
|
|
56
56
|
* @default []
|
|
57
57
|
*/
|
|
58
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;
|
|
59
67
|
}
|
|
60
68
|
declare const _default: any;
|
|
61
69
|
|
package/dist/module.d.mts
CHANGED
|
@@ -56,6 +56,14 @@ interface OtelOptions {
|
|
|
56
56
|
* @default []
|
|
57
57
|
*/
|
|
58
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;
|
|
59
67
|
}
|
|
60
68
|
declare const _default: any;
|
|
61
69
|
|
package/dist/module.d.ts
CHANGED
|
@@ -56,6 +56,14 @@ interface OtelOptions {
|
|
|
56
56
|
* @default []
|
|
57
57
|
*/
|
|
58
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;
|
|
59
67
|
}
|
|
60
68
|
declare const _default: any;
|
|
61
69
|
|
package/dist/module.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type Sampler } from '@opentelemetry/sdk-trace-base';
|
|
1
2
|
/** Серіалізована форма `logger.otel` з nuxt.config (див. module.ts). */
|
|
2
3
|
export interface OtelRuntimeConfig {
|
|
3
4
|
ignorePaths?: string[];
|
|
4
5
|
ignoreStaticAssets?: boolean;
|
|
5
6
|
ignoreUserAgents?: string[];
|
|
7
|
+
slowMs?: number;
|
|
6
8
|
}
|
|
7
9
|
export interface OtelSettings {
|
|
8
10
|
ignorePaths: string[];
|
|
@@ -13,3 +15,25 @@ export declare const TRACER_NAME = "hotelme-nuxt-logger";
|
|
|
13
15
|
export declare function otelRuntimeConfig(): OtelRuntimeConfig;
|
|
14
16
|
/** Регекспи компілюються один раз, на першому запиті. */
|
|
15
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;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { useRuntimeConfig } from "#imports";
|
|
2
|
+
import { AlwaysOffSampler, AlwaysOnSampler, ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
import { logger } from "../../logger.js";
|
|
2
4
|
export const TRACER_NAME = "hotelme-nuxt-logger";
|
|
3
5
|
export function otelRuntimeConfig() {
|
|
4
6
|
return useRuntimeConfig().logger?.otel ?? {};
|
|
@@ -15,3 +17,43 @@ export function otelSettings() {
|
|
|
15
17
|
};
|
|
16
18
|
return settings;
|
|
17
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
|
+
}
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
* - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT
|
|
6
6
|
* (insecure чи TLS — за схемою URL). Без них трейсинг не вмикається:
|
|
7
7
|
* експортер SDK інакше мовчки слав би на localhost:4317.
|
|
8
|
-
* - OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG (типово parentbased_always_on)
|
|
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).
|
|
9
15
|
*
|
|
10
16
|
* Root trace id — з `X-Request-ID`, див. requestTraceId.ts.
|
|
11
17
|
*
|
|
@@ -11,6 +11,8 @@ import { logger } from "../../logger.js";
|
|
|
11
11
|
import { currentRequestEvent, requestIdOf } from "../requestContext.js";
|
|
12
12
|
import { serverSpanContext } from "./serverSpan.js";
|
|
13
13
|
import { requestIdGenerator } from "./requestTraceId.js";
|
|
14
|
+
import { envSampler, otelSampling } from "./config.js";
|
|
15
|
+
import { ForceSampler, HeadSampler, SlowTraceSpanProcessor } from "./sampling.js";
|
|
14
16
|
const diagLogger = {
|
|
15
17
|
error: (message, ...args) => logger.error("otel_diag", { error: message, args }),
|
|
16
18
|
warn: (message, ...args) => logger.warn("otel_diag", { error: message, args }),
|
|
@@ -47,14 +49,18 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
47
49
|
}
|
|
48
50
|
diag.setLogger(diagLogger, DiagLogLevel.WARN);
|
|
49
51
|
const serviceName = process.env.OTEL_SERVICE_NAME || String(useRuntimeConfig().public.projectName || "hotelme");
|
|
50
|
-
const
|
|
52
|
+
const batchProcessor = new BatchSpanProcessor(new OTLPTraceExporter(), {
|
|
51
53
|
maxQueueSize: 2048,
|
|
52
54
|
maxExportBatchSize: 512,
|
|
53
55
|
scheduledDelayMillis: 5e3,
|
|
54
56
|
exportTimeoutMillis: 1e4
|
|
55
57
|
});
|
|
58
|
+
const sampling = otelSampling();
|
|
59
|
+
const spanProcessor = sampling ? new SlowTraceSpanProcessor(batchProcessor, sampling.slowMs) : batchProcessor;
|
|
60
|
+
const baseSampler = sampling ? new HeadSampler(sampling.ratio) : envSampler();
|
|
56
61
|
const provider = new NodeTracerProvider({
|
|
57
62
|
resource: resourceFromAttributes({ "service.name": serviceName }),
|
|
63
|
+
...baseSampler && { sampler: new ForceSampler(baseSampler) },
|
|
58
64
|
spanProcessors: [spanProcessor],
|
|
59
65
|
idGenerator: requestIdGenerator
|
|
60
66
|
});
|
|
@@ -77,5 +83,12 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
77
83
|
context.disable();
|
|
78
84
|
}
|
|
79
85
|
});
|
|
80
|
-
logger.info("otel_initialized", {
|
|
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
|
+
});
|
|
81
94
|
});
|
|
@@ -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
|
+
}
|
|
@@ -20,6 +20,9 @@ declare module 'h3' {
|
|
|
20
20
|
* /api-виклики, вихідні fetch і span'и бекенда стали б осиротілими
|
|
21
21
|
* root-трейсами. Дочірні запити успадковують його через parent-based семплер.
|
|
22
22
|
*
|
|
23
|
+
* `?force_otel=1` (як у libwts3) — трейс вибраний попри фільтри, частку
|
|
24
|
+
* семплера й вхідний `traceparent …-00`, root з `sampling.kept_by: "force"`.
|
|
25
|
+
*
|
|
23
26
|
* Кладе в event.context `otelSpan` (назва сторінки, `i18n.locale`), віддає
|
|
24
27
|
* `X-Trace-Id` у відповіді.
|
|
25
28
|
*/
|
|
@@ -3,6 +3,7 @@ import { context, isSpanContextValid, propagation, trace, SpanKind, SpanStatusCo
|
|
|
3
3
|
import { isTracingSuppressed } from "@opentelemetry/core";
|
|
4
4
|
import { requestIdOf } from "../requestContext.js";
|
|
5
5
|
import { otelSettings, TRACER_NAME } from "./config.js";
|
|
6
|
+
import { forceSampled } from "./sampling.js";
|
|
6
7
|
import { requestIdGenerator } from "./requestTraceId.js";
|
|
7
8
|
const STATIC_ASSET = /\.(css|m?js|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|map|json|txt|xml)$/i;
|
|
8
9
|
function unsampledContext(active) {
|
|
@@ -22,15 +23,18 @@ export function serverSpanContext(event) {
|
|
|
22
23
|
const settings = otelSettings();
|
|
23
24
|
const userAgent = getHeader(event, "user-agent") ?? "";
|
|
24
25
|
const [pathname = "", query = ""] = event.path.split("?");
|
|
25
|
-
if (isTracingSuppressed(active)
|
|
26
|
-
|
|
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);
|
|
27
30
|
}
|
|
28
31
|
const carrier = {};
|
|
29
32
|
const traceparent = getHeader(event, "traceparent");
|
|
30
33
|
const tracestate = getHeader(event, "tracestate");
|
|
31
34
|
if (traceparent) carrier.traceparent = traceparent;
|
|
32
35
|
if (tracestate) carrier.tracestate = tracestate;
|
|
33
|
-
const
|
|
36
|
+
const extracted = propagation.extract(active, carrier);
|
|
37
|
+
const parentCtx = forced ? forceSampled(extracted) : extracted;
|
|
34
38
|
const span = trace.getTracer(TRACER_NAME).startSpan(
|
|
35
39
|
`${event.method} ${pathname.startsWith("/api/") ? "/api/*" : "/*"}`,
|
|
36
40
|
{
|
|
@@ -40,7 +44,8 @@ export function serverSpanContext(event) {
|
|
|
40
44
|
"url.path": pathname,
|
|
41
45
|
"url.query": query,
|
|
42
46
|
"server.address": getHeader(event, "host") ?? "",
|
|
43
|
-
"user_agent.original": userAgent
|
|
47
|
+
"user_agent.original": userAgent,
|
|
48
|
+
...forced && { "sampling.kept_by": "force" }
|
|
44
49
|
}
|
|
45
50
|
},
|
|
46
51
|
parentCtx
|