@vtvlive/interactive-apm 0.0.2 → 0.0.3

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.
@@ -52,6 +52,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
52
52
  exports.shouldUseOpenTelemetry = shouldUseOpenTelemetry;
53
53
  exports.initOpenTelemetry = initOpenTelemetry;
54
54
  const apm_provider_type_1 = require("../types/apm-provider.type");
55
+ const otlp_transport_type_1 = require("../types/otlp-transport.type");
56
+ const debug_logger_1 = require("../utils/debug-logger");
57
+ const debug_exporter_wrapper_1 = require("../utils/debug-exporter-wrapper");
58
+ // @ts-ignore - Optional peer dependency
59
+ const api_1 = require("@opentelemetry/api");
60
+ /**
61
+ * Transaction Name Processor for OpenTelemetry
62
+ * Sets transaction names to format: "GET /api/healthcheck/ping"
63
+ */
64
+ class TransactionNameProcessor {
65
+ onStart(_span) {
66
+ // No-op - moved to onEnd to read final http.route
67
+ }
68
+ forceFlush() {
69
+ return Promise.resolve();
70
+ }
71
+ shutdown() {
72
+ return Promise.resolve();
73
+ }
74
+ onEnd(_span) {
75
+ // Only process root spans (SERVER spans without parent)
76
+ if (_span.parent || _span.kind !== api_1.SpanKind.SERVER) {
77
+ return;
78
+ }
79
+ // Get HTTP attributes (http.route is often set after routing completes)
80
+ const method = _span.attributes?.['http.method'];
81
+ const route = _span.attributes?.['http.route']; // /api/healthcheck/ping
82
+ const target = _span.attributes?.['http.target']; // fallback
83
+ if (method && (route || target)) {
84
+ // Format: GET /api/healthcheck/ping
85
+ const fullName = `${method} ${route || target}`;
86
+ _span.updateName(fullName);
87
+ }
88
+ }
89
+ }
55
90
  /**
56
91
  * Check if APM should use OpenTelemetry provider based on configuration
57
92
  * @returns true if OpenTelemetry should be used
@@ -60,6 +95,38 @@ function shouldUseOpenTelemetry() {
60
95
  const provider = process.env.APM_PROVIDER || apm_provider_type_1.ApmProvider.OPENTELEMETRY;
61
96
  return provider !== apm_provider_type_1.ApmProvider.ELASTIC_APM && provider !== 'elastic-apm';
62
97
  }
98
+ /**
99
+ * Normalize OTLP endpoint URL to ensure consistent format
100
+ * - Ensure port is included
101
+ * - Remove trailing slashes
102
+ * - For gRPC, don't add /v1/traces (it uses different path)
103
+ */
104
+ function normalizeEndpoint(endpoint, isGrpc) {
105
+ try {
106
+ const url = new URL(endpoint);
107
+ // Ensure port is specified
108
+ if (!url.port) {
109
+ const defaultPort = url.protocol === 'https:' ? '443' : '80';
110
+ url.port = defaultPort;
111
+ }
112
+ // Remove trailing slash
113
+ let pathname = url.pathname.replace(/\/+$/, '');
114
+ // For gRPC, remove /v1/traces as it uses different path
115
+ if (isGrpc && pathname.endsWith('/v1/traces')) {
116
+ pathname = pathname.replace('/v1/traces', '');
117
+ }
118
+ // For HTTP/PROTO, ensure /v1/traces path exists
119
+ if (!isGrpc && !pathname.endsWith('/v1/traces')) {
120
+ pathname = pathname.endsWith('/') ? pathname + 'v1/traces' : pathname + '/v1/traces';
121
+ }
122
+ url.pathname = pathname;
123
+ return url.toString();
124
+ }
125
+ catch (error) {
126
+ // If URL parsing fails, return as-is
127
+ return endpoint;
128
+ }
129
+ }
63
130
  /**
64
131
  * Initialize OpenTelemetry SDK for standalone usage
65
132
  *
@@ -71,12 +138,16 @@ async function initOpenTelemetry(options = {}) {
71
138
  const { NodeSDK } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/sdk-node')));
72
139
  // @ts-ignore - Optional peer dependency
73
140
  const { resourceFromAttributes } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/resources')));
74
- // @ts-ignore - Optional peer dependency
75
- const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-http')));
76
- console.log('[OpenTelemetry] Initializing...');
77
141
  const serviceName = options.serviceName || process.env.ELASTIC_APM_SERVICE_NAME || 'interactive-backend';
78
142
  const environment = options.environment || process.env.ELASTIC_APM_ENVIRONMENT || 'development';
143
+ // Determine transport type (HTTP, gRPC, or PROTO)
144
+ const transportType = (options.otlpTransport || process.env.ELASTIC_OTLP_TRANSPORT || otlp_transport_type_1.OtlpTransport.HTTP).toLowerCase();
145
+ const isGrpc = transportType === otlp_transport_type_1.OtlpTransport.GRPC || transportType === 'grpc';
146
+ const isProto = transportType === otlp_transport_type_1.OtlpTransport.PROTO || transportType === 'proto';
147
+ // Get endpoint from config or env (same endpoint for both transports)
79
148
  const otlpEndpoint = options.otlpEndpoint || process.env.ELASTIC_OTLP_ENDPOINT || 'http://localhost:8200/v1/traces';
149
+ // Validate and normalize endpoint
150
+ const normalizedEndpoint = normalizeEndpoint(otlpEndpoint, isGrpc);
80
151
  const secretToken = options.secretToken || process.env.ELASTIC_APM_SECRET_TOKEN;
81
152
  const otlpAuthToken = options.otlpAuthToken || process.env.ELASTIC_OTLP_AUTH_TOKEN;
82
153
  // Build headers: merge options.otlpHeaders with Authorization if token is provided
@@ -85,29 +156,49 @@ async function initOpenTelemetry(options = {}) {
85
156
  if (token) {
86
157
  headers['Authorization'] = `Bearer ${token}`;
87
158
  }
88
- console.log(`[OpenTelemetry] Service: ${serviceName}, Environment: ${environment}`);
89
- console.log(`[OpenTelemetry] Endpoint: ${otlpEndpoint}`);
159
+ // Log initialization details when debug mode is on
160
+ (0, debug_logger_1.logInitialization)('OpenTelemetry', {
161
+ serviceName,
162
+ environment,
163
+ otlpEndpoint: normalizedEndpoint,
164
+ transport: isGrpc ? 'gRPC' : isProto ? 'PROTO (protobuf over HTTP)' : 'HTTP',
165
+ hasToken: !!token,
166
+ enableConsoleExporter: options.enableConsoleExporter ?? process.env.ELASTIC_OTLP_ENABLE_CONSOLE_EXPORTER === 'true',
167
+ enableHttpInstrumentation: options.enableHttpInstrumentation !== false,
168
+ enableExpressInstrumentation: options.enableExpressInstrumentation !== false,
169
+ });
170
+ const transportLabel = isGrpc ? 'gRPC' : isProto ? 'PROTO' : 'HTTP';
171
+ (0, debug_logger_1.infoLog)(`[OpenTelemetry] Initializing with ${transportLabel} transport...`);
90
172
  // Build exporters array
91
173
  const exporters = [];
92
- // OTLP Exporter with detailed logging
93
- const otlpExporterBase = new OTLPTraceExporter({
94
- url: otlpEndpoint,
95
- headers: Object.keys(headers).length > 0 ? headers : undefined,
96
- });
97
- // Wrap OTLP exporter - only log errors
98
- const otlpExporter = {
99
- export: (spans, resultCallback) => {
100
- otlpExporterBase.export(spans, (result) => {
101
- if (result.error) {
102
- console.error(`[OpenTelemetry] Export failed: ${result.error instanceof Error ? result.error.message : result.error}`);
103
- }
104
- resultCallback(result);
105
- });
106
- },
107
- shutdown: async () => {
108
- await otlpExporterBase.shutdown();
109
- },
110
- };
174
+ // Create OTLP exporter based on transport type
175
+ let otlpExporterBase;
176
+ if (isGrpc) {
177
+ // @ts-ignore - Optional peer dependency
178
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-grpc')));
179
+ otlpExporterBase = new OTLPTraceExporter({
180
+ url: normalizedEndpoint,
181
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
182
+ });
183
+ }
184
+ else if (isProto) {
185
+ // @ts-ignore - Optional peer dependency
186
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-proto')));
187
+ otlpExporterBase = new OTLPTraceExporter({
188
+ url: normalizedEndpoint,
189
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
190
+ });
191
+ }
192
+ else {
193
+ // @ts-ignore - Optional peer dependency
194
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-http')));
195
+ otlpExporterBase = new OTLPTraceExporter({
196
+ url: normalizedEndpoint,
197
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
198
+ });
199
+ }
200
+ // Wrap with debug exporter for detailed request/response logging
201
+ const otlpExporter = (0, debug_exporter_wrapper_1.createDebugExporter)(otlpExporterBase, normalizedEndpoint);
111
202
  exporters.push(otlpExporter);
112
203
  // Console Exporter for debugging
113
204
  if (options.enableConsoleExporter ?? process.env.ELASTIC_OTLP_ENABLE_CONSOLE_EXPORTER === 'true') {
@@ -141,10 +232,16 @@ async function initOpenTelemetry(options = {}) {
141
232
  traceExporter: exporters.length === 1 ? exporters[0] : exporters, // Handle single/multiple exporters
142
233
  instrumentations,
143
234
  resource: sdkResource,
235
+ spanProcessor: new TransactionNameProcessor(),
144
236
  });
145
237
  // Start SDK
146
238
  sdk.start();
147
- console.log('[OpenTelemetry] Initialized');
239
+ // Install HTTP interceptor for debug mode
240
+ const interceptor = (0, debug_exporter_wrapper_1.createHttpRequestInterceptor)();
241
+ if (interceptor) {
242
+ interceptor.install();
243
+ }
244
+ (0, debug_logger_1.infoLog)('[OpenTelemetry] Initialized');
148
245
  // Setup graceful shutdown handlers
149
246
  const shutdown = async () => {
150
247
  await sdk.shutdown();
@@ -1 +1 @@
1
- {"version":3,"file":"elastic-apm.tracing-provider.d.ts","sourceRoot":"","sources":["../../src/providers/elastic-apm.tracing-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAE5E;;;GAGG;AACH,qBAAa,yBAA0B,YAAW,gBAAgB;IAChE,OAAO,CAAC,GAAG,CAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,MAAM,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAO;IAK1I;;;OAGG;IACH,UAAU,CAAC,MAAM,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IA6B5I;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,GAAE,MAAmB,GAAG,GAAG;IAkDzG;;OAEG;IACG,mBAAmB,CAAC,CAAC,EACzB,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;IAmBb;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAMhC;;OAEG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAOvD;;OAEG;IACH,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI;IAOzB;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAM/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAe1B"}
1
+ {"version":3,"file":"elastic-apm.tracing-provider.d.ts","sourceRoot":"","sources":["../../src/providers/elastic-apm.tracing-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAG5E;;;GAGG;AACH,qBAAa,yBAA0B,YAAW,gBAAgB;IAChE,OAAO,CAAC,GAAG,CAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,MAAM,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAO;IAK1I;;;OAGG;IACH,UAAU,CAAC,MAAM,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAgD5I;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,GAAE,MAAmB,GAAG,GAAG;IAyDzG;;OAEG;IACG,mBAAmB,CAAC,CAAC,EACzB,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;IAsBb;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAMhC;;OAEG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAOvD;;OAEG;IACH,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI;IAOzB;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAc/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAe1B"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ElasticApmTracingProvider = void 0;
4
+ const debug_logger_1 = require("../utils/debug-logger");
4
5
  /**
5
6
  * Elastic APM Tracing Provider Implementation
6
7
  * Sử dụng elastic-apm-node agent để gửi traces về Elastic APM server
@@ -22,22 +23,39 @@ class ElasticApmTracingProvider {
22
23
  // If agent is already started, just get reference
23
24
  if (apm.isStarted()) {
24
25
  this.apm = apm;
26
+ if ((0, debug_logger_1.isDebugEnabled)()) {
27
+ console.log(`[APM-DEBUG] [ElasticAPM] Agent already started, using existing instance`);
28
+ }
25
29
  return;
26
30
  }
31
+ const serverUrl = config?.serverUrl || process.env.ELASTIC_APM_SERVER_URL || 'http://localhost:8200';
32
+ const secretToken = config?.secretToken || process.env.ELASTIC_APM_SECRET_TOKEN;
33
+ const serviceVersion = config?.serviceVersion || process.env.npm_package_version || '1.0.0';
34
+ const environment = config?.environment || this.environment;
35
+ // Log initialization details when debug mode is on
36
+ (0, debug_logger_1.logInitialization)('ElasticAPM', {
37
+ serviceName: config?.serviceName || this.serviceName,
38
+ serverUrl,
39
+ hasSecretToken: !!secretToken,
40
+ serviceVersion,
41
+ environment,
42
+ });
27
43
  // Start the agent with provided config
28
44
  this.apm = apm.start({
29
45
  serviceName: config?.serviceName || this.serviceName,
30
- serverUrl: config?.serverUrl || process.env.ELASTIC_APM_SERVER_URL || 'http://localhost:8200',
31
- secretToken: config?.secretToken || process.env.ELASTIC_APM_SECRET_TOKEN,
32
- serviceVersion: config?.serviceVersion || process.env.npm_package_version || '1.0.0',
33
- environment: config?.environment || this.environment,
34
- logLevel: 'error',
46
+ serverUrl,
47
+ secretToken,
48
+ serviceVersion,
49
+ environment,
50
+ logLevel: (0, debug_logger_1.isDebugEnabled)() ? 'info' : 'error',
35
51
  });
36
- console.log(`[ElasticAPM] Service: ${this.serviceName}, Environment: ${this.environment}`);
37
- console.log('[ElasticAPM] Initialized');
52
+ if ((0, debug_logger_1.isDebugEnabled)()) {
53
+ (0, debug_logger_1.infoLog)(`[ElasticAPM] Service: ${this.serviceName}, Environment: ${this.environment}`);
54
+ (0, debug_logger_1.infoLog)('[ElasticAPM] Initialized');
55
+ }
38
56
  }
39
57
  catch (error) {
40
- console.error('[ElasticAPM] Failed to initialize:', error);
58
+ (0, debug_logger_1.errorLog)('[ElasticAPM] Failed to initialize:', error);
41
59
  throw error;
42
60
  }
43
61
  }
@@ -46,7 +64,9 @@ class ElasticApmTracingProvider {
46
64
  */
