@vtvlive/interactive-apm 0.0.3 → 0.0.5

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.
Files changed (42) hide show
  1. package/dist/factories/tracing-provider.factory.d.ts +6 -4
  2. package/dist/factories/tracing-provider.factory.d.ts.map +1 -1
  3. package/dist/factories/tracing-provider.factory.js +17 -14
  4. package/dist/index.d.ts +12 -11
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/init/elastic-apm-init.d.ts +25 -3
  7. package/dist/init/elastic-apm-init.d.ts.map +1 -1
  8. package/dist/init/elastic-apm-init.js +12 -12
  9. package/dist/init/opentelemetry-init.d.ts +6 -2
  10. package/dist/init/opentelemetry-init.d.ts.map +1 -1
  11. package/dist/init/opentelemetry-init.js +46 -42
  12. package/dist/interfaces/tracing-provider.interface.d.ts +13 -3
  13. package/dist/interfaces/tracing-provider.interface.d.ts.map +1 -1
  14. package/dist/modules/tracing.module.d.ts +5 -5
  15. package/dist/modules/tracing.module.d.ts.map +1 -1
  16. package/dist/modules/tracing.module.js +2 -1
  17. package/dist/providers/elastic-apm.tracing-provider.d.ts +23 -5
  18. package/dist/providers/elastic-apm.tracing-provider.d.ts.map +1 -1
  19. package/dist/providers/elastic-apm.tracing-provider.js +94 -30
  20. package/dist/providers/opentelemetry.tracing-provider.d.ts +6 -5
  21. package/dist/providers/opentelemetry.tracing-provider.d.ts.map +1 -1
  22. package/dist/providers/opentelemetry.tracing-provider.js +178 -85
  23. package/dist/services/tracing.service.d.ts +6 -5
  24. package/dist/services/tracing.service.d.ts.map +1 -1
  25. package/dist/services/tracing.service.js +2 -2
  26. package/dist/types/apm-provider.type.d.ts +2 -0
  27. package/dist/types/apm-provider.type.d.ts.map +1 -1
  28. package/dist/types/apm.types.d.ts +132 -0
  29. package/dist/types/apm.types.d.ts.map +1 -0
  30. package/dist/types/apm.types.js +15 -0
  31. package/dist/types/otlp-transport.type.d.ts +2 -0
  32. package/dist/types/otlp-transport.type.d.ts.map +1 -1
  33. package/dist/utils/debug-exporter-wrapper.d.ts +13 -2
  34. package/dist/utils/debug-exporter-wrapper.d.ts.map +1 -1
  35. package/dist/utils/debug-exporter-wrapper.js +82 -42
  36. package/dist/utils/debug-logger.d.ts +34 -9
  37. package/dist/utils/debug-logger.d.ts.map +1 -1
  38. package/dist/utils/debug-logger.js +37 -28
  39. package/dist/utils/tracing.helper.d.ts +2 -2
  40. package/dist/utils/tracing.helper.d.ts.map +1 -1
  41. package/dist/utils/tracing.helper.js +8 -4
  42. package/package.json +15 -4
