@vicacha-devs/strapi-plugin-opentelemetry 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,544 @@
1
+ import { trace, SpanKind, SpanStatusCode, metrics } from "@opentelemetry/api";
2
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
3
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
4
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
5
+ import { Resource } from "@opentelemetry/resources";
6
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
7
+ import { NodeSDK } from "@opentelemetry/sdk-node";
8
+ import { ConsoleSpanExporter, ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
9
+ import { ATTR_SERVICE_VERSION, ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
10
+ function stripTrailingSlashes(value) {
11
+ let end = value.length;
12
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
13
+ return value.slice(0, end);
14
+ }
15
+ const defaultConfig = {
16
+ enabled: true,
17
+ serviceName: "strapi",
18
+ serviceVersion: "1.0.0",
19
+ environment: process.env.NODE_ENV || "development",
20
+ otlpEndpoint: "",
21
+ otlpHeaders: {},
22
+ sampleRate: 1,
23
+ ignorePaths: ["/_health", "/favicon.ico"],
24
+ captureDocumentService: true,
25
+ consoleExporter: false,
26
+ metricExportIntervalMs: 6e4
27
+ };
28
+ function parseOtlpHeaders(raw) {
29
+ if (!raw || typeof raw !== "string") {
30
+ return {};
31
+ }
32
+ const headers = {};
33
+ for (const pair of raw.split(",")) {
34
+ const eqIdx = pair.indexOf("=");
35
+ if (eqIdx <= 0) continue;
36
+ const key = pair.slice(0, eqIdx).trim();
37
+ const value = pair.slice(eqIdx + 1).trim();
38
+ if (key) {
39
+ headers[key] = value;
40
+ }
41
+ }
42
+ return headers;
43
+ }
44
+ function resolveOtelConfig(rawConfig = {}) {
45
+ const envEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() ?? "";
46
+ const envServiceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "";
47
+ const envHeaders = parseOtlpHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS);
48
+ return {
49
+ ...defaultConfig,
50
+ ...rawConfig,
51
+ serviceName: rawConfig.serviceName || envServiceName || defaultConfig.serviceName,
52
+ otlpEndpoint: stripTrailingSlashes(rawConfig.otlpEndpoint || envEndpoint),
53
+ otlpHeaders: {
54
+ ...envHeaders,
55
+ ...rawConfig.otlpHeaders
56
+ },
57
+ ignorePaths: rawConfig.ignorePaths ?? defaultConfig.ignorePaths
58
+ };
59
+ }
60
+ function validateOtelConfig(pluginConfig) {
61
+ if (pluginConfig === null || typeof pluginConfig !== "object" || Array.isArray(pluginConfig)) {
62
+ throw new Error("OpenTelemetry plugin config must be an object");
63
+ }
64
+ const cfg = pluginConfig;
65
+ if (cfg.enabled !== void 0 && typeof cfg.enabled !== "boolean") {
66
+ throw new Error("OpenTelemetry plugin config.enabled must be a boolean");
67
+ }
68
+ if (cfg.serviceName !== void 0 && typeof cfg.serviceName !== "string") {
69
+ throw new Error("OpenTelemetry plugin config.serviceName must be a string");
70
+ }
71
+ if (cfg.serviceVersion !== void 0 && typeof cfg.serviceVersion !== "string") {
72
+ throw new Error(
73
+ "OpenTelemetry plugin config.serviceVersion must be a string"
74
+ );
75
+ }
76
+ if (cfg.environment !== void 0 && typeof cfg.environment !== "string") {
77
+ throw new Error("OpenTelemetry plugin config.environment must be a string");
78
+ }
79
+ if (cfg.otlpEndpoint !== void 0 && typeof cfg.otlpEndpoint !== "string") {
80
+ throw new Error(
81
+ "OpenTelemetry plugin config.otlpEndpoint must be a string"
82
+ );
83
+ }
84
+ if (cfg.otlpHeaders !== void 0 && (cfg.otlpHeaders === null || typeof cfg.otlpHeaders !== "object" || Array.isArray(cfg.otlpHeaders))) {
85
+ throw new Error(
86
+ "OpenTelemetry plugin config.otlpHeaders must be a key-value object"
87
+ );
88
+ }
89
+ if (cfg.sampleRate !== void 0 && (typeof cfg.sampleRate !== "number" || Number.isNaN(cfg.sampleRate) || cfg.sampleRate < 0 || cfg.sampleRate > 1)) {
90
+ throw new Error(
91
+ "OpenTelemetry plugin config.sampleRate must be a number between 0 and 1"
92
+ );
93
+ }
94
+ if (cfg.ignorePaths !== void 0 && (!Array.isArray(cfg.ignorePaths) || cfg.ignorePaths.some((p) => typeof p !== "string"))) {
95
+ throw new Error(
96
+ "OpenTelemetry plugin config.ignorePaths must be an array of strings"
97
+ );
98
+ }
99
+ if (cfg.captureDocumentService !== void 0 && typeof cfg.captureDocumentService !== "boolean") {
100
+ throw new Error(
101
+ "OpenTelemetry plugin config.captureDocumentService must be a boolean"
102
+ );
103
+ }
104
+ if (cfg.consoleExporter !== void 0 && typeof cfg.consoleExporter !== "boolean") {
105
+ throw new Error(
106
+ "OpenTelemetry plugin config.consoleExporter must be a boolean"
107
+ );
108
+ }
109
+ if (cfg.metricExportIntervalMs !== void 0 && (typeof cfg.metricExportIntervalMs !== "number" || cfg.metricExportIntervalMs < 1e3)) {
110
+ throw new Error(
111
+ "OpenTelemetry plugin config.metricExportIntervalMs must be a number >= 1000"
112
+ );
113
+ }
114
+ }
115
+ const MAX_MESSAGE_LENGTH = 1e3;
116
+ const SENSITIVE_QUERY_KEYS = /(^|[?&])(token|access_token|refresh_token|id_token|api_key|apikey|key|secret|password|passwd|auth|signature|sig|code)=([^&\s]+)/gi;
117
+ const BEARER_PATTERN = /\b(Bearer|Basic)\s+[A-Z0-9\-._~+/]+=*/gi;
118
+ const EMAIL_PATTERN = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
119
+ const MAX_INPUT_LENGTH = MAX_MESSAGE_LENGTH * 4;
120
+ const isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122;
121
+ const isSchemeChar = (c) => isAlpha(c) || c >= 48 && c <= 57 || c === 43 || c === 46 || c === 45;
122
+ const isWhitespace = (c) => c === 32 || c >= 9 && c <= 13 || c === 160 || c === 5760 || c >= 8192 && c <= 8202 || c === 8232 || c === 8233 || c === 8239 || c === 8287 || c === 12288 || c === 65279;
123
+ function redactUrlCredentials(input) {
124
+ let out = "";
125
+ let from = 0;
126
+ let pos = 0;
127
+ for (; ; ) {
128
+ const idx = input.indexOf("://", pos);
129
+ if (idx === -1) break;
130
+ let start = idx;
131
+ while (start > from && isSchemeChar(input.charCodeAt(start - 1))) start--;
132
+ while (start < idx && !isAlpha(input.charCodeAt(start))) start++;
133
+ let end = -1;
134
+ if (start < idx) {
135
+ let i = idx + 3;
136
+ const userStart = i;
137
+ while (i < input.length) {
138
+ const c = input.charCodeAt(i);
139
+ if (c === 58 || c === 64 || isWhitespace(c)) break;
140
+ i++;
141
+ }
142
+ if (i > userStart && input.charCodeAt(i) === 58) {
143
+ i++;
144
+ const passStart = i;
145
+ while (i < input.length) {
146
+ const c = input.charCodeAt(i);
147
+ if (c === 64 || isWhitespace(c)) break;
148
+ i++;
149
+ }
150
+ if (i > passStart && input.charCodeAt(i) === 64) end = i + 1;
151
+ }
152
+ }
153
+ if (end === -1) {
154
+ pos = idx + 3;
155
+ } else {
156
+ out += `${input.slice(from, idx + 3)}[REDACTED]@`;
157
+ from = end;
158
+ pos = end;
159
+ }
160
+ }
161
+ return out + input.slice(from);
162
+ }
163
+ function sanitizeTelemetryText(input) {
164
+ if (!input) return "";
165
+ let sanitized = redactUrlCredentials(
166
+ input.slice(0, MAX_INPUT_LENGTH).replace(BEARER_PATTERN, "$1 [REDACTED]")
167
+ ).replace(SENSITIVE_QUERY_KEYS, "$1$2=[REDACTED]").replace(EMAIL_PATTERN, "[REDACTED_EMAIL]");
168
+ if (sanitized.length > MAX_MESSAGE_LENGTH) {
169
+ sanitized = `${sanitized.slice(0, MAX_MESSAGE_LENGTH)}...[truncated]`;
170
+ }
171
+ return sanitized;
172
+ }
173
+ function sanitizeError(err) {
174
+ const raw = err instanceof Error ? err : new Error(String(err));
175
+ const safeMessage = sanitizeTelemetryText(raw.message);
176
+ const safeError = new Error(safeMessage);
177
+ safeError.name = raw.name || "Error";
178
+ if (raw.stack) {
179
+ safeError.stack = sanitizeTelemetryText(raw.stack);
180
+ }
181
+ return safeError;
182
+ }
183
+ function extractPluginName(input) {
184
+ if (input.uid?.startsWith("plugin::")) {
185
+ const rest = input.uid.slice("plugin::".length);
186
+ const dotIdx = rest.indexOf(".");
187
+ return dotIdx > 0 ? rest.slice(0, dotIdx) : rest || void 0;
188
+ }
189
+ if (input.route) {
190
+ const match = /^\/(?:api\/)?([a-z0-9-]+)(?:\/|$)/i.exec(input.route);
191
+ if (match?.[1] && match[1] !== "api" && match[1] !== "admin") {
192
+ return match[1];
193
+ }
194
+ }
195
+ return void 0;
196
+ }
197
+ const TRACER_NAME$2 = "@vicacha-devs/strapi-plugin-opentelemetry";
198
+ function registerDocumentServiceInstrumentation(documents, config) {
199
+ if (!config.enabled || !config.captureDocumentService || !documents?.use) {
200
+ return;
201
+ }
202
+ const tracer = trace.getTracer(TRACER_NAME$2, config.serviceVersion);
203
+ documents.use(async (context, next) => {
204
+ const spanName = `strapi.document.${context.action} ${context.uid}`;
205
+ const pluginName = extractPluginName({ uid: context.uid });
206
+ return tracer.startActiveSpan(
207
+ spanName,
208
+ {
209
+ kind: SpanKind.INTERNAL,
210
+ attributes: {
211
+ "strapi.document.uid": context.uid,
212
+ "strapi.document.action": context.action,
213
+ ...pluginName ? { "strapi.plugin": pluginName } : {}
214
+ }
215
+ },
216
+ async (span) => {
217
+ try {
218
+ const result = await next();
219
+ span.setStatus({ code: SpanStatusCode.OK });
220
+ return result;
221
+ } catch (err) {
222
+ const error = sanitizeError(err);
223
+ span.recordException(error);
224
+ span.setStatus({
225
+ code: SpanStatusCode.ERROR,
226
+ message: error.message
227
+ });
228
+ throw err;
229
+ } finally {
230
+ span.end();
231
+ }
232
+ }
233
+ );
234
+ });
235
+ }
236
+ const WRAPPED_SYMBOL = Symbol.for("vicacha.strapi.otel.logger.wrapped");
237
+ const TRACER_NAME$1 = "@vicacha-devs/strapi-plugin-opentelemetry";
238
+ function instrumentStrapiLogger(logger, config) {
239
+ if (!config.enabled || !logger || typeof logger.error !== "function") {
240
+ return;
241
+ }
242
+ if (logger[WRAPPED_SYMBOL]) {
243
+ return;
244
+ }
245
+ const originalError = logger.error.bind(logger);
246
+ const tracer = trace.getTracer(TRACER_NAME$1, config.serviceVersion);
247
+ const meter = metrics.getMeter(TRACER_NAME$1, config.serviceVersion);
248
+ const loggedErrorCounter = meter.createCounter("strapi.log.errors", {
249
+ description: "Count of errors logged via strapi.log.error"
250
+ });
251
+ logger[WRAPPED_SYMBOL] = true;
252
+ logger.error = (...args) => {
253
+ try {
254
+ const firstArg = args[0];
255
+ if (typeof firstArg === "string" && firstArg.startsWith("[opentelemetry]")) {
256
+ return originalError(...args);
257
+ }
258
+ const errorObj = args.find((arg) => arg instanceof Error);
259
+ const safeError = errorObj ? sanitizeError(errorObj) : new Error(
260
+ sanitizeTelemetryText(
261
+ args.map((a) => typeof a === "string" ? a : "").filter(Boolean).join(" ") || "Logged error"
262
+ )
263
+ );
264
+ loggedErrorCounter.add(1, { "error.type": safeError.name });
265
+ const activeSpan = trace.getActiveSpan();
266
+ if (activeSpan) {
267
+ activeSpan.recordException(safeError);
268
+ activeSpan.setStatus({
269
+ code: SpanStatusCode.ERROR,
270
+ message: safeError.message
271
+ });
272
+ } else {
273
+ tracer.startActiveSpan(
274
+ "strapi.log.error",
275
+ { kind: SpanKind.INTERNAL },
276
+ (span) => {
277
+ span.recordException(safeError);
278
+ span.setStatus({
279
+ code: SpanStatusCode.ERROR,
280
+ message: safeError.message
281
+ });
282
+ span.end();
283
+ }
284
+ );
285
+ }
286
+ } catch {
287
+ }
288
+ return originalError(...args);
289
+ };
290
+ }
291
+ const TRACER_NAME = "@vicacha-devs/strapi-plugin-opentelemetry";
292
+ function createOtelRequestMiddleware(config, logger) {
293
+ const tracer = trace.getTracer(TRACER_NAME, config.serviceVersion);
294
+ const meter = metrics.getMeter(TRACER_NAME, config.serviceVersion);
295
+ const durationHistogram = meter.createHistogram(
296
+ "http.server.request.duration",
297
+ {
298
+ description: "Duration of Strapi HTTP server requests in milliseconds",
299
+ unit: "ms"
300
+ }
301
+ );
302
+ const errorCounter = meter.createCounter("http.server.errors", {
303
+ description: "Count of failed Strapi HTTP requests (5xx or unhandled)"
304
+ });
305
+ return async function otelRequestTracker(ctx, next) {
306
+ if (!config.enabled || config.ignorePaths.some((prefix) => ctx.path.startsWith(prefix))) {
307
+ await next();
308
+ return;
309
+ }
310
+ const activeSpan = trace.getActiveSpan();
311
+ if (activeSpan) {
312
+ await executeWithSpan(activeSpan, ctx, next);
313
+ return;
314
+ }
315
+ await tracer.startActiveSpan(
316
+ `${ctx.method} ${ctx.path}`,
317
+ { kind: SpanKind.SERVER },
318
+ async (span) => {
319
+ try {
320
+ await executeWithSpan(span, ctx, next);
321
+ } finally {
322
+ span.end();
323
+ }
324
+ }
325
+ );
326
+ };
327
+ async function executeWithSpan(span, ctx, next) {
328
+ const startMs = performance.now();
329
+ span.setAttributes({
330
+ "http.request.method": ctx.method,
331
+ "url.path": ctx.path
332
+ });
333
+ try {
334
+ await next();
335
+ const route = ctx._matchedRoute || ctx.path;
336
+ const status = ctx.status ?? 200;
337
+ const pluginName = extractPluginName({ route });
338
+ span.setAttributes({
339
+ "http.route": route,
340
+ "http.response.status_code": status,
341
+ ...pluginName ? { "strapi.plugin": pluginName } : {}
342
+ });
343
+ attachUserContext(span, ctx);
344
+ if (status >= 500) {
345
+ span.setStatus({
346
+ code: SpanStatusCode.ERROR,
347
+ message: `HTTP ${status}`
348
+ });
349
+ errorCounter.add(1, {
350
+ "http.request.method": ctx.method,
351
+ "http.route": route,
352
+ "http.response.status_code": status,
353
+ ...pluginName ? { "strapi.plugin": pluginName } : {}
354
+ });
355
+ }
356
+ durationHistogram.record(performance.now() - startMs, {
357
+ "http.request.method": ctx.method,
358
+ "http.route": route,
359
+ "http.response.status_code": status,
360
+ ...pluginName ? { "strapi.plugin": pluginName } : {}
361
+ });
362
+ } catch (err) {
363
+ const error = sanitizeError(err);
364
+ const route = ctx._matchedRoute || ctx.path;
365
+ const pluginName = extractPluginName({ route });
366
+ span.recordException(error);
367
+ span.setStatus({
368
+ code: SpanStatusCode.ERROR,
369
+ message: error.message
370
+ });
371
+ span.setAttribute("http.route", route);
372
+ if (pluginName) {
373
+ span.setAttribute("strapi.plugin", pluginName);
374
+ }
375
+ attachUserContext(span, ctx);
376
+ errorCounter.add(1, {
377
+ "http.request.method": ctx.method,
378
+ "http.route": route,
379
+ "error.type": error.name,
380
+ ...pluginName ? { "strapi.plugin": pluginName } : {}
381
+ });
382
+ durationHistogram.record(performance.now() - startMs, {
383
+ "http.request.method": ctx.method,
384
+ "http.route": route,
385
+ "error.type": error.name,
386
+ ...pluginName ? { "strapi.plugin": pluginName } : {}
387
+ });
388
+ const spanCtx = span.spanContext();
389
+ logger?.error(
390
+ `[opentelemetry] Unhandled request error: ${error.message}`,
391
+ {
392
+ trace_id: spanCtx.traceId,
393
+ span_id: spanCtx.spanId,
394
+ method: ctx.method,
395
+ path: ctx.path,
396
+ ...pluginName ? { plugin: pluginName } : {}
397
+ }
398
+ );
399
+ throw err;
400
+ }
401
+ }
402
+ }
403
+ function attachUserContext(span, ctx) {
404
+ const user = ctx.state?.user;
405
+ if (!user) return;
406
+ const userId = user.documentId ?? user.id;
407
+ if (userId !== void 0 && userId !== null) {
408
+ span.setAttribute("enduser.id", String(userId));
409
+ }
410
+ }
411
+ const SDK_SYMBOL = Symbol.for("vicacha.strapi.otel.sdk");
412
+ const URL_ATTR_KEYS = ["url.full", "http.url", "url.query"];
413
+ class SanitizingSpanExporter {
414
+ constructor(inner) {
415
+ this.inner = inner;
416
+ }
417
+ export(spans, resultCallback) {
418
+ for (const span of spans) {
419
+ const attrs = span.attributes;
420
+ for (const key of URL_ATTR_KEYS) {
421
+ if (typeof attrs[key] === "string") {
422
+ attrs[key] = sanitizeTelemetryText(attrs[key]);
423
+ }
424
+ }
425
+ }
426
+ this.inner.export(spans, resultCallback);
427
+ }
428
+ shutdown() {
429
+ return this.inner.shutdown();
430
+ }
431
+ forceFlush() {
432
+ return this.inner.forceFlush?.() ?? Promise.resolve();
433
+ }
434
+ }
435
+ function startOtelSdk(config) {
436
+ if (!config.enabled) {
437
+ return false;
438
+ }
439
+ const g = globalThis;
440
+ if (g[SDK_SYMBOL]) {
441
+ return false;
442
+ }
443
+ const resource = new Resource({
444
+ [ATTR_SERVICE_NAME]: config.serviceName,
445
+ [ATTR_SERVICE_VERSION]: config.serviceVersion,
446
+ "deployment.environment.name": config.environment
447
+ });
448
+ const rawTraceExporter = config.otlpEndpoint ? new OTLPTraceExporter({
449
+ url: `${config.otlpEndpoint}/v1/traces`,
450
+ headers: config.otlpHeaders
451
+ }) : config.consoleExporter ? new ConsoleSpanExporter() : void 0;
452
+ const traceExporter = rawTraceExporter ? new SanitizingSpanExporter(rawTraceExporter) : void 0;
453
+ const metricReader = config.otlpEndpoint ? new PeriodicExportingMetricReader({
454
+ exporter: new OTLPMetricExporter({
455
+ url: `${config.otlpEndpoint}/v1/metrics`,
456
+ headers: config.otlpHeaders
457
+ }),
458
+ exportIntervalMillis: config.metricExportIntervalMs
459
+ }) : void 0;
460
+ const sdk = new NodeSDK({
461
+ resource,
462
+ sampler: new ParentBasedSampler({
463
+ root: new TraceIdRatioBasedSampler(config.sampleRate)
464
+ }),
465
+ ...traceExporter ? { traceExporter } : {},
466
+ ...metricReader ? { metricReader } : {},
467
+ instrumentations: [
468
+ getNodeAutoInstrumentations({
469
+ "@opentelemetry/instrumentation-fs": { enabled: false },
470
+ "@opentelemetry/instrumentation-http": {
471
+ ignoreIncomingRequestHook: (req) => {
472
+ const url = req.url ?? "";
473
+ return config.ignorePaths.some((prefix) => url.startsWith(prefix));
474
+ }
475
+ }
476
+ })
477
+ ]
478
+ });
479
+ sdk.start();
480
+ g[SDK_SYMBOL] = sdk;
481
+ return true;
482
+ }
483
+ async function shutdownOtelSdk() {
484
+ const g = globalThis;
485
+ const sdk = g[SDK_SYMBOL];
486
+ if (!sdk) {
487
+ return;
488
+ }
489
+ delete g[SDK_SYMBOL];
490
+ await sdk.shutdown();
491
+ }
492
+ const PLUGIN_ID = "opentelemetry";
493
+ function getPluginConfig(strapi) {
494
+ const raw = strapi.config.get(
495
+ `plugin::${PLUGIN_ID}`,
496
+ {}
497
+ );
498
+ return resolveOtelConfig(raw);
499
+ }
500
+ const index = {
501
+ register({ strapi }) {
502
+ const cfg = getPluginConfig(strapi);
503
+ if (!cfg.enabled) {
504
+ return;
505
+ }
506
+ startOtelSdk(cfg);
507
+ instrumentStrapiLogger(
508
+ strapi.log,
509
+ cfg
510
+ );
511
+ const requestMiddleware = createOtelRequestMiddleware(cfg, strapi.log);
512
+ strapi.server.use(
513
+ requestMiddleware
514
+ );
515
+ registerDocumentServiceInstrumentation(
516
+ strapi.documents,
517
+ cfg
518
+ );
519
+ },
520
+ bootstrap({ strapi }) {
521
+ const cfg = getPluginConfig(strapi);
522
+ if (cfg.enabled) {
523
+ strapi.log.info(
524
+ `[opentelemetry] Active for service "${cfg.serviceName}" (${cfg.otlpEndpoint || (cfg.consoleExporter ? "console" : "no-exporter")})`
525
+ );
526
+ }
527
+ },
528
+ async destroy() {
529
+ await shutdownOtelSdk();
530
+ },
531
+ config: {
532
+ default: defaultConfig,
533
+ validator: validateOtelConfig
534
+ },
535
+ middlewares: {
536
+ tracker: (_config, { strapi }) => {
537
+ const cfg = getPluginConfig(strapi);
538
+ return createOtelRequestMiddleware(cfg, strapi.log);
539
+ }
540
+ }
541
+ };
542
+ export {
543
+ index as default
544
+ };
@@ -0,0 +1,12 @@
1
+ import { OtelPluginConfig } from './config';
2
+ declare const WRAPPED_SYMBOL: unique symbol;
3
+ export interface StrapiLoggerWithError {
4
+ error: (...args: unknown[]) => unknown;
5
+ [WRAPPED_SYMBOL]?: boolean;
6
+ }
7
+ /**
8
+ * Wraps `strapi.log.error` so caught plugin/lifecycle errors that are logged
9
+ * without rethrowing are still recorded on the active span (or a new span).
10
+ */
11
+ export declare function instrumentStrapiLogger(logger: StrapiLoggerWithError | undefined, config: OtelPluginConfig): void;
12
+ export {};
@@ -0,0 +1,20 @@
1
+ import { OtelPluginConfig } from './config';
2
+ export interface KoaLikeContext {
3
+ method: string;
4
+ path: string;
5
+ status: number;
6
+ _matchedRoute?: string;
7
+ state?: {
8
+ user?: {
9
+ id?: string | number;
10
+ documentId?: string;
11
+ };
12
+ };
13
+ }
14
+ export interface StrapiLoggerLike {
15
+ error: (message: string, meta?: Record<string, unknown>) => void;
16
+ }
17
+ /**
18
+ * Enriches an active span and records HTTP metrics and errors for Strapi requests.
19
+ */
20
+ export declare function createOtelRequestMiddleware(config: OtelPluginConfig, logger?: StrapiLoggerLike): (ctx: KoaLikeContext, next: () => Promise<void>) => Promise<void>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Linear-time equivalent of replacing
3
+ * /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^:@\s]+:[^@\s]+@)/g with "$1[REDACTED]@"
4
+ * (that pattern rescans long scheme-like runs from every start position).
5
+ */
6
+ export declare function redactUrlCredentials(input: string): string;
7
+ /**
8
+ * Redacts common secrets, credentials, tokens, and email addresses from telemetry text.
9
+ */
10
+ export declare function sanitizeTelemetryText(input: string): string;
11
+ /**
12
+ * Normalizes and sanitizes an unknown thrown value into a safe Error instance for span recording.
13
+ */
14
+ export declare function sanitizeError(err: unknown): Error;
15
+ /**
16
+ * Extracts a Strapi plugin name from a content-type UID (e.g. `plugin::bookings.booking`)
17
+ * or a route path (e.g. `/api/bookings/slots`).
18
+ */
19
+ export declare function extractPluginName(input: {
20
+ uid?: string;
21
+ route?: string;
22
+ }): string | undefined;
@@ -0,0 +1,10 @@
1
+ import { OtelPluginConfig } from './config';
2
+ /**
3
+ * Starts the OpenTelemetry NodeSDK once per process.
4
+ * Returns `true` if a new SDK instance was started, `false` if already running or disabled.
5
+ */
6
+ export declare function startOtelSdk(config: OtelPluginConfig): boolean;
7
+ /**
8
+ * Flushes and shuts down the active OpenTelemetry NodeSDK instance, if any.
9
+ */
10
+ export declare function shutdownOtelSdk(): Promise<void>;