47
65
  startSpan(name, attributes, spanKind = 'INTERNAL') {
48
66
  if (!this.apm) {
49
- console.warn('[ElasticAPM] Not initialized');
67
+ if ((0, debug_logger_1.isDebugEnabled)()) {
68
+ console.warn('[ElasticAPM] Not initialized');
69
+ }
50
70
  return null;
51
71
  }
52
72
  // Elastic APM cần transaction để tạo span
@@ -75,10 +95,13 @@ class ElasticApmTracingProvider {
75
95
  }
76
96
  // Set span type dựa trên kind
77
97
  span.setType(this.mapSpanKindToType(spanKind));
98
+ // Log span details when debug mode is on
99
+ (0, debug_logger_1.logSpan)('ElasticAPM', span);
78
100
  // Override end() để end transaction sau
79
101
  const originalEnd = span.end.bind(span);
80
102
  span.end = () => {
81
103
  originalEnd();
104
+ // End transaction if we created it
82
105
  if (createdTransaction && transaction) {
83
106
  transaction.end();
84
107
  }
@@ -99,6 +122,9 @@ class ElasticApmTracingProvider {
99
122
  if (span) {
100
123
  span.outcome = 'failure';
101
124
  }
125
+ if ((0, debug_logger_1.isDebugEnabled)()) {
126
+ (0, debug_logger_1.infoLog)(`[ElasticAPM] Span failed: ${name} ${error.message || error}`);
127
+ }
102
128
  throw error;
103
129
  }
104
130
  finally {
@@ -138,7 +164,16 @@ class ElasticApmTracingProvider {
138
164
  */
139
165
  async shutdown() {
140
166
  if (this.apm) {
141
- await this.apm.flush();
167
+ try {
168
+ await this.apm.flush();
169
+ if ((0, debug_logger_1.isDebugEnabled)()) {
170
+ (0, debug_logger_1.infoLog)('[ElasticAPM] Shutdown completed successfully');
171
+ }
172
+ }
173
+ catch (error) {
174
+ (0, debug_logger_1.errorLog)('[ElasticAPM] Shutdown failed:', error);
175
+ throw error;
176
+ }
142
177
  }
143
178
  }
144
179
  /**
@@ -1,4 +1,5 @@
1
1
  import { ITracingProvider } from '../interfaces/tracing-provider.interface';
2
+ import { OtlpTransport } from '../types/otlp-transport.type';
2
3
  /**
3
4
  * OpenTelemetry Tracing Provider Implementation
4
5
  * Sử dụng OpenTelemetry SDK với OTLP exporter để gửi traces về Elastic APM
@@ -9,6 +10,7 @@ export declare class OpenTelemetryTracingProvider implements ITracingProvider {
9
10
  private readonly serviceName;
10
11
  private readonly environment;
11
12
  private readonly otlpEndpoint;
13
+ private readonly otlpTransport;
12
14
  private readonly secretToken;
13
15
  private readonly otlpAuthToken;
14
16
  private readonly otlpHeaders;
@@ -18,6 +20,7 @@ export declare class OpenTelemetryTracingProvider implements ITracingProvider {
18
20
  serviceName?: string;
19
21
  environment?: string;
20
22
  otlpEndpoint?: string;
23
+ otlpTransport?: OtlpTransport | string;
21
24
  secretToken?: string;
22
25
  otlpAuthToken?: string;
23
26
  otlpHeaders?: Record<string, string>;
@@ -52,6 +55,10 @@ export declare class OpenTelemetryTracingProvider implements ITracingProvider {
52
55
  * Set attribute cho active span
53
56
  */
54
57
  setAttribute(key: string, value: string | number): void;
58
+ /**
59
+ * Force flush all pending spans
60
+ */
61
+ forceFlush(): Promise<void>;
55
62
  /**
56
63
  * End span manually
57
64
  */
@@ -1 +1 @@
1
- {"version":3,"file":"opentelemetry.tracing-provider.d.ts","sourceRoot":"","sources":["../../src/providers/opentelemetry.tracing-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAE5E;;;GAGG;AACH,qBAAa,4BAA6B,YAAW,gBAAgB;IACnE,OAAO,CAAC,GAAG,CAAa;IACxB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;IACrD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAU;IAChD,OAAO,CAAC,WAAW,CAAS;gBAEhB,MAAM,GAAE;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACrC,qBAAqB,CAAC,EAAE,OAAO,CAAC;KAC5B;IAUN;;;OAGG;IACG,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACrC,qBAAqB,CAAC,EAAE,OAAO,CAAC;QAChC,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwIjB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,GAAE,MAAmB,GAAG,GAAG;IAqBzG;;OAEG;IACG,mBAAmB,CAAC,CAAC,EACzB,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;IAoBb;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAchC;;OAEG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAavD;;OAEG;IACH,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI;IAazB;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAM/B;;OAEG;IACH,OAAO,CAAC,WAAW;CAiBpB"}
1
+ {"version":3,"file":"opentelemetry.tracing-provider.d.ts","sourceRoot":"","sources":["../../src/providers/opentelemetry.tracing-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC5E,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AA2F7D;;;GAGG;AACH,qBAAa,4BAA6B,YAAW,gBAAgB;IACnE,OAAO,CAAC,GAAG,CAAa;IACxB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;IACrD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAU;IAChD,OAAO,CAAC,WAAW,CAAS;gBAEhB,MAAM,GAAE;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,aAAa,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;QACvC,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACrC,qBAAqB,CAAC,EAAE,OAAO,CAAC;KAC5B;IAqBN;;;OAGG;IACG,UAAU,CAAC,MAAM,CAAC,EAAE;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACrC,qBAAqB,CAAC,EAAE,OAAO,CAAC;QAChC,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqMjB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,GAAE,MAAmB,GAAG,GAAG;IA0BzG;;OAEG;IACG,mBAAmB,CAAC,CAAC,EACzB,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;IAwBb;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAchC;;OAEG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAavD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAajC;;OAEG;IACH,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI;IAazB;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAM/B;;OAEG;IACH,OAAO,CAAC,WAAW;CAiBpB"}
@@ -34,6 +34,85 @@ 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
+ // 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) {
58
+ // Lazy load SpanKind to avoid top-level import failure when @opentelemetry/api is absent
59
+ if (!this.cachedSpanKind) {
60
+ try {
61
+ // @ts-ignore - Optional peer dependency
62
+ this.cachedSpanKind = require('@opentelemetry/api').SpanKind;
63
+ }
64
+ catch {
65
+ // If @opentelemetry/api is not available, skip processing
66
+ return;
67
+ }
68
+ }
69
+ // Only process root spans (SERVER spans without parent)
70
+ if (_span.parent || _span.kind !== this.cachedSpanKind.SERVER) {
71
+ return;
72
+ }
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)) {
78
+ // Format: GET /api/healthcheck/ping
79
+ const fullName = `${method} ${route || target}`;
80
+ _span.updateName(fullName);
81
+ }
82
+ }
83
+ }
84
+ /**
85
+ * Normalize OTLP endpoint URL to ensure consistent format
86
+ * - Ensure port is included
87
+ * - Remove trailing slashes
88
+ * - For gRPC, don't add /v1/traces (it uses different path)
89
+ */
90
+ function normalizeEndpoint(endpoint, isGrpc) {
91
+ try {
92
+ const url = new URL(endpoint);
93
+ // Ensure port is specified
94
+ if (!url.port) {
95
+ const defaultPort = url.protocol === 'https:' ? '443' : '80';
96
+ url.port = defaultPort;
97
+ }
98
+ // Remove trailing slash
99
+ let pathname = url.pathname.replace(/\/+$/, '');
100
+ // For gRPC, remove /v1/traces as it uses different path
101
+ if (isGrpc && pathname.endsWith('/v1/traces')) {
102
+ pathname = pathname.replace('/v1/traces', '');
103
+ }
104
+ // For HTTP/PROTO, ensure /v1/traces path exists
105
+ if (!isGrpc && !pathname.endsWith('/v1/traces')) {
106
+ pathname = pathname + '/v1/traces';
107
+ }
108
+ url.pathname = pathname;
109
+ return url.toString();
110
+ }
111
+ catch (error) {
112
+ // If URL parsing fails, return as-is
113
+ return endpoint;
114
+ }
115
+ }
37
116
  /**
38
117
  * OpenTelemetry Tracing Provider Implementation
39
118
  * Sử dụng OpenTelemetry SDK với OTLP exporter để gửi traces về Elastic APM
@@ -46,6 +125,17 @@ class OpenTelemetryTracingProvider {
46
125
  this.serviceName = config.serviceName || process.env.ELASTIC_APM_SERVICE_NAME || 'interactive-backend';
47
126
  this.environment = config.environment || process.env.ELASTIC_APM_ENVIRONMENT || 'development';
48
127
  this.otlpEndpoint = config.otlpEndpoint || process.env.ELASTIC_OTLP_ENDPOINT || 'http://localhost:8200/v1/traces';
128
+ // 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') {
131
+ this.otlpTransport = otlp_transport_type_1.OtlpTransport.GRPC;
132
+ }
133
+ else if (transportType === otlp_transport_type_1.OtlpTransport.PROTO || transportType === 'proto') {
134
+ this.otlpTransport = otlp_transport_type_1.OtlpTransport.PROTO;
135
+ }
136
+ else {
137
+ this.otlpTransport = otlp_transport_type_1.OtlpTransport.HTTP;
138
+ }
49
139
  this.secretToken = config.secretToken || process.env.ELASTIC_APM_SECRET_TOKEN || '';
50
140
  this.otlpAuthToken = config.otlpAuthToken || process.env.ELASTIC_OTLP_AUTH_TOKEN || '';
51
141
  this.otlpHeaders = config.otlpHeaders || {};
@@ -68,13 +158,28 @@ class OpenTelemetryTracingProvider {
68
158
  // @ts-ignore - Optional peer dependency
69
159
  const { ExpressInstrumentation } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/instrumentation-express')));
70
160
  // @ts-ignore - Optional peer dependency
71
- const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-http')));
72
- // @ts-ignore - Optional peer dependency
73
161
  const { resourceFromAttributes } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/resources')));
74
162
  // @ts-ignore - Optional peer dependency
75
163
  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}`);
164
+ const endpoint = config?.otlpEndpoint || this.otlpEndpoint;
165
+ const isGrpc = this.otlpTransport === otlp_transport_type_1.OtlpTransport.GRPC;
166
+ const isProto = this.otlpTransport === otlp_transport_type_1.OtlpTransport.PROTO;
167
+ // Normalize endpoint to ensure consistent format
168
+ const normalizedEndpoint = normalizeEndpoint(endpoint, isGrpc);
169
+ // Log initialization details when debug mode is on
170
+ (0, debug_logger_1.logInitialization)('OpenTelemetry', {
171
+ serviceName: config?.serviceName || this.serviceName,
172
+ environment: config?.environment || this.environment,
173
+ otlpEndpoint: normalizedEndpoint,
174
+ transport: isGrpc ? 'gRPC' : isProto ? 'PROTO (protobuf over HTTP)' : 'HTTP',
175
+ hasToken: !!(config?.otlpAuthToken || this.otlpAuthToken || config?.secretToken || this.secretToken),
176
+ enableConsoleExporter: config?.enableConsoleExporter ?? this.enableConsoleExporter,
177
+ });
178
+ const transportLabel = isGrpc ? 'gRPC' : isProto ? 'PROTO' : 'HTTP';
179
+ if ((0, debug_logger_1.isDebugEnabled)()) {
180
+ (0, debug_logger_1.infoLog)(`[OpenTelemetry] Service: ${this.serviceName}, Environment: ${this.environment}`);
181
+ (0, debug_logger_1.infoLog)(`[OpenTelemetry] Endpoint: ${normalizedEndpoint} (${transportLabel})`);
182
+ }
78
183
  // OTLP Exporter - Build headers with Authorization
79
184
  const buildHeaders = () => {
80
185
  const headers = {
@@ -87,25 +192,34 @@ class OpenTelemetryTracingProvider {
87
192
  }
88
193
  return Object.keys(headers).length > 0 ? headers : undefined;
89
194
  };
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
- };
195
+ // Import the correct exporter based on transport type
196
+ let otlpExporter;
197
+ if (isGrpc) {
198
+ // @ts-ignore - Optional peer dependency
199
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-grpc')));
200
+ otlpExporter = new OTLPTraceExporter({
201
+ url: normalizedEndpoint,
202
+ headers: buildHeaders(),
203
+ });
204
+ }
205
+ else if (isProto) {
206
+ // @ts-ignore - Optional peer dependency
207
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-proto')));
208
+ otlpExporter = new OTLPTraceExporter({
209
+ url: normalizedEndpoint,
210
+ headers: buildHeaders(),
211
+ });
212
+ }
213
+ else {
214
+ // @ts-ignore - Optional peer dependency
215
+ const { OTLPTraceExporter } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/exporter-trace-otlp-http')));
216
+ otlpExporter = new OTLPTraceExporter({
217
+ url: normalizedEndpoint,
218
+ headers: buildHeaders(),
219
+ });
220
+ }
221
+ // Wrap with debug exporter for detailed request/response logging
222
+ const otlpExporterWithLogging = (0, debug_exporter_wrapper_1.createDebugExporter)(otlpExporter, normalizedEndpoint);
109
223
  // Use console exporter if enabled
110
224
  const shouldEnableConsole = config?.enableConsoleExporter ?? this.enableConsoleExporter;
111
225
  let traceExporter = otlpExporterWithLogging;
@@ -156,22 +270,53 @@ class OpenTelemetryTracingProvider {
156
270
  'service.instance.id': `${process.pid}`,
157
271
  'host.name': require('os').hostname(),
158
272
  });
273
+ // Use BatchSpanProcessor for efficient batching with explicit flush
274
+ const { BatchSpanProcessor } = await Promise.resolve().then(() => __importStar(require('@opentelemetry/sdk-trace-base')));
275
+ // Create a custom processor that extends BatchSpanProcessor behavior
276
+ // but also applies transaction naming
277
+ const transactionNameProcessor = new TransactionNameProcessor();
278
+ const batchProcessor = new BatchSpanProcessor(traceExporter);
279
+ // Wrap the batch processor to also do transaction naming
280
+ const processorWithNaming = {
281
+ onStart: (span, context) => {
282
+ transactionNameProcessor.onStart(span);
283
+ batchProcessor.onStart(span, context);
284
+ },
285
+ onEnd: (span) => {
286
+ transactionNameProcessor.onEnd(span);
287
+ batchProcessor.onEnd(span);
288
+ },
289
+ forceFlush: async () => {
290
+ await batchProcessor.forceFlush();
291
+ },
292
+ shutdown: async () => {
293
+ await batchProcessor.shutdown();
294
+ },
295
+ };
159
296
  // Khởi tạo SDK
160
297
  this.sdk = new NodeSDK({
161
298
  serviceName: config?.serviceName || this.serviceName,
162
- traceExporter,
299
+ traceExporter: undefined, // Don't use traceExporter when using spanProcessor
163
300
  instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()],
164
301
  resource: sdkResource,
302
+ spanProcessor: processorWithNaming,
165
303
  });
166
304
  // Khởi động SDK
167
305
  this.sdk.start();
306
+ // Install HTTP interceptor for debug mode
307
+ const interceptor = (0, debug_exporter_wrapper_1.createHttpRequestInterceptor)();
308
+ if (interceptor) {
309
+ interceptor.install();
310
+ }
168
311
  // Get tracer
169
312
  this.tracer = trace.getTracer(config?.serviceName || this.serviceName);
170
313
  this.initialized = true;
171
- console.log('[OpenTelemetry] Initialized');
314
+ if ((0, debug_logger_1.isDebugEnabled)()) {
315
+ (0, debug_logger_1.infoLog)('[OpenTelemetry] Initialized');
316
+ }
172
317
  }
173
318
  catch (error) {
174
- console.error('[OpenTelemetry] Failed to initialize:', error);
319
+ (0, debug_logger_1.errorLog)('[OpenTelemetry] Failed to initialize:', error);
175
320
  throw error;
176
321
  }
177
322
  }
@@ -180,7 +325,9 @@ class OpenTelemetryTracingProvider {
180
325
  */
181
326
  startSpan(name, attributes, spanKind = 'INTERNAL') {
182
327
  if (!this.initialized || !this.tracer) {
183
- console.warn('[OpenTelemetry] Not initialized');
328
+ if ((0, debug_logger_1.isDebugEnabled)()) {
329
+ console.warn('[OpenTelemetry] Not initialized');
330
+ }
184
331
  return null;
185
332
  }
186
333
  // @ts-ignore - Optional peer dependency
@@ -189,6 +336,8 @@ class OpenTelemetryTracingProvider {
189
336
  attributes,
190
337
  kind: this.mapSpanKind(spanKind),
191
338
  }, context.active());
339
+ // Log span details when debug mode is on
340
+ (0, debug_logger_1.logSpan)('OpenTelemetry', span);
192
341
  return span;
193
342
  }
194
343
  /**
@@ -206,6 +355,9 @@ class OpenTelemetryTracingProvider {
206
355
  catch (error) {
207
356
  span?.recordException(error);
208
357
  span?.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
358
+ if ((0, debug_logger_1.isDebugEnabled)()) {
359
+ console.log(`[APM-DEBUG] [OpenTelemetry] Span failed:`, name, error.message || error);
360
+ }
209
361
  throw error;
210
362
  }
211
363
  finally {
@@ -242,6 +394,22 @@ class OpenTelemetryTracingProvider {
242
394
  span.setAttribute(key, value);
243
395
  }
244
396
  }
397
+ /**
398
+ * Force flush all pending spans
399
+ */
400
+ async forceFlush() {
401
+ if (this.sdk && this.sdk.forceFlush) {
402
+ try {
403
+ await this.sdk.forceFlush();
404
+ if ((0, debug_logger_1.isDebugEnabled)()) {
405
+ console.log('[APM-DEBUG] Force flush completed');
406
+ }
407
+ }
408
+ catch (error) {
409
+ (0, debug_logger_1.errorLog)('[OpenTelemetry] Force flush failed:', error);
410
+ }
411
+ }
412
+ }
245
413
  /**
246
414
  * End span manually
247
415
  */