@vtvlive/interactive-apm 0.0.2 → 0.0.4

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 (43) hide show
  1. package/README.md +201 -123
  2. package/dist/factories/tracing-provider.factory.d.ts +8 -3
  3. package/dist/factories/tracing-provider.factory.d.ts.map +1 -1
  4. package/dist/factories/tracing-provider.factory.js +17 -13
  5. package/dist/index.d.ts +12 -10
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -1
  8. package/dist/init/elastic-apm-init.d.ts +25 -3
  9. package/dist/init/elastic-apm-init.d.ts.map +1 -1
  10. package/dist/init/elastic-apm-init.js +29 -12
  11. package/dist/init/opentelemetry-init.d.ts +8 -1
  12. package/dist/init/opentelemetry-init.d.ts.map +1 -1
  13. package/dist/init/opentelemetry-init.js +145 -44
  14. package/dist/interfaces/tracing-provider.interface.d.ts +13 -3
  15. package/dist/interfaces/tracing-provider.interface.d.ts.map +1 -1
  16. package/dist/modules/tracing.module.d.ts +5 -5
  17. package/dist/modules/tracing.module.d.ts.map +1 -1
  18. package/dist/modules/tracing.module.js +2 -1
  19. package/dist/providers/elastic-apm.tracing-provider.d.ts +23 -5
  20. package/dist/providers/elastic-apm.tracing-provider.d.ts.map +1 -1
  21. package/dist/providers/elastic-apm.tracing-provider.js +127 -28
  22. package/dist/providers/opentelemetry.tracing-provider.d.ts +12 -4
  23. package/dist/providers/opentelemetry.tracing-provider.d.ts.map +1 -1
  24. package/dist/providers/opentelemetry.tracing-provider.js +328 -67
  25. package/dist/services/tracing.service.d.ts +6 -5
  26. package/dist/services/tracing.service.d.ts.map +1 -1
  27. package/dist/services/tracing.service.js +2 -2
  28. package/dist/types/apm.types.d.ts +162 -0
  29. package/dist/types/apm.types.d.ts.map +1 -0
  30. package/dist/types/apm.types.js +37 -0
  31. package/dist/types/otlp-transport.type.d.ts +14 -0
  32. package/dist/types/otlp-transport.type.d.ts.map +1 -0
  33. package/dist/types/otlp-transport.type.js +17 -0
  34. package/dist/utils/debug-exporter-wrapper.d.ts +36 -0
  35. package/dist/utils/debug-exporter-wrapper.d.ts.map +1 -0
  36. package/dist/utils/debug-exporter-wrapper.js +247 -0
  37. package/dist/utils/debug-logger.d.ts +81 -0
  38. package/dist/utils/debug-logger.d.ts.map +1 -0
  39. package/dist/utils/debug-logger.js +236 -0
  40. package/dist/utils/tracing.helper.d.ts +2 -2
  41. package/dist/utils/tracing.helper.d.ts.map +1 -1
  42. package/dist/utils/tracing.helper.js +8 -4
  43. package/package.json +24 -3
