polartrace 2.0.0
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/CHANGELOG.md +113 -0
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/dist/diagnostics.d.ts +20 -0
- package/dist/diagnostics.js +105 -0
- package/dist/index.d.ts +131 -0
- package/dist/index.js +1391 -0
- package/dist/instrumentations/instrumentation-manager.d.ts +12 -0
- package/dist/instrumentations/instrumentation-manager.js +50 -0
- package/dist/instrumentations/instrumentation.d.ts +4 -0
- package/dist/instrumentations/instrumentation.js +2 -0
- package/dist/instrumentations/mongo.instrumentation.d.ts +32 -0
- package/dist/instrumentations/mongo.instrumentation.js +131 -0
- package/dist/instrumentations/postgres.instrumentation.d.ts +29 -0
- package/dist/instrumentations/postgres.instrumentation.js +142 -0
- package/dist/instrumentations/redis.instrumentation.d.ts +31 -0
- package/dist/instrumentations/redis.instrumentation.js +183 -0
- package/dist/metrics/host-metrics-monitor.d.ts +57 -0
- package/dist/metrics/host-metrics-monitor.js +189 -0
- package/dist/module-hook.d.ts +3 -0
- package/dist/module-hook.js +82 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +16 -0
- package/dist/trace/index.d.ts +1 -0
- package/dist/trace/index.js +5 -0
- package/dist/trace/trace-monitor.d.ts +42 -0
- package/dist/trace/trace-monitor.js +376 -0
- package/dist/ts-loader.d.ts +1 -0
- package/dist/ts-loader.js +75 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +12 -0
- package/package.json +118 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PolarTrace = void 0;
|
|
4
|
+
const api_1 = require("@opentelemetry/api");
|
|
5
|
+
const trace_1 = require("./trace");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const diagnostics_1 = require("./diagnostics");
|
|
8
|
+
const module_hook_1 = require("./module-hook");
|
|
9
|
+
const SENSITIVE_FIELDS = [
|
|
10
|
+
"password",
|
|
11
|
+
"token",
|
|
12
|
+
"secret",
|
|
13
|
+
"apikey",
|
|
14
|
+
"api-key",
|
|
15
|
+
"authorization",
|
|
16
|
+
"creditcard",
|
|
17
|
+
"ssn",
|
|
18
|
+
"cookie",
|
|
19
|
+
];
|
|
20
|
+
const version_1 = require("./version");
|
|
21
|
+
const instrumentation_manager_1 = require("./instrumentations/instrumentation-manager");
|
|
22
|
+
const mongo_instrumentation_1 = require("./instrumentations/mongo.instrumentation");
|
|
23
|
+
const redis_instrumentation_1 = require("./instrumentations/redis.instrumentation");
|
|
24
|
+
const postgres_instrumentation_1 = require("./instrumentations/postgres.instrumentation");
|
|
25
|
+
const host_metrics_monitor_1 = require("./metrics/host-metrics-monitor");
|
|
26
|
+
class PolarTrace {
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.connectionStatus = "pending";
|
|
29
|
+
this.statusAnnounced = false;
|
|
30
|
+
this.logQueue = [];
|
|
31
|
+
this.isFlushing = false;
|
|
32
|
+
this.isTraceFlushing = false;
|
|
33
|
+
this.isHostMetricsFlushing = false;
|
|
34
|
+
this.traceQueue = [];
|
|
35
|
+
this.hostMetricsQueue = [];
|
|
36
|
+
this.HOST_METRICS_QUEUE_LIMIT = 1000;
|
|
37
|
+
this.LOG_QUEUE_LIMIT = 5000;
|
|
38
|
+
this.TRACE_QUEUE_LIMIT = 5000;
|
|
39
|
+
this.MAX_BATCH_SIZE = 500;
|
|
40
|
+
this.FLUSH_INTERVAL = 10000;
|
|
41
|
+
this.API_TIMEOUT = 10000;
|
|
42
|
+
this.MAX_BACKOFF_MS = 300000;
|
|
43
|
+
this.logConsecutiveFailures = 0;
|
|
44
|
+
this.logBackoffUntil = 0;
|
|
45
|
+
this.traceConsecutiveFailures = 0;
|
|
46
|
+
this.traceBackoffUntil = 0;
|
|
47
|
+
this.hostMetricsConsecutiveFailures = 0;
|
|
48
|
+
this.hostMetricsBackoffUntil = 0;
|
|
49
|
+
this.traceCache = new Map();
|
|
50
|
+
this.parentSpanTracker = new Map();
|
|
51
|
+
this.traceTimestamps = new Map();
|
|
52
|
+
this.TRACE_CACHE_TIMEOUT = 60000;
|
|
53
|
+
this.spanIdMap = new Map();
|
|
54
|
+
const defaultEndpoint = "https://collector.polartrace.io/api/log";
|
|
55
|
+
this.config = {
|
|
56
|
+
enableConsoleLog: false,
|
|
57
|
+
captureHeaders: true,
|
|
58
|
+
captureBody: true,
|
|
59
|
+
captureQuery: true,
|
|
60
|
+
captureConsoleLogs: true,
|
|
61
|
+
enableMongoSpanCollection: true,
|
|
62
|
+
enableRedisSpanCollection: true,
|
|
63
|
+
enablePostgresSpanCollection: true,
|
|
64
|
+
enableHostMetrics: true,
|
|
65
|
+
endpoint: defaultEndpoint,
|
|
66
|
+
...config,
|
|
67
|
+
};
|
|
68
|
+
diagnostics_1.diagnostics.configure({
|
|
69
|
+
verbose: this.config.enableConsoleLog,
|
|
70
|
+
filePath: this.config.logFile,
|
|
71
|
+
});
|
|
72
|
+
if (!this.config.serviceName) {
|
|
73
|
+
throw new Error("serviceName is required (set POLARTRACE_APP_NAME)");
|
|
74
|
+
}
|
|
75
|
+
this.defaultHeaders = {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
"internal-access-token": this.config.apiKey,
|
|
78
|
+
"service-name": this.config.serviceName,
|
|
79
|
+
};
|
|
80
|
+
this.validateApiKey();
|
|
81
|
+
this.setupFlushTimer();
|
|
82
|
+
this.initializeTraceMonitoringSync();
|
|
83
|
+
this.initializeHostMetricsMonitoring();
|
|
84
|
+
this.validateConnection().catch((err) => {
|
|
85
|
+
});
|
|
86
|
+
setImmediate(() => {
|
|
87
|
+
this.showConnectionStatus();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async postJson(url, body) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timeoutId = setTimeout(() => controller.abort(), this.API_TIMEOUT);
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(url, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: this.defaultHeaders,
|
|
97
|
+
body: JSON.stringify(body),
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
});
|
|
100
|
+
const text = await res.text();
|
|
101
|
+
let data = undefined;
|
|
102
|
+
if (text) {
|
|
103
|
+
try {
|
|
104
|
+
data = JSON.parse(text);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
data = text;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (res.status >= 500) {
|
|
111
|
+
const err = new Error(`API returned status ${res.status}`);
|
|
112
|
+
err.response = { status: res.status, data };
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
let retryAfterSeconds;
|
|
116
|
+
if (res.status === 429) {
|
|
117
|
+
const retryAfterHeader = res.headers.get("retry-after");
|
|
118
|
+
if (retryAfterHeader) {
|
|
119
|
+
const seconds = Number(retryAfterHeader);
|
|
120
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
121
|
+
retryAfterSeconds = seconds;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { status: res.status, data, retryAfterSeconds };
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
clearTimeout(timeoutId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
shouldRetainBatch(status) {
|
|
132
|
+
if (status === undefined) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return status === 429 || status >= 500;
|
|
136
|
+
}
|
|
137
|
+
nextBackoffMs(consecutiveFailures, retryAfterSeconds) {
|
|
138
|
+
const exponential = Math.min(this.FLUSH_INTERVAL * Math.pow(2, consecutiveFailures - 1), this.MAX_BACKOFF_MS);
|
|
139
|
+
return Math.max(exponential, (retryAfterSeconds ?? 0) * 1000);
|
|
140
|
+
}
|
|
141
|
+
getBaseUrl() {
|
|
142
|
+
if (!this.config.endpoint) {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
const endpoint = this.config.endpoint.trim();
|
|
146
|
+
try {
|
|
147
|
+
const url = new URL(endpoint);
|
|
148
|
+
return `${url.protocol}//${url.host}`;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
const match = endpoint.match(/^(https?:\/\/[^\/]+)/);
|
|
152
|
+
if (match) {
|
|
153
|
+
return match[1];
|
|
154
|
+
}
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
validateApiKey() {
|
|
159
|
+
if (!this.config.apiKey) {
|
|
160
|
+
throw new Error("apiKey is required (set POLARTRACE_LICENSE_KEY)");
|
|
161
|
+
}
|
|
162
|
+
if (this.config.apiKey.length < 10) {
|
|
163
|
+
throw new Error("apiKey does not look like a PolarTrace license key (set POLARTRACE_LICENSE_KEY)");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
setupFlushTimer() {
|
|
167
|
+
if (this.config.endpoint) {
|
|
168
|
+
this.flushTimer = setInterval(() => {
|
|
169
|
+
this.flushLogs();
|
|
170
|
+
}, this.FLUSH_INTERVAL);
|
|
171
|
+
this.flushTimer.unref();
|
|
172
|
+
this.traceFlushTimer = setInterval(() => {
|
|
173
|
+
this.flushTraces();
|
|
174
|
+
}, this.FLUSH_INTERVAL);
|
|
175
|
+
this.traceFlushTimer.unref();
|
|
176
|
+
this.hostMetricsFlushTimer = setInterval(() => {
|
|
177
|
+
this.flushHostMetrics();
|
|
178
|
+
}, this.FLUSH_INTERVAL);
|
|
179
|
+
this.hostMetricsFlushTimer.unref();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
initializeHostMetricsMonitoring() {
|
|
183
|
+
if (!this.config.enableHostMetrics) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
this.hostMetricsMonitor = new host_metrics_monitor_1.HostMetricsMonitor({
|
|
188
|
+
serviceName: this.config.serviceName,
|
|
189
|
+
sampleIntervalMs: this.FLUSH_INTERVAL,
|
|
190
|
+
onSample: (sample) => this.queueHostMetricSample(sample),
|
|
191
|
+
onError: (err) => {
|
|
192
|
+
diagnostics_1.diagnostics.debug(`Host metrics sampling error: ${err.message}`);
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
this.hostMetricsMonitor.initialize();
|
|
196
|
+
diagnostics_1.diagnostics.debug("Host metrics monitoring started (CPU/memory/event-loop)");
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
diagnostics_1.diagnostics.warn(`Failed to initialize host metrics monitoring: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
queueHostMetricSample(sample) {
|
|
203
|
+
this.hostMetricsQueue.push({ sample });
|
|
204
|
+
if (this.hostMetricsQueue.length > this.HOST_METRICS_QUEUE_LIMIT) {
|
|
205
|
+
const overflow = this.hostMetricsQueue.length - this.HOST_METRICS_QUEUE_LIMIT;
|
|
206
|
+
this.hostMetricsQueue.splice(0, overflow);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
describeNetworkError(error) {
|
|
210
|
+
const baseUrl = this.getBaseUrl() || this.config.endpoint || "(unknown)";
|
|
211
|
+
const cause = error?.cause;
|
|
212
|
+
const code = cause?.code || cause?.errno;
|
|
213
|
+
const detail = cause?.message || error?.message || "unknown error";
|
|
214
|
+
if (code === "ECONNREFUSED") {
|
|
215
|
+
return `Collector at ${baseUrl} refused connection - is it running?`;
|
|
216
|
+
}
|
|
217
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
218
|
+
return `Cannot resolve collector host ${baseUrl} (${code})`;
|
|
219
|
+
}
|
|
220
|
+
if (code === "ETIMEDOUT") {
|
|
221
|
+
return `Collector at ${baseUrl} timed out`;
|
|
222
|
+
}
|
|
223
|
+
if (code) {
|
|
224
|
+
return `Cannot reach collector at ${baseUrl} (${code})`;
|
|
225
|
+
}
|
|
226
|
+
return `Cannot reach collector at ${baseUrl} - ${detail}`;
|
|
227
|
+
}
|
|
228
|
+
async validateConnection() {
|
|
229
|
+
if (!this.config.endpoint) {
|
|
230
|
+
this.connectionStatus = "failed";
|
|
231
|
+
this.connectionError = "Endpoint not configured";
|
|
232
|
+
this.showConnectionStatus();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const baseUrl = this.getBaseUrl();
|
|
236
|
+
const validationEndpoint = `${baseUrl}/api/service/validate`;
|
|
237
|
+
const backoffsMs = [0, 2000, 4000, 8000, 16000];
|
|
238
|
+
for (let attempt = 0; attempt < backoffsMs.length; attempt++) {
|
|
239
|
+
if (backoffsMs[attempt] > 0) {
|
|
240
|
+
await new Promise((r) => setTimeout(r, backoffsMs[attempt]));
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const response = await this.postJson(validationEndpoint, {});
|
|
244
|
+
if (response.status >= 200 && response.status < 300) {
|
|
245
|
+
this.connectionStatus = "connected";
|
|
246
|
+
this.connectionError = undefined;
|
|
247
|
+
diagnostics_1.diagnostics.debug("Connection validated successfully");
|
|
248
|
+
this.showConnectionStatus();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.connectionStatus = "failed";
|
|
252
|
+
this.connectionError =
|
|
253
|
+
response.data?.message || `API returned status ${response.status}`;
|
|
254
|
+
this.showConnectionStatus();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (error?.response) {
|
|
259
|
+
this.connectionStatus = "failed";
|
|
260
|
+
this.connectionError =
|
|
261
|
+
error.response.data?.message ||
|
|
262
|
+
`API returned status ${error.response.status}`;
|
|
263
|
+
this.showConnectionStatus();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const isLast = attempt === backoffsMs.length - 1;
|
|
267
|
+
const reason = error?.name === "AbortError"
|
|
268
|
+
? `Collector at ${baseUrl} did not respond within ${this.API_TIMEOUT / 1000}s`
|
|
269
|
+
: this.describeNetworkError(error);
|
|
270
|
+
diagnostics_1.diagnostics.debug(`validate attempt ${attempt + 1}/${backoffsMs.length} failed - ${reason}`);
|
|
271
|
+
if (isLast) {
|
|
272
|
+
this.connectionStatus = "failed";
|
|
273
|
+
this.connectionError = reason;
|
|
274
|
+
this.showConnectionStatus();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
showConnectionStatus() {
|
|
281
|
+
if (this.connectionStatus === "pending" || this.statusAnnounced) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const globals = globalThis;
|
|
285
|
+
const components = [
|
|
286
|
+
{
|
|
287
|
+
name: "collector",
|
|
288
|
+
ok: this.connectionStatus === "connected",
|
|
289
|
+
reason: this.connectionError,
|
|
290
|
+
},
|
|
291
|
+
{ name: "http/express tracing", ok: !!this.traceMonitor?.initialized },
|
|
292
|
+
{
|
|
293
|
+
name: "mongodb spans",
|
|
294
|
+
ok: !this.config.enableMongoSpanCollection ||
|
|
295
|
+
!!globals.__POLARTRACE_MONGO_INSTRUMENTATION_ENABLED__,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: "redis spans",
|
|
299
|
+
ok: !this.config.enableRedisSpanCollection ||
|
|
300
|
+
!!globals.__POLARTRACE_REDIS_INSTRUMENTATION_ENABLED__,
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: "postgres spans",
|
|
304
|
+
ok: !this.config.enablePostgresSpanCollection ||
|
|
305
|
+
!!globals.__POLARTRACE_POSTGRES_INSTRUMENTATION_ENABLED__,
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
this.statusAnnounced = true;
|
|
309
|
+
const failed = components.filter((component) => !component.ok);
|
|
310
|
+
if (failed.length === 0) {
|
|
311
|
+
diagnostics_1.diagnostics.info(`agent ready (v${version_1.AGENT_VERSION}, service "${this.config.serviceName}")`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
for (const component of failed) {
|
|
315
|
+
diagnostics_1.diagnostics.error(component.reason
|
|
316
|
+
? `${component.name} unavailable: ${component.reason}`
|
|
317
|
+
: `${component.name} unavailable`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
initializeTraceMonitoringSync() {
|
|
321
|
+
try {
|
|
322
|
+
const agentEndpointPatterns = [];
|
|
323
|
+
if (this.config.endpoint) {
|
|
324
|
+
const baseUrl = this.getBaseUrl();
|
|
325
|
+
agentEndpointPatterns.push("/api/traces", "/api/log", "/api/logs", "/api/host-metrics", "/api/service/validate");
|
|
326
|
+
agentEndpointPatterns.push(`${baseUrl}/api/traces`, `${baseUrl}/api/log`, `${baseUrl}/api/logs`, `${baseUrl}/api/host-metrics`, `${baseUrl}/api/service/validate`);
|
|
327
|
+
}
|
|
328
|
+
this.traceMonitor = new trace_1.TraceMonitor({
|
|
329
|
+
serviceName: this.config.serviceName,
|
|
330
|
+
serviceVersion: version_1.AGENT_VERSION,
|
|
331
|
+
excludeAgentSpans: true,
|
|
332
|
+
agentEndpoints: agentEndpointPatterns.length > 0 ? agentEndpointPatterns : undefined,
|
|
333
|
+
sharedSpanIdMap: this.spanIdMap,
|
|
334
|
+
onSpan: (span) => {
|
|
335
|
+
this.queueTraceSpan(span);
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
this.traceMonitor.initialize().catch((err) => {
|
|
339
|
+
diagnostics_1.diagnostics.error(`Failed to initialize trace monitoring: ${err}`);
|
|
340
|
+
});
|
|
341
|
+
diagnostics_1.diagnostics.debug("Trace monitoring initialization started (SDK will be ready before mongoose loads)");
|
|
342
|
+
const instrumentationManager = instrumentation_manager_1.InstrumentationManager.getInstance();
|
|
343
|
+
if (this.config.enableMongoSpanCollection) {
|
|
344
|
+
const mongoCallback = (mongoSpan) => {
|
|
345
|
+
const traceSpan = this.convertMongoSpanToTraceSpan(mongoSpan);
|
|
346
|
+
this.queueTraceSpan(traceSpan);
|
|
347
|
+
};
|
|
348
|
+
const updated = instrumentationManager.updateMongoCallback(mongoCallback);
|
|
349
|
+
if (!updated) {
|
|
350
|
+
instrumentationManager.register(new mongo_instrumentation_1.MongoInstrumentation(mongoCallback, this.config.mongoose));
|
|
351
|
+
instrumentationManager.enableAll();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (this.config.enableRedisSpanCollection) {
|
|
355
|
+
const redisCallback = (redisSpan) => {
|
|
356
|
+
const traceSpan = this.convertRedisSpanToTraceSpan(redisSpan);
|
|
357
|
+
this.queueTraceSpan(traceSpan);
|
|
358
|
+
};
|
|
359
|
+
const updated = instrumentationManager.updateRedisCallback(redisCallback);
|
|
360
|
+
if (!updated) {
|
|
361
|
+
instrumentationManager.register(new redis_instrumentation_1.RedisInstrumentation(redisCallback));
|
|
362
|
+
instrumentationManager.enableAll();
|
|
363
|
+
}
|
|
364
|
+
globalThis.__POLARTRACE_REDIS_INSTRUMENTATION_ENABLED__ = true;
|
|
365
|
+
}
|
|
366
|
+
if (this.config.enablePostgresSpanCollection) {
|
|
367
|
+
const postgresCallback = (pgSpan) => {
|
|
368
|
+
const traceSpan = this.convertPostgresSpanToTraceSpan(pgSpan);
|
|
369
|
+
this.queueTraceSpan(traceSpan);
|
|
370
|
+
};
|
|
371
|
+
const updated = instrumentationManager.updatePostgresCallback(postgresCallback);
|
|
372
|
+
if (!updated) {
|
|
373
|
+
instrumentationManager.register(new postgres_instrumentation_1.PostgresInstrumentation(postgresCallback));
|
|
374
|
+
instrumentationManager.enableAll();
|
|
375
|
+
}
|
|
376
|
+
globalThis.__POLARTRACE_POSTGRES_INSTRUMENTATION_ENABLED__ = true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
diagnostics_1.diagnostics.error(`Failed to initialize trace monitoring: ${error.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
queueTraceSpan(span) {
|
|
384
|
+
const traceSpanData = {
|
|
385
|
+
...span,
|
|
386
|
+
};
|
|
387
|
+
const traceId = span.traceId;
|
|
388
|
+
const isParentSpan = this.isParentSpan(span);
|
|
389
|
+
if (isParentSpan) {
|
|
390
|
+
const existingParentSpanId = this.parentSpanTracker.get(traceId);
|
|
391
|
+
if (!existingParentSpanId) {
|
|
392
|
+
this.parentSpanTracker.set(traceId, span.spanId);
|
|
393
|
+
if (!this.traceTimestamps.has(traceId)) {
|
|
394
|
+
this.traceTimestamps.set(traceId, Date.now());
|
|
395
|
+
}
|
|
396
|
+
if (!this.traceCache.has(traceId)) {
|
|
397
|
+
this.traceCache.set(traceId, []);
|
|
398
|
+
}
|
|
399
|
+
this.traceCache.get(traceId).push(traceSpanData);
|
|
400
|
+
this.flushTrace(traceId);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (!this.traceCache.has(traceId)) {
|
|
405
|
+
this.traceCache.set(traceId, []);
|
|
406
|
+
this.traceTimestamps.set(traceId, Date.now());
|
|
407
|
+
}
|
|
408
|
+
this.traceCache.get(traceId).push(traceSpanData);
|
|
409
|
+
if (this.traceCache.size % 100 === 0) {
|
|
410
|
+
this.cleanupOldTraces();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
isParentSpan(span) {
|
|
414
|
+
return (span.kind === "SERVER" &&
|
|
415
|
+
!span.parentSpanId &&
|
|
416
|
+
(span.attributes?.["http.method"] !== undefined ||
|
|
417
|
+
span.attributes?.["http.request.method"] !== undefined));
|
|
418
|
+
}
|
|
419
|
+
flushTrace(traceId) {
|
|
420
|
+
const spans = this.traceCache.get(traceId);
|
|
421
|
+
if (!spans || spans.length === 0) {
|
|
422
|
+
this.parentSpanTracker.delete(traceId);
|
|
423
|
+
this.traceTimestamps.delete(traceId);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
for (const span of spans) {
|
|
427
|
+
this.traceQueue.push({ span });
|
|
428
|
+
}
|
|
429
|
+
if (this.traceQueue.length > this.TRACE_QUEUE_LIMIT) {
|
|
430
|
+
const overflow = this.traceQueue.length - this.TRACE_QUEUE_LIMIT;
|
|
431
|
+
this.traceQueue.splice(0, overflow);
|
|
432
|
+
}
|
|
433
|
+
diagnostics_1.diagnostics.debug(`Flushed trace ${traceId.substring(0, 8)}... with ${spans.length} spans (parent completed)`);
|
|
434
|
+
this.traceCache.delete(traceId);
|
|
435
|
+
this.parentSpanTracker.delete(traceId);
|
|
436
|
+
this.traceTimestamps.delete(traceId);
|
|
437
|
+
}
|
|
438
|
+
cleanupOldTraces() {
|
|
439
|
+
const now = Date.now();
|
|
440
|
+
const tracesToCleanup = [];
|
|
441
|
+
for (const [traceId, timestamp] of this.traceTimestamps.entries()) {
|
|
442
|
+
if (now - timestamp > this.TRACE_CACHE_TIMEOUT) {
|
|
443
|
+
tracesToCleanup.push(traceId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
for (const traceId of tracesToCleanup) {
|
|
447
|
+
diagnostics_1.diagnostics.debug(`Cleaning up orphaned trace ${traceId.substring(0, 8)}... (timeout)`);
|
|
448
|
+
this.flushTrace(traceId);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
convertMongoSpanToTraceSpan(mongoSpan) {
|
|
452
|
+
const startMs = mongoSpan.startTime || Date.now();
|
|
453
|
+
const endMs = mongoSpan.endTime ?? Date.now();
|
|
454
|
+
const durationMs = mongoSpan.durationMs ?? endMs - startMs;
|
|
455
|
+
const startTimeNs = startMs * 1000000;
|
|
456
|
+
const endTimeNs = endMs * 1000000;
|
|
457
|
+
const traceId = mongoSpan.traceId || this.generateUUID();
|
|
458
|
+
const parentSpanIdUUID = mongoSpan.parentSpanId
|
|
459
|
+
? this.convertSpanIdToUUID(mongoSpan.parentSpanId)
|
|
460
|
+
: undefined;
|
|
461
|
+
return {
|
|
462
|
+
traceId,
|
|
463
|
+
spanId: this.generateUUID(),
|
|
464
|
+
parentSpanId: parentSpanIdUUID,
|
|
465
|
+
name: mongoSpan.name,
|
|
466
|
+
kind: "CLIENT",
|
|
467
|
+
startTime: startTimeNs,
|
|
468
|
+
endTime: endTimeNs,
|
|
469
|
+
duration: durationMs,
|
|
470
|
+
status: {
|
|
471
|
+
code: mongoSpan.error ? "ERROR" : "OK",
|
|
472
|
+
message: mongoSpan.error?.message,
|
|
473
|
+
},
|
|
474
|
+
attributes: {
|
|
475
|
+
"db.system": "mongodb",
|
|
476
|
+
"db.name": mongoSpan.attributes.db,
|
|
477
|
+
"db.operation": mongoSpan.attributes.command,
|
|
478
|
+
"db.mongodb.collection": mongoSpan.attributes.collection,
|
|
479
|
+
...mongoSpan.attributes,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
convertRedisSpanToTraceSpan(redisSpan) {
|
|
484
|
+
const startMs = redisSpan.startTime || Date.now();
|
|
485
|
+
const endMs = redisSpan.endTime ?? Date.now();
|
|
486
|
+
const durationMs = redisSpan.durationMs ?? endMs - startMs;
|
|
487
|
+
const startTimeNs = startMs * 1000000;
|
|
488
|
+
const endTimeNs = endMs * 1000000;
|
|
489
|
+
const traceId = redisSpan.traceId || this.generateUUID();
|
|
490
|
+
const parentSpanIdUUID = redisSpan.parentSpanId
|
|
491
|
+
? this.convertSpanIdToUUID(redisSpan.parentSpanId)
|
|
492
|
+
: undefined;
|
|
493
|
+
return {
|
|
494
|
+
traceId,
|
|
495
|
+
spanId: this.generateUUID(),
|
|
496
|
+
parentSpanId: parentSpanIdUUID,
|
|
497
|
+
name: redisSpan.name,
|
|
498
|
+
kind: "CLIENT",
|
|
499
|
+
startTime: startTimeNs,
|
|
500
|
+
endTime: endTimeNs,
|
|
501
|
+
duration: durationMs,
|
|
502
|
+
status: {
|
|
503
|
+
code: redisSpan.error ? "ERROR" : "OK",
|
|
504
|
+
message: redisSpan.error?.message,
|
|
505
|
+
},
|
|
506
|
+
attributes: {
|
|
507
|
+
"db.system": "redis",
|
|
508
|
+
"db.operation": redisSpan.attributes.command,
|
|
509
|
+
"db.redis.library": redisSpan.attributes.library,
|
|
510
|
+
"db.statement.key": redisSpan.attributes.key,
|
|
511
|
+
...redisSpan.attributes,
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
convertPostgresSpanToTraceSpan(pgSpan) {
|
|
516
|
+
const startMs = pgSpan.startTime || Date.now();
|
|
517
|
+
const endMs = pgSpan.endTime ?? Date.now();
|
|
518
|
+
const durationMs = pgSpan.durationMs ?? endMs - startMs;
|
|
519
|
+
const traceId = pgSpan.traceId || this.generateUUID();
|
|
520
|
+
const parentSpanIdUUID = pgSpan.parentSpanId
|
|
521
|
+
? this.convertSpanIdToUUID(pgSpan.parentSpanId)
|
|
522
|
+
: undefined;
|
|
523
|
+
return {
|
|
524
|
+
traceId,
|
|
525
|
+
spanId: this.generateUUID(),
|
|
526
|
+
parentSpanId: parentSpanIdUUID,
|
|
527
|
+
name: pgSpan.name,
|
|
528
|
+
kind: "CLIENT",
|
|
529
|
+
startTime: startMs * 1000000,
|
|
530
|
+
endTime: endMs * 1000000,
|
|
531
|
+
duration: durationMs,
|
|
532
|
+
status: {
|
|
533
|
+
code: pgSpan.error ? "ERROR" : "OK",
|
|
534
|
+
message: pgSpan.error?.message,
|
|
535
|
+
},
|
|
536
|
+
attributes: {
|
|
537
|
+
"db.system": "postgresql",
|
|
538
|
+
"db.operation": pgSpan.attributes.operation,
|
|
539
|
+
"db.sql.table": pgSpan.attributes.table,
|
|
540
|
+
"db.statement": pgSpan.attributes.statement,
|
|
541
|
+
...pgSpan.attributes,
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
convertSpanIdToUUID(spanId) {
|
|
546
|
+
if (!spanId)
|
|
547
|
+
return spanId;
|
|
548
|
+
if (spanId.length === 32) {
|
|
549
|
+
return spanId;
|
|
550
|
+
}
|
|
551
|
+
if (this.spanIdMap.has(spanId)) {
|
|
552
|
+
return this.spanIdMap.get(spanId);
|
|
553
|
+
}
|
|
554
|
+
const uuidString = (0, crypto_1.randomUUID)().replace(/-/g, "");
|
|
555
|
+
this.spanIdMap.set(spanId, uuidString);
|
|
556
|
+
if (this.spanIdMap.size > 10000) {
|
|
557
|
+
const entries = Array.from(this.spanIdMap.entries());
|
|
558
|
+
this.spanIdMap.clear();
|
|
559
|
+
entries.slice(-5000).forEach(([key, value]) => {
|
|
560
|
+
this.spanIdMap.set(key, value);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return uuidString;
|
|
564
|
+
}
|
|
565
|
+
generateUUID() {
|
|
566
|
+
return (0, crypto_1.randomUUID)().replace(/-/g, "");
|
|
567
|
+
}
|
|
568
|
+
getActiveTraceId() {
|
|
569
|
+
try {
|
|
570
|
+
const span = api_1.trace.getActiveSpan();
|
|
571
|
+
const id = span?.spanContext().traceId;
|
|
572
|
+
if (!id || /^0+$/.test(id))
|
|
573
|
+
return undefined;
|
|
574
|
+
return id;
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
middleware() {
|
|
581
|
+
return (req, res, next) => {
|
|
582
|
+
const startTimeMs = Date.now();
|
|
583
|
+
const startTime = new Date(startTimeMs).toISOString();
|
|
584
|
+
const capturedLogs = [];
|
|
585
|
+
this.generateRequestId();
|
|
586
|
+
const traceId = this.getActiveTraceId();
|
|
587
|
+
const requestLog = {
|
|
588
|
+
timestamp: startTime,
|
|
589
|
+
method: req.method,
|
|
590
|
+
path: req.path,
|
|
591
|
+
userAgent: req.get("user-agent"),
|
|
592
|
+
ip: this.getClientIp(req),
|
|
593
|
+
host: req.get("host"),
|
|
594
|
+
startTime: startTime,
|
|
595
|
+
...(traceId ? { traceId } : {}),
|
|
596
|
+
};
|
|
597
|
+
req.__POLARTRACE_REQUEST_LOG__ = requestLog;
|
|
598
|
+
if (this.config.captureHeaders) {
|
|
599
|
+
requestLog.headers = this.redactSensitive(req.headers);
|
|
600
|
+
}
|
|
601
|
+
if (this.config.captureQuery && Object.keys(req.query).length > 0) {
|
|
602
|
+
requestLog.query = req.query;
|
|
603
|
+
}
|
|
604
|
+
let consoleIntercepted = false;
|
|
605
|
+
if (this.config.captureConsoleLogs) {
|
|
606
|
+
this.interceptConsole(capturedLogs);
|
|
607
|
+
consoleIntercepted = true;
|
|
608
|
+
}
|
|
609
|
+
const errorHandler = (err) => {
|
|
610
|
+
requestLog.error = {
|
|
611
|
+
message: err.message,
|
|
612
|
+
stack: err.stack,
|
|
613
|
+
name: err.name,
|
|
614
|
+
};
|
|
615
|
+
};
|
|
616
|
+
const originalSend = res.send;
|
|
617
|
+
const originalJson = res.json;
|
|
618
|
+
let isLogged = false;
|
|
619
|
+
res.send = (data) => {
|
|
620
|
+
if (!isLogged) {
|
|
621
|
+
isLogged = true;
|
|
622
|
+
return this.handleResponse(req, res, originalSend, data, startTimeMs, requestLog, capturedLogs, consoleIntercepted);
|
|
623
|
+
}
|
|
624
|
+
return originalSend.call(res, data);
|
|
625
|
+
};
|
|
626
|
+
res.json = (data) => {
|
|
627
|
+
if (!isLogged) {
|
|
628
|
+
isLogged = true;
|
|
629
|
+
return this.handleResponse(req, res, originalJson, data, startTimeMs, requestLog, capturedLogs, consoleIntercepted);
|
|
630
|
+
}
|
|
631
|
+
return originalJson.call(res, data);
|
|
632
|
+
};
|
|
633
|
+
req.on("error", errorHandler);
|
|
634
|
+
res.on("error", errorHandler);
|
|
635
|
+
res.on("finish", () => {
|
|
636
|
+
if (!isLogged) {
|
|
637
|
+
isLogged = true;
|
|
638
|
+
this.handleResponse(req, res, () => res, undefined, startTimeMs, requestLog, capturedLogs, consoleIntercepted);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
next();
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
koaMiddleware() {
|
|
645
|
+
return async (ctx, next) => {
|
|
646
|
+
const startTimeMs = Date.now();
|
|
647
|
+
const startTime = new Date(startTimeMs).toISOString();
|
|
648
|
+
const capturedLogs = [];
|
|
649
|
+
const traceId = this.getActiveTraceId();
|
|
650
|
+
const requestLog = {
|
|
651
|
+
timestamp: startTime,
|
|
652
|
+
method: ctx.method,
|
|
653
|
+
path: ctx.path,
|
|
654
|
+
userAgent: ctx.get("user-agent"),
|
|
655
|
+
ip: ctx.ip,
|
|
656
|
+
host: ctx.host,
|
|
657
|
+
startTime,
|
|
658
|
+
...(traceId ? { traceId } : {}),
|
|
659
|
+
};
|
|
660
|
+
if (this.config.captureHeaders) {
|
|
661
|
+
requestLog.headers = this.redactSensitive(ctx.headers);
|
|
662
|
+
}
|
|
663
|
+
if (this.config.captureQuery &&
|
|
664
|
+
ctx.query &&
|
|
665
|
+
Object.keys(ctx.query).length > 0) {
|
|
666
|
+
requestLog.query = ctx.query;
|
|
667
|
+
}
|
|
668
|
+
let consoleIntercepted = false;
|
|
669
|
+
if (this.config.captureConsoleLogs) {
|
|
670
|
+
this.interceptConsole(capturedLogs);
|
|
671
|
+
consoleIntercepted = true;
|
|
672
|
+
}
|
|
673
|
+
let thrown;
|
|
674
|
+
try {
|
|
675
|
+
await next();
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
thrown = err;
|
|
679
|
+
requestLog.error = {
|
|
680
|
+
message: String(err?.message ?? err),
|
|
681
|
+
stack: err?.stack,
|
|
682
|
+
name: err?.name,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
finally {
|
|
686
|
+
const endTimeMs = Date.now();
|
|
687
|
+
requestLog.statusCode = thrown
|
|
688
|
+
? thrown.status || thrown.statusCode || 500
|
|
689
|
+
: ctx.status;
|
|
690
|
+
requestLog.duration = endTimeMs - startTimeMs;
|
|
691
|
+
requestLog.endTime = new Date(endTimeMs).toISOString();
|
|
692
|
+
if (this.config.captureBody && ctx.request?.body) {
|
|
693
|
+
requestLog.body = this.redactSensitive(ctx.request.body);
|
|
694
|
+
}
|
|
695
|
+
if (consoleIntercepted) {
|
|
696
|
+
this.restoreConsole();
|
|
697
|
+
if (capturedLogs.length > 0) {
|
|
698
|
+
requestLog.consoleLogs = capturedLogs;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
this.logRequest(requestLog);
|
|
702
|
+
}
|
|
703
|
+
if (thrown)
|
|
704
|
+
throw thrown;
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
instrumentFastify(app) {
|
|
708
|
+
if (!app || typeof app.addHook !== "function")
|
|
709
|
+
return;
|
|
710
|
+
if (app.__POLARTRACE_MIDDLEWARE_ATTACHED__)
|
|
711
|
+
return;
|
|
712
|
+
app.__POLARTRACE_MIDDLEWARE_ATTACHED__ = true;
|
|
713
|
+
const states = new WeakMap();
|
|
714
|
+
app.addHook("onRequest", async (request) => {
|
|
715
|
+
const state = {
|
|
716
|
+
startTimeMs: Date.now(),
|
|
717
|
+
traceId: this.getActiveTraceId(),
|
|
718
|
+
capturedLogs: [],
|
|
719
|
+
consoleIntercepted: false,
|
|
720
|
+
};
|
|
721
|
+
if (this.config.captureConsoleLogs) {
|
|
722
|
+
this.interceptConsole(state.capturedLogs);
|
|
723
|
+
state.consoleIntercepted = true;
|
|
724
|
+
}
|
|
725
|
+
states.set(request, state);
|
|
726
|
+
request.__POLARTRACE_FASTIFY_STATE__ = state;
|
|
727
|
+
});
|
|
728
|
+
app.addHook("onError", async (request, _reply, error) => {
|
|
729
|
+
const state = states.get(request);
|
|
730
|
+
if (state)
|
|
731
|
+
state.error = error;
|
|
732
|
+
});
|
|
733
|
+
app.addHook("onResponse", async (request, reply) => {
|
|
734
|
+
const state = states.get(request);
|
|
735
|
+
if (!state)
|
|
736
|
+
return;
|
|
737
|
+
states.delete(request);
|
|
738
|
+
const endTimeMs = Date.now();
|
|
739
|
+
const startTime = new Date(state.startTimeMs).toISOString();
|
|
740
|
+
const requestLog = {
|
|
741
|
+
timestamp: startTime,
|
|
742
|
+
method: request.method,
|
|
743
|
+
path: String(request.raw?.url || request.url || "").split("?")[0],
|
|
744
|
+
statusCode: reply.statusCode,
|
|
745
|
+
duration: endTimeMs - state.startTimeMs,
|
|
746
|
+
startTime,
|
|
747
|
+
endTime: new Date(endTimeMs).toISOString(),
|
|
748
|
+
userAgent: request.headers?.["user-agent"],
|
|
749
|
+
ip: request.ip,
|
|
750
|
+
host: request.headers?.host,
|
|
751
|
+
...(state.traceId ? { traceId: state.traceId } : {}),
|
|
752
|
+
};
|
|
753
|
+
if (this.config.captureHeaders) {
|
|
754
|
+
requestLog.headers = this.redactSensitive(request.headers);
|
|
755
|
+
}
|
|
756
|
+
if (this.config.captureQuery &&
|
|
757
|
+
request.query &&
|
|
758
|
+
Object.keys(request.query).length > 0) {
|
|
759
|
+
requestLog.query = request.query;
|
|
760
|
+
}
|
|
761
|
+
if (this.config.captureBody && request.body) {
|
|
762
|
+
requestLog.body = this.redactSensitive(request.body);
|
|
763
|
+
}
|
|
764
|
+
if (state.consoleIntercepted) {
|
|
765
|
+
this.restoreConsole();
|
|
766
|
+
if (state.capturedLogs.length > 0) {
|
|
767
|
+
requestLog.consoleLogs = state.capturedLogs;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (state.error) {
|
|
771
|
+
requestLog.error = {
|
|
772
|
+
message: String(state.error?.message ?? state.error),
|
|
773
|
+
stack: state.error?.stack,
|
|
774
|
+
name: state.error?.name,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
this.logRequest(requestLog);
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
handleResponse(req, res, originalMethod, data, startTimeMs, requestLog, capturedLogs, consoleIntercepted) {
|
|
781
|
+
const endTimeMs = Date.now();
|
|
782
|
+
const duration = endTimeMs - startTimeMs;
|
|
783
|
+
const endTime = new Date(endTimeMs).toISOString();
|
|
784
|
+
requestLog.statusCode = res.statusCode;
|
|
785
|
+
requestLog.duration = duration;
|
|
786
|
+
requestLog.endTime = endTime;
|
|
787
|
+
if (this.config.captureBody && req?.body) {
|
|
788
|
+
requestLog.body = this.redactSensitive(req.body);
|
|
789
|
+
}
|
|
790
|
+
if (consoleIntercepted) {
|
|
791
|
+
this.restoreConsole();
|
|
792
|
+
if (capturedLogs.length > 0) {
|
|
793
|
+
requestLog.consoleLogs = capturedLogs;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
this.logRequest(requestLog);
|
|
797
|
+
return originalMethod.call(res, data);
|
|
798
|
+
}
|
|
799
|
+
interceptConsole(capturedLogs) {
|
|
800
|
+
if (!this.originalConsoleLog) {
|
|
801
|
+
this.originalConsoleLog = console.log;
|
|
802
|
+
this.originalConsoleError = console.error;
|
|
803
|
+
this.originalConsoleWarn = console.warn;
|
|
804
|
+
this.originalConsoleInfo = console.info;
|
|
805
|
+
}
|
|
806
|
+
console.log = (...args) => {
|
|
807
|
+
capturedLogs.push({
|
|
808
|
+
timestamp: new Date().toISOString(),
|
|
809
|
+
level: "log",
|
|
810
|
+
args: args,
|
|
811
|
+
});
|
|
812
|
+
this.originalConsoleLog.apply(console, args);
|
|
813
|
+
};
|
|
814
|
+
console.error = (...args) => {
|
|
815
|
+
capturedLogs.push({
|
|
816
|
+
timestamp: new Date().toISOString(),
|
|
817
|
+
level: "error",
|
|
818
|
+
args: args,
|
|
819
|
+
});
|
|
820
|
+
this.originalConsoleError.apply(console, args);
|
|
821
|
+
};
|
|
822
|
+
console.warn = (...args) => {
|
|
823
|
+
capturedLogs.push({
|
|
824
|
+
timestamp: new Date().toISOString(),
|
|
825
|
+
level: "warn",
|
|
826
|
+
args: args,
|
|
827
|
+
});
|
|
828
|
+
this.originalConsoleWarn.apply(console, args);
|
|
829
|
+
};
|
|
830
|
+
console.info = (...args) => {
|
|
831
|
+
capturedLogs.push({
|
|
832
|
+
timestamp: new Date().toISOString(),
|
|
833
|
+
level: "info",
|
|
834
|
+
args: args,
|
|
835
|
+
});
|
|
836
|
+
this.originalConsoleInfo.apply(console, args);
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
restoreConsole() {
|
|
840
|
+
if (this.originalConsoleLog) {
|
|
841
|
+
console.log = this.originalConsoleLog;
|
|
842
|
+
console.error = this.originalConsoleError;
|
|
843
|
+
console.warn = this.originalConsoleWarn;
|
|
844
|
+
console.info = this.originalConsoleInfo;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
redactSensitive(body) {
|
|
848
|
+
if (typeof body !== "object" || body === null) {
|
|
849
|
+
return body;
|
|
850
|
+
}
|
|
851
|
+
const sensitiveFields = SENSITIVE_FIELDS;
|
|
852
|
+
const sanitized = Array.isArray(body) ? [...body] : { ...body };
|
|
853
|
+
for (const key in sanitized) {
|
|
854
|
+
if (sensitiveFields.some((field) => key.toLowerCase().includes(field.toLowerCase()))) {
|
|
855
|
+
sanitized[key] = "[REDACTED]";
|
|
856
|
+
}
|
|
857
|
+
else if (typeof sanitized[key] === "object" &&
|
|
858
|
+
sanitized[key] !== null) {
|
|
859
|
+
sanitized[key] = this.redactSensitive(sanitized[key]);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return sanitized;
|
|
863
|
+
}
|
|
864
|
+
getClientIp(req) {
|
|
865
|
+
return (req.headers["x-forwarded-for"]?.split(",")[0] ||
|
|
866
|
+
req.headers["x-real-ip"] ||
|
|
867
|
+
req.socket.remoteAddress ||
|
|
868
|
+
req.ip ||
|
|
869
|
+
"unknown");
|
|
870
|
+
}
|
|
871
|
+
generateRequestId() {
|
|
872
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
873
|
+
}
|
|
874
|
+
logRequest(log) {
|
|
875
|
+
this.queueLog(log);
|
|
876
|
+
}
|
|
877
|
+
queueLog(log) {
|
|
878
|
+
this.logQueue.push({ log });
|
|
879
|
+
if (this.logQueue.length > this.LOG_QUEUE_LIMIT) {
|
|
880
|
+
const overflow = this.logQueue.length - this.LOG_QUEUE_LIMIT;
|
|
881
|
+
this.logQueue.splice(0, overflow);
|
|
882
|
+
}
|
|
883
|
+
diagnostics_1.diagnostics.debug(`Log queued. Queue size: ${this.logQueue.length}`);
|
|
884
|
+
}
|
|
885
|
+
async flushLogs() {
|
|
886
|
+
if (this.isFlushing || this.logQueue.length === 0) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (Date.now() < this.logBackoffUntil) {
|
|
890
|
+
diagnostics_1.diagnostics.debug(`Log flush skipped - backing off until ${new Date(this.logBackoffUntil).toISOString()}`);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
this.isFlushing = true;
|
|
894
|
+
const batchLength = Math.min(this.logQueue.length, this.MAX_BATCH_SIZE);
|
|
895
|
+
const logsToSend = this.logQueue
|
|
896
|
+
.slice(0, batchLength)
|
|
897
|
+
.map((item) => item.log);
|
|
898
|
+
diagnostics_1.diagnostics.debug(`Attempting to send ${logsToSend.length} logs...`);
|
|
899
|
+
try {
|
|
900
|
+
await this.sendLogsToAPI(logsToSend);
|
|
901
|
+
this.logQueue.splice(0, batchLength);
|
|
902
|
+
this.logConsecutiveFailures = 0;
|
|
903
|
+
this.logBackoffUntil = 0;
|
|
904
|
+
diagnostics_1.diagnostics.debug(`Successfully sent ${logsToSend.length} logs and removed from queue`);
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
const status = error?.response?.status;
|
|
908
|
+
if (this.shouldRetainBatch(status)) {
|
|
909
|
+
this.logConsecutiveFailures++;
|
|
910
|
+
this.logBackoffUntil =
|
|
911
|
+
Date.now() +
|
|
912
|
+
this.nextBackoffMs(this.logConsecutiveFailures, error?.response?.retryAfterSeconds);
|
|
913
|
+
diagnostics_1.diagnostics.debug(`Failed to send logs to API: ${error?.message}`);
|
|
914
|
+
diagnostics_1.diagnostics.debug(`Keeping ${batchLength} logs in queue for retry (failure #${this.logConsecutiveFailures})`);
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
this.logQueue.splice(0, batchLength);
|
|
918
|
+
this.logConsecutiveFailures = 0;
|
|
919
|
+
this.logBackoffUntil = 0;
|
|
920
|
+
diagnostics_1.diagnostics.warnOnce(`drop:logs:${status}`, `dropped ${batchLength} logs rejected by collector (status ${status})`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
finally {
|
|
924
|
+
this.isFlushing = false;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async sendLogsToAPI(logs) {
|
|
928
|
+
if (!this.config.endpoint) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
const response = await this.postJson(this.config.endpoint, { logs });
|
|
933
|
+
if (response.status >= 400) {
|
|
934
|
+
const err = new Error(`API returned status ${response.status}`);
|
|
935
|
+
err.response = response;
|
|
936
|
+
throw err;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
if (error?.response) {
|
|
941
|
+
diagnostics_1.diagnostics.debug(`collector responded with error: ${error.response.status} ${JSON.stringify(error.response.data, null, 2)}`);
|
|
942
|
+
}
|
|
943
|
+
else if (error?.name === "AbortError") {
|
|
944
|
+
diagnostics_1.diagnostics.debug("no response received from collector");
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
diagnostics_1.diagnostics.debug(`failed to send logs: ${error?.message}`);
|
|
948
|
+
}
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async flushTraces() {
|
|
953
|
+
this.cleanupOldTraces();
|
|
954
|
+
diagnostics_1.diagnostics.debug(`flushTraces called - queue size: ${this.traceQueue.length}, isFlushing: ${this.isTraceFlushing}`);
|
|
955
|
+
if (this.isTraceFlushing || this.traceQueue.length === 0) {
|
|
956
|
+
if (this.isTraceFlushing) {
|
|
957
|
+
diagnostics_1.diagnostics.debug("Trace flush skipped - already flushing");
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
diagnostics_1.diagnostics.debug("Trace flush skipped - queue is empty");
|
|
961
|
+
}
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (Date.now() < this.traceBackoffUntil) {
|
|
965
|
+
diagnostics_1.diagnostics.debug(`Trace flush skipped - backing off until ${new Date(this.traceBackoffUntil).toISOString()}`);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
this.isTraceFlushing = true;
|
|
969
|
+
const batchLength = Math.min(this.traceQueue.length, this.MAX_BATCH_SIZE);
|
|
970
|
+
const spansToSend = this.traceQueue
|
|
971
|
+
.slice(0, batchLength)
|
|
972
|
+
.map((item) => item.span);
|
|
973
|
+
diagnostics_1.diagnostics.debug(`Attempting to send ${spansToSend.length} trace spans...`);
|
|
974
|
+
diagnostics_1.diagnostics.debug(`Current queue size before send: ${this.traceQueue.length}`);
|
|
975
|
+
try {
|
|
976
|
+
const response = await this.sendTracesToAPI(spansToSend);
|
|
977
|
+
const status = response?.status;
|
|
978
|
+
if (response && status >= 200 && status < 300) {
|
|
979
|
+
const removed = this.traceQueue.splice(0, batchLength);
|
|
980
|
+
const removedCount = removed.length;
|
|
981
|
+
this.traceConsecutiveFailures = 0;
|
|
982
|
+
this.traceBackoffUntil = 0;
|
|
983
|
+
diagnostics_1.diagnostics.debug(`Successfully sent ${spansToSend.length} trace spans (status: ${status})`);
|
|
984
|
+
diagnostics_1.diagnostics.debug(`Removed ${removedCount} spans from queue`);
|
|
985
|
+
diagnostics_1.diagnostics.debug(`Remaining queue size: ${this.traceQueue.length}`);
|
|
986
|
+
if (removedCount !== batchLength) {
|
|
987
|
+
diagnostics_1.diagnostics.debug(`Expected to remove ${batchLength} spans but removed ${removedCount}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
else if (this.shouldRetainBatch(status)) {
|
|
991
|
+
this.traceConsecutiveFailures++;
|
|
992
|
+
this.traceBackoffUntil =
|
|
993
|
+
Date.now() +
|
|
994
|
+
this.nextBackoffMs(this.traceConsecutiveFailures, response?.retryAfterSeconds);
|
|
995
|
+
diagnostics_1.diagnostics.debug(`API returned status ${status}, keeping ${batchLength} spans in queue for retry (failure #${this.traceConsecutiveFailures})`);
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
this.traceQueue.splice(0, batchLength);
|
|
999
|
+
this.traceConsecutiveFailures = 0;
|
|
1000
|
+
this.traceBackoffUntil = 0;
|
|
1001
|
+
diagnostics_1.diagnostics.warnOnce(`drop:traces:${status}`, `dropped ${batchLength} trace spans rejected by collector (status ${status})`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
this.traceConsecutiveFailures++;
|
|
1006
|
+
this.traceBackoffUntil =
|
|
1007
|
+
Date.now() + this.nextBackoffMs(this.traceConsecutiveFailures);
|
|
1008
|
+
diagnostics_1.diagnostics.debug(`Failed to send trace spans to API: ${error?.message}`);
|
|
1009
|
+
diagnostics_1.diagnostics.debug(`Keeping ${batchLength} trace spans in queue for retry (failure #${this.traceConsecutiveFailures})`);
|
|
1010
|
+
diagnostics_1.diagnostics.debug(`Current queue size: ${this.traceQueue.length}`);
|
|
1011
|
+
}
|
|
1012
|
+
finally {
|
|
1013
|
+
this.isTraceFlushing = false;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
async sendTracesToAPI(spans) {
|
|
1017
|
+
if (!this.config.endpoint) {
|
|
1018
|
+
throw new Error("Endpoint not configured");
|
|
1019
|
+
}
|
|
1020
|
+
let tracesEndpoint;
|
|
1021
|
+
const endpoint = this.config.endpoint.trim();
|
|
1022
|
+
if (endpoint.endsWith("/log")) {
|
|
1023
|
+
tracesEndpoint = endpoint.replace("/log", "/traces");
|
|
1024
|
+
}
|
|
1025
|
+
else if (endpoint.includes("/api/log")) {
|
|
1026
|
+
tracesEndpoint = endpoint.replace("/api/log", "/api/traces");
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
const baseUrl = this.getBaseUrl();
|
|
1030
|
+
tracesEndpoint = `${baseUrl}/traces`;
|
|
1031
|
+
}
|
|
1032
|
+
tracesEndpoint = tracesEndpoint.replace(/([^:]\/)\/+/g, "$1");
|
|
1033
|
+
diagnostics_1.diagnostics.debug(`Sending traces to endpoint: ${tracesEndpoint}`);
|
|
1034
|
+
try {
|
|
1035
|
+
const response = await this.postJson(tracesEndpoint, { spans });
|
|
1036
|
+
diagnostics_1.diagnostics.debug(`API response received - status: ${response.status}`);
|
|
1037
|
+
return response;
|
|
1038
|
+
}
|
|
1039
|
+
catch (error) {
|
|
1040
|
+
diagnostics_1.diagnostics.debug(`Failed to send trace spans: ${error?.message}`);
|
|
1041
|
+
if (error?.response) {
|
|
1042
|
+
diagnostics_1.diagnostics.debug(`Response status: ${error.response.status}`);
|
|
1043
|
+
diagnostics_1.diagnostics.debug(`Response data: ${JSON.stringify(error.response.data)}`);
|
|
1044
|
+
}
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
async flushHostMetrics() {
|
|
1049
|
+
if (this.isHostMetricsFlushing || this.hostMetricsQueue.length === 0) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (Date.now() < this.hostMetricsBackoffUntil) {
|
|
1053
|
+
diagnostics_1.diagnostics.debug(`Host metrics flush skipped - backing off until ${new Date(this.hostMetricsBackoffUntil).toISOString()}`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
this.isHostMetricsFlushing = true;
|
|
1057
|
+
const batchLength = Math.min(this.hostMetricsQueue.length, this.MAX_BATCH_SIZE);
|
|
1058
|
+
const samplesToSend = this.hostMetricsQueue
|
|
1059
|
+
.slice(0, batchLength)
|
|
1060
|
+
.map((item) => item.sample);
|
|
1061
|
+
try {
|
|
1062
|
+
const response = await this.sendHostMetricsToAPI(samplesToSend);
|
|
1063
|
+
const status = response?.status;
|
|
1064
|
+
if (response && status >= 200 && status < 300) {
|
|
1065
|
+
this.hostMetricsQueue.splice(0, batchLength);
|
|
1066
|
+
this.hostMetricsConsecutiveFailures = 0;
|
|
1067
|
+
this.hostMetricsBackoffUntil = 0;
|
|
1068
|
+
diagnostics_1.diagnostics.debug(`Successfully sent ${samplesToSend.length} host metric samples`);
|
|
1069
|
+
}
|
|
1070
|
+
else if (this.shouldRetainBatch(status)) {
|
|
1071
|
+
this.hostMetricsConsecutiveFailures++;
|
|
1072
|
+
this.hostMetricsBackoffUntil =
|
|
1073
|
+
Date.now() +
|
|
1074
|
+
this.nextBackoffMs(this.hostMetricsConsecutiveFailures, response?.retryAfterSeconds);
|
|
1075
|
+
diagnostics_1.diagnostics.debug(`API returned status ${status}, keeping ${batchLength} host metric samples in queue for retry (failure #${this.hostMetricsConsecutiveFailures})`);
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
this.hostMetricsQueue.splice(0, batchLength);
|
|
1079
|
+
this.hostMetricsConsecutiveFailures = 0;
|
|
1080
|
+
this.hostMetricsBackoffUntil = 0;
|
|
1081
|
+
diagnostics_1.diagnostics.warnOnce(`drop:host-metrics:${status}`, `dropped ${batchLength} host metric samples rejected by collector (status ${status})`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
this.hostMetricsConsecutiveFailures++;
|
|
1086
|
+
this.hostMetricsBackoffUntil =
|
|
1087
|
+
Date.now() + this.nextBackoffMs(this.hostMetricsConsecutiveFailures);
|
|
1088
|
+
diagnostics_1.diagnostics.debug(`Failed to send host metrics: ${error?.message}`);
|
|
1089
|
+
diagnostics_1.diagnostics.debug(`Keeping ${batchLength} host metric samples in queue for retry (failure #${this.hostMetricsConsecutiveFailures})`);
|
|
1090
|
+
}
|
|
1091
|
+
finally {
|
|
1092
|
+
this.isHostMetricsFlushing = false;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
async sendHostMetricsToAPI(samples) {
|
|
1096
|
+
if (!this.config.endpoint) {
|
|
1097
|
+
throw new Error("Endpoint not configured");
|
|
1098
|
+
}
|
|
1099
|
+
const baseUrl = this.getBaseUrl();
|
|
1100
|
+
const hostMetricsEndpoint = `${baseUrl}/api/host-metrics`.replace(/([^:]\/)\/+/g, "$1");
|
|
1101
|
+
return this.postJson(hostMetricsEndpoint, { samples });
|
|
1102
|
+
}
|
|
1103
|
+
async shutdown(timeoutMs = 3000) {
|
|
1104
|
+
if (this.flushTimer)
|
|
1105
|
+
clearInterval(this.flushTimer);
|
|
1106
|
+
if (this.traceFlushTimer)
|
|
1107
|
+
clearInterval(this.traceFlushTimer);
|
|
1108
|
+
if (this.hostMetricsFlushTimer)
|
|
1109
|
+
clearInterval(this.hostMetricsFlushTimer);
|
|
1110
|
+
for (const traceId of this.traceCache.keys()) {
|
|
1111
|
+
this.flushTrace(traceId);
|
|
1112
|
+
}
|
|
1113
|
+
const pending = [];
|
|
1114
|
+
if (this.logQueue.length > 0)
|
|
1115
|
+
pending.push(this.flushLogs());
|
|
1116
|
+
if (this.traceQueue.length > 0)
|
|
1117
|
+
pending.push(this.flushTraces());
|
|
1118
|
+
if (this.hostMetricsQueue.length > 0)
|
|
1119
|
+
pending.push(this.flushHostMetrics());
|
|
1120
|
+
if (pending.length > 0) {
|
|
1121
|
+
const settled = Promise.allSettled(pending);
|
|
1122
|
+
const deadline = new Promise((resolve) => {
|
|
1123
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
1124
|
+
if (typeof timer.unref === "function")
|
|
1125
|
+
timer.unref();
|
|
1126
|
+
});
|
|
1127
|
+
await Promise.race([settled, deadline]);
|
|
1128
|
+
}
|
|
1129
|
+
if (this.traceMonitor)
|
|
1130
|
+
this.traceMonitor.destroy();
|
|
1131
|
+
if (this.hostMetricsMonitor)
|
|
1132
|
+
this.hostMetricsMonitor.destroy();
|
|
1133
|
+
diagnostics_1.diagnostics.close();
|
|
1134
|
+
}
|
|
1135
|
+
destroy() {
|
|
1136
|
+
if (this.flushTimer) {
|
|
1137
|
+
clearInterval(this.flushTimer);
|
|
1138
|
+
}
|
|
1139
|
+
if (this.traceFlushTimer) {
|
|
1140
|
+
clearInterval(this.traceFlushTimer);
|
|
1141
|
+
}
|
|
1142
|
+
if (this.hostMetricsFlushTimer) {
|
|
1143
|
+
clearInterval(this.hostMetricsFlushTimer);
|
|
1144
|
+
}
|
|
1145
|
+
for (const traceId of this.traceCache.keys()) {
|
|
1146
|
+
this.flushTrace(traceId);
|
|
1147
|
+
}
|
|
1148
|
+
if (this.logQueue.length > 0) {
|
|
1149
|
+
this.flushLogs().catch((err) => {
|
|
1150
|
+
const errorMsg = `Error flushing logs on destroy: ${err}`;
|
|
1151
|
+
diagnostics_1.diagnostics.debug(errorMsg);
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
if (this.traceQueue.length > 0) {
|
|
1155
|
+
this.flushTraces().catch((err) => {
|
|
1156
|
+
const errorMsg = `Error flushing traces on destroy: ${err}`;
|
|
1157
|
+
diagnostics_1.diagnostics.debug(errorMsg);
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
if (this.hostMetricsQueue.length > 0) {
|
|
1161
|
+
this.flushHostMetrics().catch((err) => {
|
|
1162
|
+
const errorMsg = `Error flushing host metrics on destroy: ${err}`;
|
|
1163
|
+
diagnostics_1.diagnostics.debug(errorMsg);
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
if (this.traceMonitor) {
|
|
1167
|
+
this.traceMonitor.destroy();
|
|
1168
|
+
}
|
|
1169
|
+
if (this.hostMetricsMonitor) {
|
|
1170
|
+
this.hostMetricsMonitor.destroy();
|
|
1171
|
+
}
|
|
1172
|
+
diagnostics_1.diagnostics.close();
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
exports.PolarTrace = PolarTrace;
|
|
1176
|
+
function envFlag(name, fallback) {
|
|
1177
|
+
const raw = process.env[name]?.trim().toLowerCase();
|
|
1178
|
+
if (raw === undefined || raw === "") {
|
|
1179
|
+
return fallback;
|
|
1180
|
+
}
|
|
1181
|
+
return raw === "1" || raw === "true";
|
|
1182
|
+
}
|
|
1183
|
+
function autoInitFromEnv() {
|
|
1184
|
+
const serviceName = process.env.POLARTRACE_APP_NAME;
|
|
1185
|
+
const apiKey = process.env.POLARTRACE_LICENSE_KEY;
|
|
1186
|
+
if (!serviceName || !apiKey) {
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const globalObj = globalThis;
|
|
1190
|
+
if (globalObj.__POLARTRACE_AGENT__) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const endpoint = process.env.POLARTRACE_ENDPOINT?.trim() || undefined;
|
|
1194
|
+
const logFile = process.env.POLARTRACE_LOG_FILE?.trim() || undefined;
|
|
1195
|
+
let agent;
|
|
1196
|
+
try {
|
|
1197
|
+
agent = new PolarTrace({
|
|
1198
|
+
apiKey,
|
|
1199
|
+
serviceName,
|
|
1200
|
+
enableConsoleLog: envFlag("POLARTRACE_ENABLE_CONSOLE_LOG", false),
|
|
1201
|
+
enableHostMetrics: !envFlag("POLARTRACE_DISABLE_HOST_METRICS", false),
|
|
1202
|
+
enableMongoSpanCollection: !envFlag("POLARTRACE_DISABLE_MONGO_SPANS", false),
|
|
1203
|
+
enableRedisSpanCollection: !envFlag("POLARTRACE_DISABLE_REDIS_SPANS", false),
|
|
1204
|
+
enablePostgresSpanCollection: !envFlag("POLARTRACE_DISABLE_POSTGRES_SPANS", false),
|
|
1205
|
+
...(endpoint ? { endpoint } : {}),
|
|
1206
|
+
...(logFile ? { logFile } : {}),
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
diagnostics_1.diagnostics.configure({
|
|
1211
|
+
verbose: envFlag("POLARTRACE_ENABLE_CONSOLE_LOG", false),
|
|
1212
|
+
filePath: logFile,
|
|
1213
|
+
});
|
|
1214
|
+
diagnostics_1.diagnostics.error(`agent disabled: ${error?.message ?? error}`);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
globalObj.__POLARTRACE_AGENT__ = agent;
|
|
1218
|
+
installShutdownHooks(agent);
|
|
1219
|
+
autoAttachExpressMiddleware(agent);
|
|
1220
|
+
autoAttachFastifyHook(agent);
|
|
1221
|
+
autoAttachKoaHook(agent);
|
|
1222
|
+
autoAttachNestErrorHook();
|
|
1223
|
+
}
|
|
1224
|
+
function installShutdownHooks(agent) {
|
|
1225
|
+
const globalObj = globalThis;
|
|
1226
|
+
if (globalObj.__POLARTRACE_SHUTDOWN_HOOKS__)
|
|
1227
|
+
return;
|
|
1228
|
+
globalObj.__POLARTRACE_SHUTDOWN_HOOKS__ = true;
|
|
1229
|
+
let finished = null;
|
|
1230
|
+
const flushOnce = () => {
|
|
1231
|
+
if (!finished)
|
|
1232
|
+
finished = agent.shutdown().catch(() => undefined);
|
|
1233
|
+
return finished;
|
|
1234
|
+
};
|
|
1235
|
+
process.on("beforeExit", () => {
|
|
1236
|
+
void flushOnce();
|
|
1237
|
+
});
|
|
1238
|
+
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
1239
|
+
process.on(signal, () => {
|
|
1240
|
+
const otherListeners = process.listenerCount(signal) - 1;
|
|
1241
|
+
void flushOnce().then(() => {
|
|
1242
|
+
if (otherListeners <= 0) {
|
|
1243
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
function autoAttachExpressMiddleware(agent) {
|
|
1250
|
+
(0, module_hook_1.onModuleLoad)("express", (module) => {
|
|
1251
|
+
if (typeof module !== "function")
|
|
1252
|
+
return;
|
|
1253
|
+
const originalExpress = module;
|
|
1254
|
+
const patchedExpress = function () {
|
|
1255
|
+
const app = originalExpress.apply(this, arguments);
|
|
1256
|
+
if (!app.__POLARTRACE_MIDDLEWARE_ATTACHED__) {
|
|
1257
|
+
app.__POLARTRACE_MIDDLEWARE_ATTACHED__ = true;
|
|
1258
|
+
try {
|
|
1259
|
+
app.use(agent.middleware());
|
|
1260
|
+
}
|
|
1261
|
+
catch {
|
|
1262
|
+
}
|
|
1263
|
+
try {
|
|
1264
|
+
const originalListen = app.listen;
|
|
1265
|
+
app.listen = function (...args) {
|
|
1266
|
+
if (!app.__POLARTRACE_ERROR_MW_ATTACHED__) {
|
|
1267
|
+
app.__POLARTRACE_ERROR_MW_ATTACHED__ = true;
|
|
1268
|
+
app.use((err, req, _res, next) => {
|
|
1269
|
+
const log = req?.__POLARTRACE_REQUEST_LOG__;
|
|
1270
|
+
if (err && log && !log.error) {
|
|
1271
|
+
log.error = {
|
|
1272
|
+
message: String(err?.message ?? err),
|
|
1273
|
+
stack: err?.stack,
|
|
1274
|
+
name: err?.name,
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
next(err);
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
return originalListen.apply(this, args);
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
catch {
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return app;
|
|
1287
|
+
};
|
|
1288
|
+
Object.setPrototypeOf(patchedExpress, originalExpress);
|
|
1289
|
+
Object.keys(originalExpress).forEach((key) => {
|
|
1290
|
+
patchedExpress[key] = originalExpress[key];
|
|
1291
|
+
});
|
|
1292
|
+
return patchedExpress;
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
function autoAttachFastifyHook(agent) {
|
|
1296
|
+
(0, module_hook_1.onModuleLoad)("fastify", (module) => {
|
|
1297
|
+
if (typeof module !== "function")
|
|
1298
|
+
return;
|
|
1299
|
+
const originalFactory = module;
|
|
1300
|
+
if (!originalFactory.__POLARTRACE_PATCHED_FACTORY__) {
|
|
1301
|
+
const patchedFactory = function (...args) {
|
|
1302
|
+
const app = originalFactory.apply(this, args);
|
|
1303
|
+
try {
|
|
1304
|
+
agent.instrumentFastify(app);
|
|
1305
|
+
}
|
|
1306
|
+
catch {
|
|
1307
|
+
}
|
|
1308
|
+
return app;
|
|
1309
|
+
};
|
|
1310
|
+
Object.setPrototypeOf(patchedFactory, originalFactory);
|
|
1311
|
+
Object.keys(originalFactory).forEach((key) => {
|
|
1312
|
+
patchedFactory[key] = originalFactory[key];
|
|
1313
|
+
});
|
|
1314
|
+
patchedFactory.fastify = patchedFactory;
|
|
1315
|
+
patchedFactory.default = patchedFactory;
|
|
1316
|
+
originalFactory.__POLARTRACE_PATCHED_FACTORY__ = patchedFactory;
|
|
1317
|
+
}
|
|
1318
|
+
return originalFactory.__POLARTRACE_PATCHED_FACTORY__;
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
function autoAttachKoaHook(agent) {
|
|
1322
|
+
(0, module_hook_1.onModuleLoad)("koa", (module) => {
|
|
1323
|
+
if (typeof module !== "function" ||
|
|
1324
|
+
!module.prototype ||
|
|
1325
|
+
typeof module.prototype.callback !== "function" ||
|
|
1326
|
+
module.__POLARTRACE_PATCHED__) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
module.__POLARTRACE_PATCHED__ = true;
|
|
1330
|
+
const originalCallback = module.prototype.callback;
|
|
1331
|
+
module.prototype.callback = function (...args) {
|
|
1332
|
+
if (!this.__POLARTRACE_MIDDLEWARE_ATTACHED__) {
|
|
1333
|
+
this.__POLARTRACE_MIDDLEWARE_ATTACHED__ = true;
|
|
1334
|
+
try {
|
|
1335
|
+
this.middleware.unshift(agent.koaMiddleware());
|
|
1336
|
+
}
|
|
1337
|
+
catch {
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return originalCallback.apply(this, args);
|
|
1341
|
+
};
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
function recordNestException(exception, host) {
|
|
1345
|
+
if (!exception || !host || typeof host.switchToHttp !== "function")
|
|
1346
|
+
return;
|
|
1347
|
+
const req = host.switchToHttp()?.getRequest?.();
|
|
1348
|
+
if (!req)
|
|
1349
|
+
return;
|
|
1350
|
+
const status = typeof exception.getStatus === "function" ? exception.getStatus() : undefined;
|
|
1351
|
+
if (typeof status === "number" && status < 500)
|
|
1352
|
+
return;
|
|
1353
|
+
const error = {
|
|
1354
|
+
message: String(exception?.message ?? exception),
|
|
1355
|
+
stack: exception?.stack,
|
|
1356
|
+
name: exception?.name,
|
|
1357
|
+
};
|
|
1358
|
+
const log = req.__POLARTRACE_REQUEST_LOG__ ??
|
|
1359
|
+
req.raw?.__POLARTRACE_REQUEST_LOG__;
|
|
1360
|
+
if (log) {
|
|
1361
|
+
if (!log.error)
|
|
1362
|
+
log.error = error;
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const state = req.__POLARTRACE_FASTIFY_STATE__ ??
|
|
1366
|
+
req.raw?.__POLARTRACE_FASTIFY_STATE__;
|
|
1367
|
+
if (state && !state.error)
|
|
1368
|
+
state.error = error;
|
|
1369
|
+
}
|
|
1370
|
+
function autoAttachNestErrorHook() {
|
|
1371
|
+
(0, module_hook_1.onModuleLoad)("@nestjs/core", (module) => {
|
|
1372
|
+
const FilterClass = module?.BaseExceptionFilter;
|
|
1373
|
+
if (!FilterClass?.prototype ||
|
|
1374
|
+
typeof FilterClass.prototype.catch !== "function" ||
|
|
1375
|
+
FilterClass.__POLARTRACE_PATCHED__) {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
FilterClass.__POLARTRACE_PATCHED__ = true;
|
|
1379
|
+
const originalCatch = FilterClass.prototype.catch;
|
|
1380
|
+
FilterClass.prototype.catch = function (exception, host) {
|
|
1381
|
+
try {
|
|
1382
|
+
recordNestException(exception, host);
|
|
1383
|
+
}
|
|
1384
|
+
catch {
|
|
1385
|
+
}
|
|
1386
|
+
return originalCatch.call(this, exception, host);
|
|
1387
|
+
};
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
require("./register");
|
|
1391
|
+
autoInitFromEnv();
|