@@ -45,21 +45,12 @@ class TransactionNameProcessor {
45
45
  constructor() {
46
46
  this.cachedSpanKind = null;
47
47
  }
48
- onStart(_span) {
49
- // No-op - moved to onEnd to read final http.route
50
- }
51
- forceFlush() {
52
- return Promise.resolve();
53
- }
54
- shutdown() {
55
- return Promise.resolve();
56
- }
57
- onEnd(_span) {
48
+ onStart(span) {
58
49
  // Lazy load SpanKind to avoid top-level import failure when @opentelemetry/api is absent
59
50
  if (!this.cachedSpanKind) {
60
51
  try {
61
52
  // @ts-ignore - Optional peer dependency
62
- this.cachedSpanKind = require('@opentelemetry/api').SpanKind;
53
+ this.cachedSpanKind = require("@opentelemetry/api").SpanKind;
63
54
  }
64
55
  catch {
65
56
  // If @opentelemetry/api is not available, skip processing
@@ -67,19 +58,30 @@ class TransactionNameProcessor {
67
58
  }
68
59
  }
69
60
  // Only process root spans (SERVER spans without parent)
70
- if (_span.parent || _span.kind !== this.cachedSpanKind.SERVER) {
61
+ const s = span;
62
+ if (s.parent || s.kind !== this.cachedSpanKind?.SERVER) {
71
63
  return;
72
64
  }
73
- // Get HTTP attributes (http.route is often set after routing completes)
74
- const method = _span.attributes?.['http.method'];
75
- const route = _span.attributes?.['http.route']; // /api/healthcheck/ping
76
- const target = _span.attributes?.['http.target']; // fallback
77
- if (method && (route || target)) {
65
+ // Get HTTP attributes - use http.target first (path + query), fallback to http.path
66
+ const method = s.attributes?.["http.method"];
67
+ const target = s.attributes?.["http.target"]; // /api/healthcheck/ping?foo=bar
68
+ const path = s.attributes?.["http.path"]; // fallback - just path
69
+ const fullPath = target || path;
70
+ if (method && fullPath) {
78
71
  // Format: GET /api/healthcheck/ping
79
- const fullName = `${method} ${route || target}`;
80
- _span.updateName(fullName);
72
+ const fullName = `${method} ${fullPath}`;
73
+ s.updateName?.(fullName);
81
74
  }
82
75
  }
76
+ forceFlush() {
77
+ return Promise.resolve();
78
+ }
79
+ shutdown() {
80
+ return Promise.resolve();
81
+ }
82
+ onEnd(_span) {
83
+ // No-op - name is set in onStart
84
+ }
83
85
  }
84
86
  /**
85
87
  * Normalize OTLP endpoint URL to ensure consistent format
@@ -92,23 +94,23 @@ function normalizeEndpoint(endpoint, isGrpc) {
92
94
  const url = new URL(endpoint);
93
95
  // Ensure port is specified
94
96
  if (!url.port) {
95
- const defaultPort = url.protocol === 'https:' ? '443' : '80';
97
+ const defaultPort = url.protocol === "https:" ? "443" : "80";
96
98
  url.port = defaultPort;
97
99
  }
98
100
  // Remove trailing slash
99
- let pathname = url.pathname.replace(/\/+$/, '');
101
+ let pathname = url.pathname.replace(/\/+$/, "");
100
102
  // For gRPC, remove /v1/traces as it uses different path
101
- if (isGrpc && pathname.endsWith('/v1/traces')) {
102
- pathname = pathname.replace('/v1/traces', '');
103
+ if (isGrpc && pathname.endsWith("/v1/traces")) {
104
+ pathname = pathname.replace("/v1/traces", "");
103
105
  }
104
106
  // For HTTP/PROTO, ensure /v1/traces path exists
105
- if (!isGrpc && !pathname.endsWith('/v1/traces')) {
106
- pathname = pathname + '/v1/traces';
107
+ if (!isGrpc && !pathname.endsWith("/v1/traces")) {
108
+ pathname = pathname + "/v1/traces";
107
109
  }
108
110
  url.pathname = pathname;
109
111
  return url.toString();
110
112
  }
111
- catch (error) {
113
+ catch {
112
114
  // If URL parsing fails, return as-is
113
115
  return endpoint;
114
116
  }
@@ -122,24 +124,29 @@ class OpenTelemetryTracingProvider {
122
124
  this.sdk = null;
123
125
  this.tracer = null;
124
126
  this.initialized = false;
125
- this.serviceName = config.serviceName || process.env.ELASTIC_APM_SERVICE_NAME || 'interactive-backend';
126
- this.environment = config.environment || process.env.ELASTIC_APM_ENVIRONMENT || 'development';
127
- this.otlpEndpoint = config.otlpEndpoint || process.env.ELASTIC_OTLP_ENDPOINT || 'http://localhost:8200/v1/traces';
127
+ this.serviceName =
128
+ config.serviceName || process.env.ELASTIC_APM_SERVICE_NAME || "interactive-backend";
129
+ this.environment = config.environment || process.env.ELASTIC_APM_ENVIRONMENT || "development";
130
+ this.otlpEndpoint =
131
+ config.otlpEndpoint || process.env.ELASTIC_OTLP_ENDPOINT || "http://localhost:8200/v1/traces";
128
132
  // Determine transport type (HTTP, gRPC, or PROTO)
129
- const transportType = (config.otlpTransport || process.env.ELASTIC_OTLP_TRANSPORT || otlp_transport_type_1.OtlpTransport.HTTP).toLowerCase();
130
- if (transportType === otlp_transport_type_1.OtlpTransport.GRPC || transportType === 'grpc') {
133
+ const transportType = (config.otlpTransport ||
134
+ process.env.ELASTIC_OTLP_TRANSPORT ||
135
+ otlp_transport_type_1.OtlpTransport.HTTP).toLowerCase();
136
+ if (transportType === otlp_transport_type_1.OtlpTransport.GRPC || transportType === "grpc") {
131
137
  this.otlpTransport = otlp_transport_type_1.OtlpTransport.GRPC;
132
138
  }
133
- else if (transportType === otlp_transport_type_1.OtlpTransport.PROTO || transportType === 'proto') {
139
+ else if (transportType === otlp_transport_type_1.OtlpTransport.PROTO || transportType === "proto") {
134
140
  this.otlpTransport = otlp_transport_type_1.OtlpTransport.PROTO;
135
141
  }
136
142
  else {
137
143
  this.otlpTransport = otlp_transport_type_1.OtlpTransport.HTTP;
138
144
  }
139
- this.secretToken = config.secretToken || process.env.ELASTIC_APM_SECRET_TOKEN || '';
140
- this.otlpAuthToken = config.otlpAuthToken || process.env.ELASTIC_OTLP_AUTH_TOKEN || '';
145
+ this.secretToken = config.secretToken || process.env.ELASTIC_APM_SECRET_TOKEN || "";
146
+ this.otlpAuthToken = config.otlpAuthToken || process.env.ELASTIC_OTLP_AUTH_TOKEN || "";
141
147
  this.otlpHeaders = config.otlpHeaders || {};
142
- this.enableConsoleExporter = config.enableConsoleExporter ?? process.env.ELASTIC_OTLP_ENABLE_CONSOLE_EXPORTER === 'true';
148
+ this.enableConsoleExporter =
149
+ config.enableConsoleExporter ?? process.env.ELASTIC_OTLP_ENABLE_CONSOLE_EXPORTER === "true";
143
150
  }
144
151
  /**
145
152
  * Initialize OpenTelemetry SDK
@@ -152,30 +159,33 @@ class OpenTelemetryTracingProvider {
152
159
  try {
153
160
  // Dynamic import to avoid errors when @opentelemetry packages are not installed
154
161
  // @ts-ignore - Optional peer dependency
155
- const { NodeSDK } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/sdk-node')));
162
+ const { NodeSDK } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/sdk-node")));
156
163
  // @ts-ignore - Optional peer dependency
157
- const { HttpInstrumentation } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/instrumentation-http')));
164
+ const { HttpInstrumentation } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/instrumentation-http")));
158
165
  // @ts-ignore - Optional peer dependency
159
- const { ExpressInstrumentation } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/instrumentation-express')));
166
+ const { ExpressInstrumentation } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/instrumentation-express")));
160
167
  // @ts-ignore - Optional peer dependency
161
- const { resourceFromAttributes } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/resources')));
168
+ const { resourceFromAttributes } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/resources")));
162
169
  // @ts-ignore - Optional peer dependency
163
- const { trace } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/api')));
170
+ const { trace } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/api")));
164
171
  const endpoint = config?.otlpEndpoint || this.otlpEndpoint;
165
172
  const isGrpc = this.otlpTransport === otlp_transport_type_1.OtlpTransport.GRPC;
166
173
  const isProto = this.otlpTransport === otlp_transport_type_1.OtlpTransport.PROTO;
167
174
  // Normalize endpoint to ensure consistent format
168
175
  const normalizedEndpoint = normalizeEndpoint(endpoint, isGrpc);
169
176
  // Log initialization details when debug mode is on
170
- (0, debug_logger_1.logInitialization)('OpenTelemetry', {
177
+ (0, debug_logger_1.logInitialization)("OpenTelemetry", {
171
178
  serviceName: config?.serviceName || this.serviceName,
172
179
  environment: config?.environment || this.environment,
173
180
  otlpEndpoint: normalizedEndpoint,
174
- transport: isGrpc ? 'gRPC' : isProto ? 'PROTO (protobuf over HTTP)' : 'HTTP',
175
- hasToken: !!(config?.otlpAuthToken || this.otlpAuthToken || config?.secretToken || this.secretToken),
181
+ transport: isGrpc ? "gRPC" : isProto ? "PROTO (protobuf over HTTP)" : "HTTP",
182
+ hasToken: !!(config?.otlpAuthToken ||
183
+ this.otlpAuthToken ||
184
+ config?.secretToken ||
185
+ this.secretToken),
176
186
  enableConsoleExporter: config?.enableConsoleExporter ?? this.enableConsoleExporter,
177
187
  });
178
- const transportLabel = isGrpc ? 'gRPC' : isProto ? 'PROTO' : 'HTTP';
188
+ const transportLabel = isGrpc ? "gRPC" : isProto ? "PROTO" : "HTTP";
179
189
  if ((0, debug_logger_1.isDebugEnabled)()) {
180
190
  (0, debug_logger_1.infoLog)(`[OpenTelemetry] Service: ${this.serviceName}, Environment: ${this.environment}`);
181
191
  (0, debug_logger_1.infoLog)(`[OpenTelemetry] Endpoint: ${normalizedEndpoint} (${transportLabel})`);
@@ -188,7 +198,7 @@ class OpenTelemetryTracingProvider {
188
198
  };
189
199
  const token = config?.otlpAuthToken || this.otlpAuthToken || config?.secretToken || this.secretToken;
190
200
  if (token) {
191
- headers['Authorization'] = `Bearer ${token}`;
201
+ headers["Authorization"] = `Bearer ${token}`;
192
202
  }
193
203
  return Object.keys(headers).length > 0 ? headers : undefined;
194
204
  };
@@ -196,7 +206,7 @@ class OpenTelemetryTracingProvider {
196
206
  let otlpExporter;
197
207
  if (isGrpc) {
198
208
  // @ts-ignore - Optional peer dependency
199
- const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-grpc')));
209
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/exporter-trace-otlp-grpc")));
200
210
  otlpExporter = new OTLPTraceExporter({
201
211
  url: normalizedEndpoint,
202
212
  headers: buildHeaders(),
@@ -204,7 +214,7 @@ class OpenTelemetryTracingProvider {
204
214
  }
205
215
  else if (isProto) {
206
216
  // @ts-ignore - Optional peer dependency
207
- const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-proto')));
217
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/exporter-trace-otlp-proto")));
208
218
  otlpExporter = new OTLPTraceExporter({
209
219
  url: normalizedEndpoint,
210
220
  headers: buildHeaders(),
@@ -212,7 +222,7 @@ class OpenTelemetryTracingProvider {
212
222
  }
213
223
  else {
214
224
  // @ts-ignore - Optional peer dependency
215
- const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-http')));
225
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/exporter-trace-otlp-http")));
216
226
  otlpExporter = new OTLPTraceExporter({
217
227
  url: normalizedEndpoint,
218
228
  headers: buildHeaders(),
@@ -225,7 +235,7 @@ class OpenTelemetryTracingProvider {
225
235
  let traceExporter = otlpExporterWithLogging;
226
236
  if (shouldEnableConsole) {
227
237
  // @ts-ignore - Optional peer dependency
228
- const { ConsoleSpanExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/sdk-trace-base')));
238
+ const { ConsoleSpanExporter } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/sdk-trace-base")));
229
239
  const consoleExporter = new ConsoleSpanExporter();
230
240
  // Combined Exporter để export ra cả console và OTLP
231
241
  traceExporter = {
@@ -235,43 +245,50 @@ class OpenTelemetryTracingProvider {
235
245
  const errors = [];
236
246
  const checkComplete = () => {
237
247
  if (completed === 2) {
238
- resultCallback(hasError ? { code: 1, error: new Error(errors.map(e => e.message).join('; ')) } : { code: 0 });
248
+ resultCallback(hasError
249
+ ? { code: 1, error: new Error(errors.map(e => e.message).join("; ")) }
250
+ : { code: 0 });
239
251
  }
240
252
  };
241
253
  // Console exporter
242
254
  consoleExporter.export(spans, (result) => {
243
- if (result.error) {
255
+ const exportResult = result;
256
+ if (exportResult.error) {
244
257
  hasError = true;
245
- errors.push(result.error);
258
+ errors.push(exportResult.error);
246
259
  }
247
260
  completed++;
248
261
  checkComplete();
249
262
  });
250
263
  // OTLP exporter with logging
251
264
  otlpExporterWithLogging.export(spans, (result) => {
252
- if (result.error) {
265
+ const exportResult = result;
266
+ if (exportResult.error) {
253
267
  hasError = true;
254
- errors.push(result.error);
268
+ errors.push(exportResult.error);
255
269
  }
256
270
  completed++;
257
271
  checkComplete();
258
272
  });
259
273
  },
260
274
  shutdown: async () => {
261
- await Promise.all([consoleExporter.shutdown(), otlpExporterWithLogging.shutdown()]);
275
+ await Promise.all([
276
+ consoleExporter.shutdown(),
277
+ otlpExporterWithLogging.shutdown(),
278
+ ]);
262
279
  },
263
280
  };
264
281
  }
265
282
  // Resource attributes
266
283
  const sdkResource = resourceFromAttributes({
267
- 'service.name': config?.serviceName || this.serviceName,
268
- 'deployment.environment': config?.environment || this.environment,
269
- 'service.version': process.env.npm_package_version || '1.0.0',
270
- 'service.instance.id': `${process.pid}`,
271
- 'host.name': require('os').hostname(),
284
+ "service.name": config?.serviceName || this.serviceName,
285
+ "deployment.environment": config?.environment || this.environment,
286
+ "service.version": process.env.npm_package_version || "1.0.0",
287
+ "service.instance.id": `${process.pid}`,
288
+ "host.name": require("os").hostname(),
272
289
  });
273
290
  // Use BatchSpanProcessor for efficient batching with explicit flush
274
- const { BatchSpanProcessor } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/sdk-trace-base')));
291
+ const { BatchSpanProcessor } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/sdk-trace-base")));
275
292
  // Create a custom processor that extends BatchSpanProcessor behavior
276
293
  // but also applies transaction naming
277
294
  const transactionNameProcessor = new TransactionNameProcessor();
@@ -304,7 +321,8 @@ class OpenTelemetryTracingProvider {
304
321
  // Khởi động SDK
305
322
  this.sdk.start();
306
323
  // Install HTTP interceptor for debug mode
307
- const interceptor = (0, debug_exporter_wrapper_1.createHttpRequestInterceptor)();
324
+ const { createHttpRequestInterceptor } = require("../utils/debug-exporter-wrapper");
325
+ const interceptor = createHttpRequestInterceptor();
308
326
  if (interceptor) {
309
327
  interceptor.install();
310
328
  }
@@ -312,32 +330,99 @@ class OpenTelemetryTracingProvider {
312
330
  this.tracer = trace.getTracer(config?.serviceName || this.serviceName);
313
331
  this.initialized = true;
314
332
  if ((0, debug_logger_1.isDebugEnabled)()) {
315
- (0, debug_logger_1.infoLog)('[OpenTelemetry] Initialized');
333
+ (0, debug_logger_1.infoLog)("[OpenTelemetry] Initialized");
316
334
  }
317
335
  }
318
336
  catch (error) {
319
- (0, debug_logger_1.errorLog)('[OpenTelemetry] Failed to initialize:', error);
337
+ (0, debug_logger_1.errorLog)("[OpenTelemetry] Failed to initialize:", error);
320
338
  throw error;
321
339
  }
322
340
  }
323
341
  /**
324
342
  * Bắt đầu một span mới
325
343
  */
326
- startSpan(name, attributes, spanKind = 'INTERNAL') {
344
+ startSpan(name, attributes, spanKind = "INTERNAL") {
327
345
  if (!this.initialized || !this.tracer) {
328
346
  if ((0, debug_logger_1.isDebugEnabled)()) {
329
- console.warn('[OpenTelemetry] Not initialized');
347
+ console.warn("[OpenTelemetry] Not initialized");
330
348
  }
331
349
  return null;
332
350
  }
333
351
  // @ts-ignore - Optional peer dependency
334
- const { context, SpanKind } = require('@opentelemetry/api');
335
- const span = this.tracer.startSpan(name, {
352
+ const { context } = require("@opentelemetry/api");
353
+ const tracer = this.tracer;
354
+ const span = tracer.startSpan(name, {
336
355
  attributes,
337
356
  kind: this.mapSpanKind(spanKind),
338
357
  }, context.active());
339
358
  // Log span details when debug mode is on
340
- (0, debug_logger_1.logSpan)('OpenTelemetry', span);
359
+ if (span) {
360
+ const spanForLog = { name, kind: this.mapSpanKind(spanKind) };
361
+ (0, debug_logger_1.logSpan)("OpenTelemetry", spanForLog);
362
+ }
363
+ // Add end protection to prevent operations on ended span
364
+ if (span) {
365
+ let isEnded = false;
366
+ // Override setAttribute to check if span has ended
367
+ if (typeof span.setAttribute === "function") {
368
+ const originalSetAttribute = span.setAttribute.bind(span);
369
+ span.setAttribute = (key, value) => {
370
+ if (isEnded) {
371
+ if ((0, debug_logger_1.isDebugEnabled)()) {
372
+ console.warn(`[OpenTelemetry] Cannot set attribute "${key}" on ended span "${name}". ` +
373
+ `This attribute will not be sent to APM server.`);
374
+ }
375
+ return span;
376
+ }
377
+ originalSetAttribute(key, value);
378
+ return span;
379
+ };
380
+ }
381
+ // Override recordException to check if span has ended
382
+ if (typeof span.recordException === "function") {
383
+ const originalRecordException = span.recordException.bind(span);
384
+ span.recordException = (error) => {
385
+ if (isEnded) {
386
+ if ((0, debug_logger_1.isDebugEnabled)()) {
387
+ console.warn(`[OpenTelemetry] Cannot record exception on ended span "${name}". ` +
388
+ `This exception will not be sent to APM server.`);
389
+ }
390
+ return span;
391
+ }
392
+ originalRecordException(error);
393
+ return span;
394
+ };
395
+ }
396
+ // Override setStatus to check if span has ended
397
+ if (typeof span.setStatus === "function") {
398
+ const originalSetStatus = span.setStatus.bind(span);
399
+ span.setStatus = (status) => {
400
+ if (isEnded) {
401
+ if ((0, debug_logger_1.isDebugEnabled)()) {
402
+ console.warn(`[OpenTelemetry] Cannot set status on ended span "${name}". ` +
403
+ `This status will not be sent to APM server.`);
404
+ }
405
+ return span;
406
+ }
407
+ originalSetStatus(status);
408
+ return span;
409
+ };
410
+ }
411
+ // Override end to prevent duplicate ends
412
+ if (typeof span.end === "function") {
413
+ const originalEnd = span.end.bind(span);
414
+ span.end = () => {
415
+ if (isEnded) {
416
+ if ((0, debug_logger_1.isDebugEnabled)()) {
417
+ console.warn(`[OpenTelemetry] Span "${name}" has already been ended. Ignoring duplicate end().`);
418
+ }
419
+ return;
420
+ }
421
+ isEnded = true;
422
+ originalEnd();
423
+ };
424
+ }
425
+ }
341
426
  return span;
342
427
  }
343
428
  /**
@@ -345,7 +430,7 @@ class OpenTelemetryTracingProvider {
345
430
  */
346
431
  async startSpanWithParent(name, fn, attributes) {
347
432
  // @ts-ignore - Optional peer dependency
348
- const { context, trace, SpanStatusCode } = require('@opentelemetry/api');
433
+ const { context, trace, SpanStatusCode } = require("@opentelemetry/api");
349
434
  return context.with(trace.setSpan(context.active(), this.startSpan(name, attributes)), async () => {
350
435
  const span = trace.getActiveSpan();
351
436
  try {
@@ -353,8 +438,13 @@ class OpenTelemetryTracingProvider {
353
438
  return result;
354
439
  }
355
440
  catch (error) {
356
- span?.recordException(error);
357
- span?.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
441
+ if (span) {
442
+ span.recordException(error);
443
+ span.setStatus({
444
+ code: SpanStatusCode.ERROR,
445
+ message: error.message,
446
+ });
447
+ }
358
448
  if ((0, debug_logger_1.isDebugEnabled)()) {
359
449
  console.log(`[APM-DEBUG] [OpenTelemetry] Span failed:`, name, error.message || error);
360
450
  }
@@ -373,11 +463,14 @@ class OpenTelemetryTracingProvider {
373
463
  return;
374
464
  }
375
465
  // @ts-ignore - Optional peer dependency
376
- const { trace, SpanStatusCode } = require('@opentelemetry/api');
466
+ const { trace, SpanStatusCode } = require("@opentelemetry/api");
377
467
  const span = trace.getActiveSpan();
378
468
  if (span) {
379
469
  span.recordException(error);
380
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
470
+ span.setStatus({
471
+ code: SpanStatusCode.ERROR,
472
+ message: error.message,
473
+ });
381
474
  }
382
475
  }
383
476
  /**
@@ -388,7 +481,7 @@ class OpenTelemetryTracingProvider {
388
481
  return;
389
482
  }
390
483
  // @ts-ignore - Optional peer dependency
391
- const { trace } = require('@opentelemetry/api');
484
+ const { trace } = require("@opentelemetry/api");
392
485
  const span = trace.getActiveSpan();
393
486
  if (span) {
394
487
  span.setAttribute(key, value);
@@ -398,15 +491,15 @@ class OpenTelemetryTracingProvider {
398
491
  * Force flush all pending spans
399
492
  */
400
493
  async forceFlush() {
401
- if (this.sdk && this.sdk.forceFlush) {
494
+ if (this.sdk) {
402
495
  try {
403
496
  await this.sdk.forceFlush();
404
497
  if ((0, debug_logger_1.isDebugEnabled)()) {
405
- console.log('[APM-DEBUG] Force flush completed');
498
+ console.log("[APM-DEBUG] Force flush completed");
406
499
  }
407
500
  }
408
501
  catch (error) {
409
- (0, debug_logger_1.errorLog)('[OpenTelemetry] Force flush failed:', error);
502
+ (0, debug_logger_1.errorLog)("[OpenTelemetry] Force flush failed:", error);
410
503
  }
411
504
  }
412
505
  }
@@ -418,7 +511,7 @@ class OpenTelemetryTracingProvider {
418
511
  return;
419
512
  }
420
513
  // @ts-ignore - Optional peer dependency
421
- const { trace } = require('@opentelemetry/api');
514
+ const { trace } = require("@opentelemetry/api");
422
515
  const activeSpan = span || trace.getActiveSpan();
423
516
  if (activeSpan) {
424
517
  activeSpan.end();
@@ -437,17 +530,17 @@ class OpenTelemetryTracingProvider {
437
530
  */
438
531
  mapSpanKind(kind) {
439
532
  // @ts-ignore - Optional peer dependency
440
- const { SpanKind } = require('@opentelemetry/api');
533
+ const { SpanKind } = require("@opentelemetry/api");
441
534
  switch (kind.toUpperCase()) {
442
- case 'SERVER':
535
+ case "SERVER":
443
536
  return SpanKind.SERVER;
444
- case 'CLIENT':
537
+ case "CLIENT":
445
538
  return SpanKind.CLIENT;
446
- case 'PRODUCER':
539
+ case "PRODUCER":
447
540
  return SpanKind.PRODUCER;
448
- case 'CONSUMER':
541
+ case "CONSUMER":
449
542
  return SpanKind.CONSUMER;
450
- case 'INTERNAL':
543
+ case "INTERNAL":
451
544
  default:
452
545
  return SpanKind.INTERNAL;
453
546
  }
@@ -1,4 +1,5 @@
1
- import { ITracingProvider } from '../interfaces/tracing-provider.interface';
1
+ import { ITracingProvider } from "../interfaces/tracing-provider.interface";
2
+ import { ISpan } from "../types/apm.types";
2
3
  /**
3
4
  * Tracing Service - Wrapper cho ITracingProvider
4
5
  * Service này được inject vào controllers/services để sử dụng tracing
@@ -14,7 +15,7 @@ import { ITracingProvider } from '../interfaces/tracing-provider.interface';
14
15
  * try {
15
16
  * // ... code
16
17
  * } finally {
17
- * span.end();
18
+ * span?.end();
18
19
  * }
19
20
  * }
20
21
  *
@@ -46,7 +47,7 @@ export declare class TracingService {
46
47
  * span.end();
47
48
  * }
48
49
  */
49
- startSpan(name: string, attributes?: Record<string, string | number>, spanKind?: string): any;
50
+ startSpan(name: string, attributes?: Record<string, string | number>, spanKind?: string): ISpan | null;
50
51
  /**
51
52
  * Thực thi function với context tracing (auto-close span)
52
53
  * @param name Tên của span
@@ -63,7 +64,7 @@ export declare class TracingService {
63
64
  * }
64
65
  * );
65
66
  */
66
- startSpanWithParent<T>(name: string, fn: (span: any) => Promise<T>, attributes?: Record<string, string | number>): Promise<T>;
67
+ startSpanWithParent<T>(name: string, fn: (span: ISpan | null) => Promise<T>, attributes?: Record<string, string | number>): Promise<T>;
67
68
  /**
68
69
  * Capture error vào active span hiện tại
69
70
  * @param error Error object
@@ -79,7 +80,7 @@ export declare class TracingService {
79
80
  * Kết thúc span manually
80
81
  * @param span Span cần end (nếu không truyền, sẽ end active span)
81
82
  */
82
- endSpan(span?: any): void;
83
+ endSpan(span?: ISpan): void;
83
84
  /**
84
85
  * Flush và shutdown APM provider (cho graceful shutdown)
85
86
  */
@@ -1 +1 @@
1
- {"version":3,"file":"tracing.service.d.ts","sourceRoot":"","sources":["../../src/services/tracing.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,gBAAgB;IAEvD;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,GAAE,MAAmB,GAAG,GAAG;IAIzG;;;;;;;;;;;;;;;OAeG;IACH,mBAAmB,CAAC,CAAC,EACnB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,EAC7B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAC3C,OAAO,CAAC,CAAC,CAAC;IAIb;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAIhC;;;;OAIG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAIvD;;;OAGG;IACH,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI;IAIzB;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAGhC"}
1
+ {"version":3,"file":"tracing.service.d.ts","sourceRoot":"","sources":["../../src/services/tracing.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC5E,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,gBAAgB;IAEvD;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAC5C,QAAQ,GAAE,MAAmB,GAC5B,KAAK,GAAG,IAAI;IAIf;;;;;;;;;;;;;;;OAeG;IACH,mBAAmB,CAAC,CAAC,EACnB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EACtC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAC3C,OAAO,CAAC,CAAC,CAAC;IAIb;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAIhC;;;;OAIG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAIvD;;;OAGG;IACH,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI;IAI3B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAGhC"}
@@ -16,7 +16,7 @@ exports.TracingService = void 0;
16
16
  * try {
17
17
  * // ... code
18
18
  * } finally {
19
- * span.end();
19
+ * span?.end();
20
20
  * }
21
21
  * }
22
22
  *
@@ -49,7 +49,7 @@ class TracingService {
49
49
  * span.end();
50
50
  * }
51
51
  */
52
- startSpan(name, attributes, spanKind = 'INTERNAL') {
52
+ startSpan(name, attributes, spanKind = "INTERNAL") {
53
53
  return this.provider.startSpan(name, attributes, spanKind);
54
54
  }
55
55
  /**
@@ -10,6 +10,8 @@ export declare enum ApmProvider {
10
10
  /** Elastic APM Node.js agent */
11
11
  ELASTIC_APM = "elastic-apm"
12
12
  }
13
+ /** APM Provider type string */
14
+ export type ApmProviderType = `${ApmProvider}`;
13
15
  /**
14
16
  * Span kind types for tracing
15
17
  */
@@ -1 +1 @@
1
- {"version":3,"file":"apm-provider.type.d.ts","sourceRoot":"","sources":["../../src/types/apm-provider.type.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,oBAAY,WAAW;IACrB,uCAAuC;IACvC,aAAa,kBAAkB;IAC/B,gCAAgC;IAChC,WAAW,gBAAgB;CAC5B;AAED;;GAEG;AACH,oBAAY,QAAQ;IAClB,yBAAyB;IACzB,QAAQ,aAAa;IACrB,kDAAkD;IAClD,MAAM,WAAW;IACjB,iDAAiD;IACjD,MAAM,WAAW;IACjB,uBAAuB;IACvB,QAAQ,aAAa;IACrB,uBAAuB;IACvB,QAAQ,aAAa;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,oBAAoB;IACpB,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC,2BAA2B;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB"}
1
+ {"version":3,"file":"apm-provider.type.d.ts","sourceRoot":"","sources":["../../src/types/apm-provider.type.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,oBAAY,WAAW;IACrB,uCAAuC;IACvC,aAAa,kBAAkB;IAC/B,gCAAgC;IAChC,WAAW,gBAAgB;CAC5B;AAED,+BAA+B;AAC/B,MAAM,MAAM,eAAe,GAAG,GAAG,WAAW,EAAE,CAAC;AAE/C;;GAEG;AACH,oBAAY,QAAQ;IAClB,yBAAyB;IACzB,QAAQ,aAAa;IACrB,kDAAkD;IAClD,MAAM,WAAW;IACjB,iDAAiD;IACjD,MAAM,WAAW;IACjB,uBAAuB;IACvB,QAAQ,aAAa;IACrB,uBAAuB;IACvB,QAAQ,aAAa;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,oBAAoB;IACpB,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC,2BAA2B;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB"}