@@ -34,6 +34,87 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.OpenTelemetryTracingProvider = void 0;
37
+ const otlp_transport_type_1 = require("../types/otlp-transport.type");
38
+ const debug_logger_1 = require("../utils/debug-logger");
39
+ const debug_exporter_wrapper_1 = require("../utils/debug-exporter-wrapper");
40
+ /**
41
+ * Transaction Name Processor for OpenTelemetry
42
+ * Sets transaction names to format: "GET /api/healthcheck/ping"
43
+ */
44
+ class TransactionNameProcessor {
45
+ constructor() {
46
+ this.cachedSpanKind = null;
47
+ }
48
+ onStart(span) {
49
+ // Lazy load SpanKind to avoid top-level import failure when @opentelemetry/api is absent
50
+ if (!this.cachedSpanKind) {
51
+ try {
52
+ // @ts-ignore - Optional peer dependency
53
+ this.cachedSpanKind = require("@opentelemetry/api").SpanKind;
54
+ }
55
+ catch {
56
+ // If @opentelemetry/api is not available, skip processing
57
+ return;
58
+ }
59
+ }
60
+ // Only process root spans (SERVER spans without parent)
61
+ const s = span;
62
+ if (s.parent || s.kind !== this.cachedSpanKind?.SERVER) {
63
+ return;
64
+ }
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) {
71
+ // Format: GET /api/healthcheck/ping
72
+ const fullName = `${method} ${fullPath}`;
73
+ s.updateName?.(fullName);
74
+ }
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
+ }
85
+ }
86
+ /**
87
+ * Normalize OTLP endpoint URL to ensure consistent format
88
+ * - Ensure port is included
89
+ * - Remove trailing slashes
90
+ * - For gRPC, don't add /v1/traces (it uses different path)
91
+ */
92
+ function normalizeEndpoint(endpoint, isGrpc) {
93
+ try {
94
+ const url = new URL(endpoint);
95
+ // Ensure port is specified
96
+ if (!url.port) {
97
+ const defaultPort = url.protocol === "https:" ? "443" : "80";
98
+ url.port = defaultPort;
99
+ }
100
+ // Remove trailing slash
101
+ let pathname = url.pathname.replace(/\/+$/, "");
102
+ // For gRPC, remove /v1/traces as it uses different path
103
+ if (isGrpc && pathname.endsWith("/v1/traces")) {
104
+ pathname = pathname.replace("/v1/traces", "");
105
+ }
106
+ // For HTTP/PROTO, ensure /v1/traces path exists
107
+ if (!isGrpc && !pathname.endsWith("/v1/traces")) {
108
+ pathname = pathname + "/v1/traces";
109
+ }
110
+ url.pathname = pathname;
111
+ return url.toString();
112
+ }
113
+ catch {
114
+ // If URL parsing fails, return as-is
115
+ return endpoint;
116
+ }
117
+ }
37
118
  /**
38
119
  * OpenTelemetry Tracing Provider Implementation
39
120
  * Sử dụng OpenTelemetry SDK với OTLP exporter để gửi traces về Elastic APM
@@ -43,13 +124,29 @@ class OpenTelemetryTracingProvider {
43
124
  this.sdk = null;
44
125
  this.tracer = null;
45
126
  this.initialized = false;
46
- this.serviceName = config.serviceName || process.env.ELASTIC_APM_SERVICE_NAME || 'interactive-backend';
47
- this.environment = config.environment || process.env.ELASTIC_APM_ENVIRONMENT || 'development';
48
- this.otlpEndpoint = config.otlpEndpoint || process.env.ELASTIC_OTLP_ENDPOINT || 'http://localhost:8200/v1/traces';
49
- this.secretToken = config.secretToken || process.env.ELASTIC_APM_SECRET_TOKEN || '';
50
- this.otlpAuthToken = config.otlpAuthToken || process.env.ELASTIC_OTLP_AUTH_TOKEN || '';
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";
132
+ // Determine transport type (HTTP, gRPC, or PROTO)
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") {
137
+ this.otlpTransport = otlp_transport_type_1.OtlpTransport.GRPC;
138
+ }
139
+ else if (transportType === otlp_transport_type_1.OtlpTransport.PROTO || transportType === "proto") {
140
+ this.otlpTransport = otlp_transport_type_1.OtlpTransport.PROTO;
141
+ }
142
+ else {
143
+ this.otlpTransport = otlp_transport_type_1.OtlpTransport.HTTP;
144
+ }
145
+ this.secretToken = config.secretToken || process.env.ELASTIC_APM_SECRET_TOKEN || "";
146
+ this.otlpAuthToken = config.otlpAuthToken || process.env.ELASTIC_OTLP_AUTH_TOKEN || "";
51
147
  this.otlpHeaders = config.otlpHeaders || {};
52
- 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";
53
150
  }
54
151
  /**
55
152
  * Initialize OpenTelemetry SDK
@@ -62,19 +159,37 @@ class OpenTelemetryTracingProvider {
62
159
  try {
63
160
  // Dynamic import to avoid errors when @opentelemetry packages are not installed
64
161
  // @ts-ignore - Optional peer dependency
65
- const { NodeSDK } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/sdk-node')));
162
+ const { NodeSDK } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/sdk-node")));
66
163
  // @ts-ignore - Optional peer dependency
67
- const { HttpInstrumentation } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/instrumentation-http')));
164
+ const { HttpInstrumentation } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/instrumentation-http")));
68
165
  // @ts-ignore - Optional peer dependency
69
- const { ExpressInstrumentation } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/instrumentation-express')));
166
+ const { ExpressInstrumentation } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/instrumentation-express")));
70
167
  // @ts-ignore - Optional peer dependency
71
- const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-http')));
168
+ const { resourceFromAttributes } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/resources")));
72
169
  // @ts-ignore - Optional peer dependency
73
- const { resourceFromAttributes } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/resources')));
74
- // @ts-ignore - Optional peer dependency
75
- const { trace } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/api')));
76
- console.log(`[OpenTelemetry] Service: ${this.serviceName}, Environment: ${this.environment}`);
77
- console.log(`[OpenTelemetry] Endpoint: ${config?.otlpEndpoint || this.otlpEndpoint}`);
170
+ const { trace } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/api")));
171
+ const endpoint = config?.otlpEndpoint || this.otlpEndpoint;
172
+ const isGrpc = this.otlpTransport === otlp_transport_type_1.OtlpTransport.GRPC;
173
+ const isProto = this.otlpTransport === otlp_transport_type_1.OtlpTransport.PROTO;
174
+ // Normalize endpoint to ensure consistent format
175
+ const normalizedEndpoint = normalizeEndpoint(endpoint, isGrpc);
176
+ // Log initialization details when debug mode is on
177
+ (0, debug_logger_1.logInitialization)("OpenTelemetry", {
178
+ serviceName: config?.serviceName || this.serviceName,
179
+ environment: config?.environment || this.environment,
180
+ otlpEndpoint: normalizedEndpoint,
181
+ transport: isGrpc ? "gRPC" : isProto ? "PROTO (protobuf over HTTP)" : "HTTP",
182
+ hasToken: !!(config?.otlpAuthToken ||
183
+ this.otlpAuthToken ||
184
+ config?.secretToken ||
185
+ this.secretToken),
186
+ enableConsoleExporter: config?.enableConsoleExporter ?? this.enableConsoleExporter,
187
+ });
188
+ const transportLabel = isGrpc ? "gRPC" : isProto ? "PROTO" : "HTTP";
189
+ if ((0, debug_logger_1.isDebugEnabled)()) {
190
+ (0, debug_logger_1.infoLog)(`[OpenTelemetry] Service: ${this.serviceName}, Environment: ${this.environment}`);
191
+ (0, debug_logger_1.infoLog)(`[OpenTelemetry] Endpoint: ${normalizedEndpoint} (${transportLabel})`);
192
+ }
78
193
  // OTLP Exporter - Build headers with Authorization
79
194
  const buildHeaders = () => {
80
195
  const headers = {
@@ -83,35 +198,44 @@ class OpenTelemetryTracingProvider {
83
198
  };
84
199
  const token = config?.otlpAuthToken || this.otlpAuthToken || config?.secretToken || this.secretToken;
85
200
  if (token) {
86
- headers['Authorization'] = `Bearer ${token}`;
201
+ headers["Authorization"] = `Bearer ${token}`;
87
202
  }
88
203
  return Object.keys(headers).length > 0 ? headers : undefined;
89
204
  };
90
- const otlpExporter = new OTLPTraceExporter({
91
- url: config?.otlpEndpoint || this.otlpEndpoint,
92
- headers: buildHeaders(),
93
- });
94
- // Wrap OTLP exporter - only log errors
95
- const otlpExporterWithLogging = {
96
- export: (spans, resultCallback) => {
97
- otlpExporter.export(spans, (result) => {
98
- if (result.error) {
99
- const endpoint = config?.otlpEndpoint || this.otlpEndpoint;
100
- console.error(`[OpenTelemetry] Export to ${endpoint} failed: ${result.error instanceof Error ? result.error.message : result.error}`);
101
- }
102
- resultCallback(result);
103
- });
104
- },
105
- shutdown: async () => {
106
- await otlpExporter.shutdown();
107
- },
108
- };
205
+ // Import the correct exporter based on transport type
206
+ let otlpExporter;
207
+ if (isGrpc) {
208
+ // @ts-ignore - Optional peer dependency
209
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/exporter-trace-otlp-grpc")));
210
+ otlpExporter = new OTLPTraceExporter({
211
+ url: normalizedEndpoint,
212
+ headers: buildHeaders(),
213
+ });
214
+ }
215
+ else if (isProto) {
216
+ // @ts-ignore - Optional peer dependency
217
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/exporter-trace-otlp-proto")));
218
+ otlpExporter = new OTLPTraceExporter({
219
+ url: normalizedEndpoint,
220
+ headers: buildHeaders(),
221
+ });
222
+ }
223
+ else {
224
+ // @ts-ignore - Optional peer dependency
225
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/exporter-trace-otlp-http")));
226
+ otlpExporter = new OTLPTraceExporter({
227
+ url: normalizedEndpoint,
228
+ headers: buildHeaders(),
229
+ });
230
+ }
231
+ // Wrap with debug exporter for detailed request/response logging
232
+ const otlpExporterWithLogging = (0, debug_exporter_wrapper_1.createDebugExporter)(otlpExporter, normalizedEndpoint);
109
233
  // Use console exporter if enabled
110
234
  const shouldEnableConsole = config?.enableConsoleExporter ?? this.enableConsoleExporter;
111
235
  let traceExporter = otlpExporterWithLogging;
112
236
  if (shouldEnableConsole) {
113
237
  // @ts-ignore - Optional peer dependency
114
- 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")));
115
239
  const consoleExporter = new ConsoleSpanExporter();
116
240
  // Combined Exporter để export ra cả console và OTLP
117
241
  traceExporter = {
@@ -121,74 +245,184 @@ class OpenTelemetryTracingProvider {
121
245
  const errors = [];
122
246
  const checkComplete = () => {
123
247
  if (completed === 2) {
124
- 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 });
125
251
  }
126
252
  };
127
253
  // Console exporter
128
254
  consoleExporter.export(spans, (result) => {
129
- if (result.error) {
255
+ const exportResult = result;
256
+ if (exportResult.error) {
130
257
  hasError = true;
131
- errors.push(result.error);
258
+ errors.push(exportResult.error);
132
259
  }
133
260
  completed++;
134
261
  checkComplete();
135
262
  });
136
263
  // OTLP exporter with logging
137
264
  otlpExporterWithLogging.export(spans, (result) => {
138
- if (result.error) {
265
+ const exportResult = result;
266
+ if (exportResult.error) {
139
267
  hasError = true;
140
- errors.push(result.error);
268
+ errors.push(exportResult.error);
141
269
  }
142
270
  completed++;
143
271
  checkComplete();
144
272
  });
145
273
  },
146
274
  shutdown: async () => {
147
- await Promise.all([consoleExporter.shutdown(), otlpExporterWithLogging.shutdown()]);
275
+ await Promise.all([
276
+ consoleExporter.shutdown(),
277
+ otlpExporterWithLogging.shutdown(),
278
+ ]);
148
279
  },
149
280
  };
150
281
  }
151
282
  // Resource attributes
152
283
  const sdkResource = resourceFromAttributes({
153
- 'service.name': config?.serviceName || this.serviceName,
154
- 'deployment.environment': config?.environment || this.environment,
155
- 'service.version': process.env.npm_package_version || '1.0.0',
156
- 'service.instance.id': `${process.pid}`,
157
- '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(),
158
289
  });
290
+ // Use BatchSpanProcessor for efficient batching with explicit flush
291
+ const { BatchSpanProcessor } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/sdk-trace-base")));
292
+ // Create a custom processor that extends BatchSpanProcessor behavior
293
+ // but also applies transaction naming
294
+ const transactionNameProcessor = new TransactionNameProcessor();
295
+ const batchProcessor = new BatchSpanProcessor(traceExporter);
296
+ // Wrap the batch processor to also do transaction naming
297
+ const processorWithNaming = {
298
+ onStart: (span, context) => {
299
+ transactionNameProcessor.onStart(span);
300
+ batchProcessor.onStart(span, context);
301
+ },
302
+ onEnd: (span) => {
303
+ transactionNameProcessor.onEnd(span);
304
+ batchProcessor.onEnd(span);
305
+ },
306
+ forceFlush: async () => {
307
+ await batchProcessor.forceFlush();
308
+ },
309
+ shutdown: async () => {
310
+ await batchProcessor.shutdown();
311
+ },
312
+ };
159
313
  // Khởi tạo SDK
160
314
  this.sdk = new NodeSDK({
161
315
  serviceName: config?.serviceName || this.serviceName,
162
- traceExporter,
316
+ traceExporter: undefined, // Don't use traceExporter when using spanProcessor
163
317
  instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()],
164
318
  resource: sdkResource,
319
+ spanProcessor: processorWithNaming,
165
320
  });
166
321
  // Khởi động SDK
167
322
  this.sdk.start();
323
+ // Install HTTP interceptor for debug mode
324
+ const { createHttpRequestInterceptor } = require("../utils/debug-exporter-wrapper");
325
+ const interceptor = createHttpRequestInterceptor();
326
+ if (interceptor) {
327
+ interceptor.install();
328
+ }
168
329
  // Get tracer
169
330
  this.tracer = trace.getTracer(config?.serviceName || this.serviceName);
170
331
  this.initialized = true;
171
- console.log('[OpenTelemetry] Initialized');
332
+ if ((0, debug_logger_1.isDebugEnabled)()) {
333
+ (0, debug_logger_1.infoLog)("[OpenTelemetry] Initialized");
334
+ }
172
335
  }
173
336
  catch (error) {
174
- console.error('[OpenTelemetry] Failed to initialize:', error);
337
+ (0, debug_logger_1.errorLog)("[OpenTelemetry] Failed to initialize:", error);
175
338
  throw error;
176
339
  }
177
340
  }
178
341
  /**
179
342
  * Bắt đầu một span mới
180
343
  */
