@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.
- package/README.md +201 -123
- package/dist/factories/tracing-provider.factory.d.ts +8 -3
- package/dist/factories/tracing-provider.factory.d.ts.map +1 -1
- package/dist/factories/tracing-provider.factory.js +17 -13
- package/dist/index.d.ts +12 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/init/elastic-apm-init.d.ts +25 -3
- package/dist/init/elastic-apm-init.d.ts.map +1 -1
- package/dist/init/elastic-apm-init.js +29 -12
- package/dist/init/opentelemetry-init.d.ts +8 -1
- package/dist/init/opentelemetry-init.d.ts.map +1 -1
- package/dist/init/opentelemetry-init.js +145 -44
- package/dist/interfaces/tracing-provider.interface.d.ts +13 -3
- package/dist/interfaces/tracing-provider.interface.d.ts.map +1 -1
- package/dist/modules/tracing.module.d.ts +5 -5
- package/dist/modules/tracing.module.d.ts.map +1 -1
- package/dist/modules/tracing.module.js +2 -1
- package/dist/providers/elastic-apm.tracing-provider.d.ts +23 -5
- package/dist/providers/elastic-apm.tracing-provider.d.ts.map +1 -1
- package/dist/providers/elastic-apm.tracing-provider.js +127 -28
- package/dist/providers/opentelemetry.tracing-provider.d.ts +12 -4
- package/dist/providers/opentelemetry.tracing-provider.d.ts.map +1 -1
- package/dist/providers/opentelemetry.tracing-provider.js +328 -67
- package/dist/services/tracing.service.d.ts +6 -5
- package/dist/services/tracing.service.d.ts.map +1 -1
- package/dist/services/tracing.service.js +2 -2
- package/dist/types/apm.types.d.ts +162 -0
- package/dist/types/apm.types.d.ts.map +1 -0
- package/dist/types/apm.types.js +37 -0
- package/dist/types/otlp-transport.type.d.ts +14 -0
- package/dist/types/otlp-transport.type.d.ts.map +1 -0
- package/dist/types/otlp-transport.type.js +17 -0
- package/dist/utils/debug-exporter-wrapper.d.ts +36 -0
- package/dist/utils/debug-exporter-wrapper.d.ts.map +1 -0
- package/dist/utils/debug-exporter-wrapper.js +247 -0
- package/dist/utils/debug-logger.d.ts +81 -0
- package/dist/utils/debug-logger.d.ts.map +1 -0
- package/dist/utils/debug-logger.js +236 -0
- package/dist/utils/tracing.helper.d.ts +2 -2
- package/dist/utils/tracing.helper.d.ts.map +1 -1
- package/dist/utils/tracing.helper.js +8 -4
- 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 =
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
166
|
+
const { ExpressInstrumentation } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/instrumentation-express")));
|
|
70
167
|
// @ts-ignore - Optional peer dependency
|
|
71
|
-
const {
|
|
168
|
+
const { resourceFromAttributes } = await Promise.resolve().then(() => __importStar(require("@opentelemetry/resources")));
|
|
72
169
|
// @ts-ignore - Optional peer dependency
|
|
73
|
-
const {
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
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[
|
|
201
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
87
202
|
}
|
|
88
203
|
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
89
204
|
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
255
|
+
const exportResult = result;
|
|
256
|
+
if (exportResult.error) {
|
|
130
257
|
hasError = true;
|
|
131
|
-
errors.push(
|
|
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
|
-
|
|
265
|
+
const exportResult = result;
|
|
266
|
+
if (exportResult.error) {
|
|
139
267
|
hasError = true;
|
|
140
|
-
errors.push(
|
|
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([
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
332
|
+
if ((0, debug_logger_1.isDebugEnabled)()) {
|
|
333
|
+
(0, debug_logger_1.infoLog)("[OpenTelemetry] Initialized");
|
|
334
|
+
}
|
|
172
335
|
}
|
|
173
336
|
catch (error) {
|
|
174
|
-
|
|
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 =
|
|
344
|
+
startSpan(name, attributes, spanKind = "INTERNAL") {
|
|
182
345
|
if (!this.initialized || !this.tracer) {
|
|
183
|
-
|
|
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
|
|
188
|
-
const
|
|
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(
|
|
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
|
|
208
|
-
|
|
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(
|
|
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({
|
|
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(
|
|
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(
|
|
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(
|
|
533
|
+
const { SpanKind } = require("@opentelemetry/api");
|
|
273
534
|
switch (kind.toUpperCase()) {
|
|
274
|
-
case
|
|
535
|
+
case "SERVER":
|
|
275
536
|
return SpanKind.SERVER;
|
|
276
|
-
case
|
|
537
|
+
case "CLIENT":
|
|
277
538
|
return SpanKind.CLIENT;
|
|
278
|
-
case
|
|
539
|
+
case "PRODUCER":
|
|
279
540
|
return SpanKind.PRODUCER;
|
|
280
|
-
case
|
|
541
|
+
case "CONSUMER":
|
|
281
542
|
return SpanKind.CONSUMER;
|
|
282
|
-
case
|
|
543
|
+
case "INTERNAL":
|
|
283
544
|
default:
|
|
284
545
|
return SpanKind.INTERNAL;
|
|
285
546
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ITracingProvider } from
|
|
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
|
|
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):
|
|
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:
|
|
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?:
|
|
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;
|
|
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
|
|
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 =
|
|
52
|
+
startSpan(name, attributes, spanKind = "INTERNAL") {
|
|
53
53
|
return this.provider.startSpan(name, attributes, spanKind);
|
|
54
54
|
}
|
|
55
55
|
/**
|