181
- startSpan(name, attributes, spanKind = 'INTERNAL') {
344
+ startSpan(name, attributes, spanKind = "INTERNAL") {
182
345
  if (!this.initialized || !this.tracer) {
183
- console.warn('[OpenTelemetry] Not initialized');
346
+ if ((0, debug_logger_1.isDebugEnabled)()) {
347
+ console.warn("[OpenTelemetry] Not initialized");
348
+ }
184
349
  return null;
185
350
  }
186
351
  // @ts-ignore - Optional peer dependency
187
- const { context, SpanKind } = require('@opentelemetry/api');
188
- const span = this.tracer.startSpan(name, {
352
+ const { context } = require("@opentelemetry/api");
353
+ const tracer = this.tracer;
354
+ const span = tracer.startSpan(name, {
189
355
  attributes,
190
356
  kind: this.mapSpanKind(spanKind),
191
357
  }, context.active());
358
+ // Log span details when debug mode is on
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
+ }
192
426
  return span;
193
427
  }
194
428
  /**
@@ -196,7 +430,7 @@ class OpenTelemetryTracingProvider {
196
430
  */
197
431
  async startSpanWithParent(name, fn, attributes) {
198
432
  // @ts-ignore - Optional peer dependency
199
- const { context, trace, SpanStatusCode } = require('@opentelemetry/api');
433
+ const { context, trace, SpanStatusCode } = require("@opentelemetry/api");
200
434
  return context.with(trace.setSpan(context.active(), this.startSpan(name, attributes)), async () => {
201
435
  const span = trace.getActiveSpan();
202
436
  try {
@@ -204,8 +438,16 @@ class OpenTelemetryTracingProvider {
204
438
  return result;
205
439
  }
206
440
  catch (error) {
207
- span?.recordException(error);
208
- 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
+ }
448
+ if ((0, debug_logger_1.isDebugEnabled)()) {
449
+ console.log(`[APM-DEBUG] [OpenTelemetry] Span failed:`, name, error.message || error);
450
+ }
209
451
  throw error;
210
452
  }
211
453
  finally {
@@ -221,11 +463,14 @@ class OpenTelemetryTracingProvider {
221
463
  return;
222
464
  }
223
465
  // @ts-ignore - Optional peer dependency
224
- const { trace, SpanStatusCode } = require('@opentelemetry/api');
466
+ const { trace, SpanStatusCode } = require("@opentelemetry/api");
225
467
  const span = trace.getActiveSpan();
226
468
  if (span) {
227
469
  span.recordException(error);
228
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
470
+ span.setStatus({
471
+ code: SpanStatusCode.ERROR,
472
+ message: error.message,
473
+ });
229
474
  }
230
475
  }
231
476
  /**
@@ -236,12 +481,28 @@ class OpenTelemetryTracingProvider {
236
481
  return;
237
482
  }
238
483
  // @ts-ignore - Optional peer dependency
239
- const { trace } = require('@opentelemetry/api');
484
+ const { trace } = require("@opentelemetry/api");
240
485
  const span = trace.getActiveSpan();
241
486
  if (span) {
242
487
  span.setAttribute(key, value);
243
488
  }
244
489
  }
490
+ /**
491
+ * Force flush all pending spans
492
+ */
493
+ async forceFlush() {
494
+ if (this.sdk) {
495
+ try {
496
+ await this.sdk.forceFlush();
497
+ if ((0, debug_logger_1.isDebugEnabled)()) {
498
+ console.log("[APM-DEBUG] Force flush completed");
499
+ }
500
+ }
501
+ catch (error) {
502
+ (0, debug_logger_1.errorLog)("[OpenTelemetry] Force flush failed:", error);
503
+ }
504
+ }
505
+ }
245
506
  /**
246
507
  * End span manually
247
508
  */
@@ -250,7 +511,7 @@ class OpenTelemetryTracingProvider {
250
511
  return;
251
512
  }
252
513
  // @ts-ignore - Optional peer dependency
253
- const { trace } = require('@opentelemetry/api');
514
+ const { trace } = require("@opentelemetry/api");
254
515
  const activeSpan = span || trace.getActiveSpan();
255
516
  if (activeSpan) {
256
517
  activeSpan.end();
@@ -269,17 +530,17 @@ class OpenTelemetryTracingProvider {
269
530
  */
270
531
  mapSpanKind(kind) {
271
532
  // @ts-ignore - Optional peer dependency
272
- const { SpanKind } = require('@opentelemetry/api');
533
+ const { SpanKind } = require("@opentelemetry/api");
273
534
  switch (kind.toUpperCase()) {
274
- case 'SERVER':
535
+ case "SERVER":
275
536
  return SpanKind.SERVER;
276
- case 'CLIENT':
537
+ case "CLIENT":
277
538
  return SpanKind.CLIENT;
278
- case 'PRODUCER':
539
+ case "PRODUCER":
279
540
  return SpanKind.PRODUCER;
280
- case 'CONSUMER':
541
+ case "CONSUMER":
281
542
  return SpanKind.CONSUMER;
282
- case 'INTERNAL':
543
+ case "INTERNAL":
283
544
  default:
284
545
  return SpanKind.INTERNAL;
285
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
  /**