autotel-devtools 8.1.0 → 9.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/dist/cli.cjs +108 -1409
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +109 -1402
- package/dist/cli.js.map +1 -1
- package/dist/error-aggregator-BvNmgn7E.d.ts +120 -0
- package/dist/error-aggregator-nnfbpSR7.d.cts +120 -0
- package/dist/exporter-1Y3GmLVS.d.cts +182 -0
- package/dist/exporter-CZ5HdD3o.d.ts +182 -0
- package/dist/genai/index.cjs +650 -537
- package/dist/genai/index.cjs.map +1 -1
- package/dist/genai/index.d.cts +164 -157
- package/dist/genai/index.d.ts +164 -157
- package/dist/genai/index.js +649 -536
- package/dist/genai/index.js.map +1 -1
- package/dist/http-BkkKa9C_.js +1128 -0
- package/dist/http-BkkKa9C_.js.map +1 -0
- package/dist/http-Yj6iSrMX.cjs +1275 -0
- package/dist/http-Yj6iSrMX.cjs.map +1 -0
- package/dist/index.cjs +50 -1708
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -23
- package/dist/index.d.ts +21 -23
- package/dist/index.js +44 -1696
- package/dist/index.js.map +1 -1
- package/dist/listen-BBsxO0wm.cjs +125 -0
- package/dist/listen-BBsxO0wm.cjs.map +1 -0
- package/dist/listen-DfOCquUq.js +120 -0
- package/dist/listen-DfOCquUq.js.map +1 -0
- package/dist/resource-utils-B4UVvfnH.js +18 -0
- package/dist/resource-utils-B4UVvfnH.js.map +1 -0
- package/dist/resource-utils-DjHJB6uc.cjs +24 -0
- package/dist/resource-utils-DjHJB6uc.cjs.map +1 -0
- package/dist/server/exporter.cjs +135 -159
- package/dist/server/exporter.cjs.map +1 -1
- package/dist/server/exporter.d.cts +2 -4
- package/dist/server/exporter.d.ts +2 -4
- package/dist/server/exporter.js +134 -158
- package/dist/server/exporter.js.map +1 -1
- package/dist/server/index.cjs +29 -1640
- package/dist/server/index.d.cts +34 -31
- package/dist/server/index.d.ts +34 -31
- package/dist/server/index.js +5 -1610
- package/dist/server/log-exporter.cjs +75 -102
- package/dist/server/log-exporter.cjs.map +1 -1
- package/dist/server/log-exporter.d.cts +27 -46
- package/dist/server/log-exporter.d.ts +27 -46
- package/dist/server/log-exporter.js +74 -100
- package/dist/server/log-exporter.js.map +1 -1
- package/dist/server/remote-exporter.cjs +171 -213
- package/dist/server/remote-exporter.cjs.map +1 -1
- package/dist/server/remote-exporter.d.cts +62 -82
- package/dist/server/remote-exporter.d.ts +62 -82
- package/dist/server/remote-exporter.js +170 -212
- package/dist/server/remote-exporter.js.map +1 -1
- package/dist/widget.global.js +10 -10
- package/package.json +5 -5
- package/dist/error-aggregator-D0Uu5r38.d.ts +0 -147
- package/dist/error-aggregator-D1Mr221Y.d.cts +0 -147
- package/dist/exporter-De6p4iAD.d.cts +0 -182
- package/dist/exporter-De6p4iAD.d.ts +0 -182
- package/dist/server/index.cjs.map +0 -1
- package/dist/server/index.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -1,1423 +1,122 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
12
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
-
|
|
14
|
-
var protobuf__default = /*#__PURE__*/_interopDefault(protobuf);
|
|
15
|
-
|
|
16
|
-
// src/server/error-aggregator.ts
|
|
17
|
-
var ErrorAggregator = class {
|
|
18
|
-
errorGroups = /* @__PURE__ */ new Map();
|
|
19
|
-
options;
|
|
20
|
-
constructor(options = {}) {
|
|
21
|
-
this.options = {
|
|
22
|
-
maxGroups: options.maxGroups ?? 100,
|
|
23
|
-
maxAffectedTraces: options.maxAffectedTraces ?? 10,
|
|
24
|
-
maxAffectedSpans: options.maxAffectedSpans ?? 5,
|
|
25
|
-
stackFramesForFingerprint: options.stackFramesForFingerprint ?? 5
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Add an error occurrence to the aggregator
|
|
30
|
-
*/
|
|
31
|
-
addError(occurrence) {
|
|
32
|
-
const fingerprint = this.generateFingerprint(occurrence);
|
|
33
|
-
const existing = this.errorGroups.get(fingerprint);
|
|
34
|
-
if (existing) {
|
|
35
|
-
existing.count++;
|
|
36
|
-
existing.lastSeen = occurrence.timestamp;
|
|
37
|
-
if (!existing.affectedTraces.includes(occurrence.traceId)) {
|
|
38
|
-
existing.affectedTraces.push(occurrence.traceId);
|
|
39
|
-
if (existing.affectedTraces.length > this.options.maxAffectedTraces) {
|
|
40
|
-
existing.affectedTraces.shift();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (!existing.affectedSpans.includes(occurrence.spanName)) {
|
|
44
|
-
existing.affectedSpans.push(occurrence.spanName);
|
|
45
|
-
if (existing.affectedSpans.length > this.options.maxAffectedSpans) {
|
|
46
|
-
existing.affectedSpans.shift();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return existing;
|
|
50
|
-
}
|
|
51
|
-
const newGroup = {
|
|
52
|
-
fingerprint,
|
|
53
|
-
type: occurrence.error.type,
|
|
54
|
-
message: occurrence.error.message,
|
|
55
|
-
stackTrace: this.normalizeStackTrace(occurrence.error.stackTrace),
|
|
56
|
-
count: 1,
|
|
57
|
-
firstSeen: occurrence.timestamp,
|
|
58
|
-
lastSeen: occurrence.timestamp,
|
|
59
|
-
affectedTraces: [occurrence.traceId],
|
|
60
|
-
affectedSpans: [occurrence.spanName],
|
|
61
|
-
service: occurrence.service,
|
|
62
|
-
attributes: occurrence.attributes
|
|
63
|
-
};
|
|
64
|
-
if (this.errorGroups.size >= this.options.maxGroups) {
|
|
65
|
-
this.evictOldestGroup();
|
|
66
|
-
}
|
|
67
|
-
this.errorGroups.set(fingerprint, newGroup);
|
|
68
|
-
return newGroup;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Extract errors from a trace and add them to the aggregator
|
|
72
|
-
*/
|
|
73
|
-
addErrorsFromTrace(trace) {
|
|
74
|
-
const addedGroups = [];
|
|
75
|
-
for (const span of trace.spans) {
|
|
76
|
-
if (span.status.code === "ERROR") {
|
|
77
|
-
const occurrence = this.extractErrorFromSpan(span, trace);
|
|
78
|
-
if (occurrence) {
|
|
79
|
-
const group = this.addError(occurrence);
|
|
80
|
-
addedGroups.push(group);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return addedGroups;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Extract error occurrence from a span
|
|
88
|
-
*/
|
|
89
|
-
extractErrorFromSpan(span, trace) {
|
|
90
|
-
const exceptionEvent = span.events?.find((e) => e.name === "exception");
|
|
91
|
-
const errorType = span.attributes["exception.type"] || span.attributes["error.type"] || exceptionEvent?.attributes?.["exception.type"] || "Error";
|
|
92
|
-
const errorMessage = span.status.message || span.attributes["exception.message"] || span.attributes["error.message"] || "Unknown error";
|
|
93
|
-
const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || this.extractStackFromEvents(span);
|
|
94
|
-
return {
|
|
95
|
-
traceId: trace.traceId,
|
|
96
|
-
spanId: span.spanId,
|
|
97
|
-
spanName: span.name,
|
|
98
|
-
service: trace.service,
|
|
99
|
-
timestamp: span.endTime,
|
|
100
|
-
error: {
|
|
101
|
-
type: errorType,
|
|
102
|
-
message: errorMessage,
|
|
103
|
-
stackTrace
|
|
104
|
-
},
|
|
105
|
-
attributes: this.extractRelevantAttributes(span.attributes)
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Extract stack trace from span events (exception events)
|
|
110
|
-
*/
|
|
111
|
-
extractStackFromEvents(span) {
|
|
112
|
-
if (!span.events) return void 0;
|
|
113
|
-
const exceptionEvent = span.events.find((e) => e.name === "exception");
|
|
114
|
-
if (exceptionEvent?.attributes) {
|
|
115
|
-
return exceptionEvent.attributes["exception.stacktrace"] || exceptionEvent.attributes["exception.stack"];
|
|
116
|
-
}
|
|
117
|
-
return void 0;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Extract relevant attributes for error context
|
|
121
|
-
*/
|
|
122
|
-
extractRelevantAttributes(attributes) {
|
|
123
|
-
const relevant = {};
|
|
124
|
-
const keepKeys = [
|
|
125
|
-
"http.method",
|
|
126
|
-
"http.url",
|
|
127
|
-
"http.route",
|
|
128
|
-
"http.status_code",
|
|
129
|
-
"db.system",
|
|
130
|
-
"db.operation",
|
|
131
|
-
"rpc.method",
|
|
132
|
-
"rpc.service",
|
|
133
|
-
"code.function",
|
|
134
|
-
"code.filepath",
|
|
135
|
-
"user.id",
|
|
136
|
-
"operation.name"
|
|
137
|
-
];
|
|
138
|
-
for (const key of keepKeys) {
|
|
139
|
-
if (key in attributes) {
|
|
140
|
-
relevant[key] = attributes[key];
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return relevant;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Generate a fingerprint for error grouping
|
|
147
|
-
*
|
|
148
|
-
* Uses error type + first N stack frames (normalized)
|
|
149
|
-
*/
|
|
150
|
-
generateFingerprint(occurrence) {
|
|
151
|
-
const parts = [occurrence.error.type];
|
|
152
|
-
if (occurrence.error.stackTrace) {
|
|
153
|
-
const frames = this.extractStackFrames(
|
|
154
|
-
occurrence.error.stackTrace,
|
|
155
|
-
this.options.stackFramesForFingerprint
|
|
156
|
-
);
|
|
157
|
-
parts.push(...frames);
|
|
158
|
-
} else {
|
|
159
|
-
parts.push(this.normalizeMessage(occurrence.error.message));
|
|
160
|
-
}
|
|
161
|
-
return this.simpleHash(parts.join("|"));
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Extract and normalize stack frames from a stack trace
|
|
165
|
-
*/
|
|
166
|
-
extractStackFrames(stackTrace, count) {
|
|
167
|
-
const lines = stackTrace.split("\n");
|
|
168
|
-
const frames = [];
|
|
169
|
-
for (const line of lines) {
|
|
170
|
-
if (frames.length >= count) break;
|
|
171
|
-
const trimmed = line.trim();
|
|
172
|
-
const nodeMatch = trimmed.match(/^at\s+(.+?)\s+\((.+?):(\d+):\d+\)$/);
|
|
173
|
-
if (nodeMatch) {
|
|
174
|
-
frames.push(`${nodeMatch[1]}@${this.normalizeFilePath(nodeMatch[2])}`);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const anonMatch = trimmed.match(/^at\s+(.+?):(\d+):\d+$/);
|
|
178
|
-
if (anonMatch) {
|
|
179
|
-
frames.push(`anonymous@${this.normalizeFilePath(anonMatch[1])}`);
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
const browserMatch = trimmed.match(/^(.+?)@(.+?):(\d+):\d+$/);
|
|
183
|
-
if (browserMatch) {
|
|
184
|
-
frames.push(
|
|
185
|
-
`${browserMatch[1]}@${this.normalizeFilePath(browserMatch[2])}`
|
|
186
|
-
);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return frames;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Normalize file path by removing absolute path prefixes and node_modules paths
|
|
194
|
-
*/
|
|
195
|
-
normalizeFilePath(filePath) {
|
|
196
|
-
const nodeModulesMatch = filePath.match(
|
|
197
|
-
/node_modules\/(@[^/]+\/[^/]+|[^/]+)/
|
|
198
|
-
);
|
|
199
|
-
if (nodeModulesMatch) {
|
|
200
|
-
return `[npm]/${nodeModulesMatch[1]}`;
|
|
201
|
-
}
|
|
202
|
-
return filePath.replace(/^.*?\/src\//, "src/").replace(/^.*?\/dist\//, "dist/").replace(/^.*?\/lib\//, "lib/").replace(/^file:\/\//, "");
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Normalize error message by removing dynamic parts
|
|
206
|
-
*/
|
|
207
|
-
normalizeMessage(message) {
|
|
208
|
-
return message.replaceAll(
|
|
209
|
-
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
|
|
210
|
-
"[UUID]"
|
|
211
|
-
).replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\b\d+\b/g, "[N]").replaceAll(/"[^"]*"/g, '"[STR]"').replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200);
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Normalize stack trace for display
|
|
215
|
-
*/
|
|
216
|
-
normalizeStackTrace(stackTrace) {
|
|
217
|
-
if (!stackTrace) return void 0;
|
|
218
|
-
const lines = stackTrace.split("\n").slice(0, 10);
|
|
219
|
-
return lines.join("\n");
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Simple hash function for fingerprinting
|
|
223
|
-
*/
|
|
224
|
-
simpleHash(str) {
|
|
225
|
-
let hash = 0;
|
|
226
|
-
for (let i = 0; i < str.length; i++) {
|
|
227
|
-
const char = str.charCodeAt(i);
|
|
228
|
-
hash = (hash << 5) - hash + char;
|
|
229
|
-
hash = hash & hash;
|
|
230
|
-
}
|
|
231
|
-
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Evict the oldest error group
|
|
235
|
-
*/
|
|
236
|
-
evictOldestGroup() {
|
|
237
|
-
let oldest = null;
|
|
238
|
-
for (const [fingerprint, group] of this.errorGroups) {
|
|
239
|
-
if (!oldest || group.lastSeen < oldest.lastSeen) {
|
|
240
|
-
oldest = { fingerprint, lastSeen: group.lastSeen };
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (oldest) {
|
|
244
|
-
this.errorGroups.delete(oldest.fingerprint);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Get all error groups, sorted by most recent
|
|
249
|
-
*/
|
|
250
|
-
getErrorGroups() {
|
|
251
|
-
return [...this.errorGroups.values()].sort(
|
|
252
|
-
(a, b) => b.lastSeen - a.lastSeen
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Get error groups sorted by count (most frequent first)
|
|
257
|
-
*/
|
|
258
|
-
getErrorGroupsByFrequency() {
|
|
259
|
-
return [...this.errorGroups.values()].sort(
|
|
260
|
-
(a, b) => b.count - a.count
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Get a specific error group by fingerprint
|
|
265
|
-
*/
|
|
266
|
-
getErrorGroup(fingerprint) {
|
|
267
|
-
return this.errorGroups.get(fingerprint);
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Get error groups for a specific service
|
|
271
|
-
*/
|
|
272
|
-
getErrorGroupsByService(service) {
|
|
273
|
-
return this.getErrorGroups().filter((g) => g.service === service);
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Get total error count across all groups
|
|
277
|
-
*/
|
|
278
|
-
getTotalErrorCount() {
|
|
279
|
-
let total = 0;
|
|
280
|
-
for (const group of this.errorGroups.values()) {
|
|
281
|
-
total += group.count;
|
|
282
|
-
}
|
|
283
|
-
return total;
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Get error statistics
|
|
287
|
-
*/
|
|
288
|
-
getStats() {
|
|
289
|
-
const now = Date.now();
|
|
290
|
-
const oneHourAgo = now - 60 * 60 * 1e3;
|
|
291
|
-
let recentErrors = 0;
|
|
292
|
-
const typeCount = /* @__PURE__ */ new Map();
|
|
293
|
-
for (const group of this.errorGroups.values()) {
|
|
294
|
-
if (group.lastSeen > oneHourAgo) {
|
|
295
|
-
recentErrors += group.count;
|
|
296
|
-
}
|
|
297
|
-
typeCount.set(group.type, (typeCount.get(group.type) || 0) + group.count);
|
|
298
|
-
}
|
|
299
|
-
const topErrorTypes = [...typeCount.entries()].map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count).slice(0, 5);
|
|
300
|
-
return {
|
|
301
|
-
totalGroups: this.errorGroups.size,
|
|
302
|
-
totalErrors: this.getTotalErrorCount(),
|
|
303
|
-
recentErrors,
|
|
304
|
-
topErrorTypes
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Clear all error groups
|
|
309
|
-
*/
|
|
310
|
-
clear() {
|
|
311
|
-
this.errorGroups.clear();
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Clear old error groups (not seen in given time window)
|
|
315
|
-
*/
|
|
316
|
-
clearOlderThan(maxAgeMs) {
|
|
317
|
-
const cutoff = Date.now() - maxAgeMs;
|
|
318
|
-
let cleared = 0;
|
|
319
|
-
for (const [fingerprint, group] of this.errorGroups) {
|
|
320
|
-
if (group.lastSeen < cutoff) {
|
|
321
|
-
this.errorGroups.delete(fingerprint);
|
|
322
|
-
cleared++;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
return cleared;
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// src/server/telemetry-limits.ts
|
|
330
|
-
var defaultLimit = 100;
|
|
331
|
-
function parseLimit(value) {
|
|
332
|
-
if (!value) return void 0;
|
|
333
|
-
const parsed = Number.parseInt(value, 10);
|
|
334
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
335
|
-
}
|
|
336
|
-
function resolveTelemetryLimits(args = {}) {
|
|
337
|
-
const env = args.env ?? process.env;
|
|
338
|
-
const fallback = args.maxHistory ?? defaultLimit;
|
|
339
|
-
return {
|
|
340
|
-
maxTraceCount: args.maxTraceCount ?? parseLimit(env.AUTOTEL_MAX_TRACE_COUNT) ?? fallback,
|
|
341
|
-
maxLogCount: args.maxLogCount ?? parseLimit(env.AUTOTEL_MAX_LOG_COUNT) ?? fallback,
|
|
342
|
-
maxMetricCount: args.maxMetricCount ?? parseLimit(env.AUTOTEL_MAX_METRIC_COUNT) ?? fallback
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
function appendWithLimit(items, item, limit) {
|
|
346
|
-
if (limit <= 0) return [];
|
|
347
|
-
const next = [...items, item];
|
|
348
|
-
return next.length > limit ? next.slice(next.length - limit) : next;
|
|
349
|
-
}
|
|
350
|
-
function appendManyWithLimit(items, incoming, limit) {
|
|
351
|
-
if (limit <= 0 || incoming.length === 0) return limit <= 0 ? [] : items;
|
|
352
|
-
const next = [...items, ...incoming];
|
|
353
|
-
return next.length > limit ? next.slice(next.length - limit) : next;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// src/server/origin-guard.ts
|
|
357
|
-
var LOOPBACK_IPV6 = /* @__PURE__ */ new Set(["::1", "0:0:0:0:0:0:0:1"]);
|
|
358
|
-
function isLoopbackHostname(hostname) {
|
|
359
|
-
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
360
|
-
return h === "localhost" || /^127\./.test(h) || LOOPBACK_IPV6.has(h);
|
|
361
|
-
}
|
|
362
|
-
function hostnameFromHostHeader(host) {
|
|
363
|
-
const h = host.trim();
|
|
364
|
-
if (h.startsWith("[")) {
|
|
365
|
-
const end = h.indexOf("]");
|
|
366
|
-
return end > 0 ? h.slice(1, end) : h;
|
|
367
|
-
}
|
|
368
|
-
const colon = h.indexOf(":");
|
|
369
|
-
return colon === -1 ? h : h.slice(0, colon);
|
|
370
|
-
}
|
|
371
|
-
function hostHeaderIsLoopback(host) {
|
|
372
|
-
return isLoopbackHostname(hostnameFromHostHeader(host));
|
|
373
|
-
}
|
|
374
|
-
function originIsLoopback(origin) {
|
|
375
|
-
try {
|
|
376
|
-
return isLoopbackHostname(new URL(origin).hostname);
|
|
377
|
-
} catch {
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
function allowSensitiveRequest(headers, loopbackOnly) {
|
|
382
|
-
const { origin, host } = headers;
|
|
383
|
-
if (origin && origin.length > 0 && !originIsLoopback(origin)) return false;
|
|
384
|
-
if (loopbackOnly && host && host.length > 0 && !hostHeaderIsLoopback(host)) {
|
|
385
|
-
return false;
|
|
386
|
-
}
|
|
387
|
-
return true;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// src/server/server.ts
|
|
391
|
-
var DevtoolsServer = class {
|
|
392
|
-
wss;
|
|
393
|
-
clients = /* @__PURE__ */ new Set();
|
|
394
|
-
httpServer;
|
|
395
|
-
traces = [];
|
|
396
|
-
logs = [];
|
|
397
|
-
metrics = [];
|
|
398
|
-
errorAggregator = new ErrorAggregator();
|
|
399
|
-
limits;
|
|
400
|
-
verbose;
|
|
401
|
-
_port;
|
|
402
|
-
onData;
|
|
403
|
-
constructor(options = {}) {
|
|
404
|
-
this.limits = resolveTelemetryLimits(options);
|
|
405
|
-
this.verbose = options.verbose ?? false;
|
|
406
|
-
this._port = options.port ?? 4318;
|
|
407
|
-
this.onData = options.onData;
|
|
408
|
-
this.httpServer = options.server ?? http.createServer();
|
|
409
|
-
const loopbackOnly = options.host == null || hostHeaderIsLoopback(options.host);
|
|
410
|
-
this.wss = new ws.WebSocketServer({
|
|
411
|
-
server: this.httpServer,
|
|
412
|
-
path: options.path ?? "/ws",
|
|
413
|
-
verifyClient: ({ origin, req }) => allowSensitiveRequest({ origin, host: req.headers.host }, loopbackOnly)
|
|
414
|
-
});
|
|
415
|
-
this.wss.on("error", (err) => {
|
|
416
|
-
if (this.httpServer.listening) throw err;
|
|
417
|
-
});
|
|
418
|
-
this.wss.on("connection", (ws) => {
|
|
419
|
-
this.clients.add(ws);
|
|
420
|
-
this.log(`Client connected (${this.clients.size} total)`);
|
|
421
|
-
const data = this.getCurrentData();
|
|
422
|
-
if (data.traces.length > 0 || data.logs.length > 0 || data.errors.length > 0) {
|
|
423
|
-
ws.send(JSON.stringify(data));
|
|
424
|
-
}
|
|
425
|
-
ws.on("close", () => {
|
|
426
|
-
this.clients.delete(ws);
|
|
427
|
-
this.log(`Client disconnected (${this.clients.size} total)`);
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
if (!options.server) {
|
|
431
|
-
this.httpServer.listen(this._port, () => {
|
|
432
|
-
const addr = this.httpServer.address();
|
|
433
|
-
if (addr && typeof addr === "object") this._port = addr.port;
|
|
434
|
-
this.log(`WebSocket server listening on port ${this._port}`);
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
get port() {
|
|
439
|
-
const addr = this.httpServer.address();
|
|
440
|
-
if (addr && typeof addr === "object") return addr.port;
|
|
441
|
-
return this._port;
|
|
442
|
-
}
|
|
443
|
-
get clientCount() {
|
|
444
|
-
return this.clients.size;
|
|
445
|
-
}
|
|
446
|
-
addTrace(trace) {
|
|
447
|
-
const existing = this.traces.find((t) => t.traceId === trace.traceId);
|
|
448
|
-
if (existing) {
|
|
449
|
-
const existingSpanIds = new Set(existing.spans.map((s) => s.spanId));
|
|
450
|
-
for (const span of trace.spans) {
|
|
451
|
-
if (!existingSpanIds.has(span.spanId)) {
|
|
452
|
-
existing.spans.push(span);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
existing.startTime = Math.min(existing.startTime, trace.startTime);
|
|
456
|
-
existing.endTime = Math.max(existing.endTime, trace.endTime);
|
|
457
|
-
existing.duration = existing.endTime - existing.startTime;
|
|
458
|
-
if (trace.status === "ERROR") existing.status = "ERROR";
|
|
459
|
-
} else {
|
|
460
|
-
this.traces = appendWithLimit(
|
|
461
|
-
this.traces,
|
|
462
|
-
trace,
|
|
463
|
-
this.limits.maxTraceCount
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
this.errorAggregator.addErrorsFromTrace(trace);
|
|
467
|
-
this.broadcast({ traces: [trace], metrics: [], logs: [], errors: this.errorAggregator.getErrorGroups() });
|
|
468
|
-
}
|
|
469
|
-
addTraces(traces) {
|
|
470
|
-
for (const trace of traces) this.addTrace(trace);
|
|
471
|
-
}
|
|
472
|
-
// `errors` is full-state on every broadcast (the client replaces, not appends),
|
|
473
|
-
// so non-trace broadcasts must echo the current error groups rather than `[]` —
|
|
474
|
-
// otherwise a log/metric arriving after an error would wipe it from the UI.
|
|
475
|
-
addLog(log) {
|
|
476
|
-
this.logs = appendWithLimit(this.logs, log, this.limits.maxLogCount);
|
|
477
|
-
this.broadcast({ traces: [], metrics: [], logs: [log], errors: this.errorAggregator.getErrorGroups() });
|
|
478
|
-
}
|
|
479
|
-
addLogs(logs) {
|
|
480
|
-
this.logs = appendManyWithLimit(this.logs, logs, this.limits.maxLogCount);
|
|
481
|
-
this.broadcast({ traces: [], metrics: [], logs, errors: this.errorAggregator.getErrorGroups() });
|
|
482
|
-
}
|
|
483
|
-
addMetric(metric) {
|
|
484
|
-
this.metrics = appendWithLimit(
|
|
485
|
-
this.metrics,
|
|
486
|
-
metric,
|
|
487
|
-
this.limits.maxMetricCount
|
|
488
|
-
);
|
|
489
|
-
this.broadcast({ traces: [], metrics: [metric], logs: [], errors: this.errorAggregator.getErrorGroups() });
|
|
490
|
-
}
|
|
491
|
-
getCurrentData() {
|
|
492
|
-
return {
|
|
493
|
-
traces: this.traces,
|
|
494
|
-
metrics: this.metrics,
|
|
495
|
-
logs: this.logs,
|
|
496
|
-
errors: this.errorAggregator.getErrorGroups()
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
clearData() {
|
|
500
|
-
this.traces = [];
|
|
501
|
-
this.logs = [];
|
|
502
|
-
this.metrics = [];
|
|
503
|
-
this.errorAggregator.clear();
|
|
504
|
-
}
|
|
505
|
-
broadcast(data) {
|
|
506
|
-
const msg = JSON.stringify(data);
|
|
507
|
-
for (const client of this.clients) {
|
|
508
|
-
if (client.readyState === ws.WebSocket.OPEN) {
|
|
509
|
-
client.send(msg);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
if (this.onData) {
|
|
513
|
-
try {
|
|
514
|
-
this.onData(data);
|
|
515
|
-
} catch {
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
log(message) {
|
|
520
|
-
if (this.verbose) console.log(`[autotel-devtools] ${message}`);
|
|
521
|
-
}
|
|
522
|
-
async close() {
|
|
523
|
-
for (const client of this.clients) client.close();
|
|
524
|
-
this.clients.clear();
|
|
525
|
-
this.wss.close();
|
|
526
|
-
await new Promise((resolve3) => this.httpServer.close(() => resolve3()));
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
// src/server/resource-utils.ts
|
|
531
|
-
function getResourceName(resource, fallback = "unknown") {
|
|
532
|
-
if (!resource) return fallback;
|
|
533
|
-
const candidates = [
|
|
534
|
-
resource["service.name"],
|
|
535
|
-
resource["service.namespace"],
|
|
536
|
-
resource["deployment.environment.name"],
|
|
537
|
-
resource["host.name"],
|
|
538
|
-
resource["container.name"],
|
|
539
|
-
resource["process.executable.name"]
|
|
540
|
-
];
|
|
541
|
-
for (const candidate of candidates) {
|
|
542
|
-
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
543
|
-
return candidate;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
return fallback;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// src/server/otlp.ts
|
|
550
|
-
function resolveOtlpValue(v) {
|
|
551
|
-
if (!v) return void 0;
|
|
552
|
-
if (v.stringValue !== void 0) return v.stringValue;
|
|
553
|
-
if (v.boolValue !== void 0) return v.boolValue;
|
|
554
|
-
if (v.intValue !== void 0) return typeof v.intValue === "string" ? Number(v.intValue) : v.intValue;
|
|
555
|
-
if (v.doubleValue !== void 0) return v.doubleValue;
|
|
556
|
-
if (v.bytesValue !== void 0) return v.bytesValue;
|
|
557
|
-
if (v.arrayValue?.values) return v.arrayValue.values.map(resolveOtlpValue);
|
|
558
|
-
if (v.kvlistValue?.values) return flattenAttributes(v.kvlistValue.values);
|
|
559
|
-
return void 0;
|
|
560
|
-
}
|
|
561
|
-
function flattenAttributes(attrs) {
|
|
562
|
-
const out = {};
|
|
563
|
-
if (!attrs) return out;
|
|
564
|
-
for (const { key, value } of attrs) {
|
|
565
|
-
out[key] = resolveOtlpValue(value);
|
|
566
|
-
}
|
|
567
|
-
return out;
|
|
568
|
-
}
|
|
569
|
-
function nanoToMs(nano) {
|
|
570
|
-
if (!nano) return 0;
|
|
571
|
-
const ns = BigInt(nano);
|
|
572
|
-
const ms = ns / 1000000n;
|
|
573
|
-
const remNs = ns % 1000000n;
|
|
574
|
-
return Number(ms) + Number(remNs) / 1e6;
|
|
575
|
-
}
|
|
576
|
-
var SPAN_KIND_MAP = {
|
|
577
|
-
0: "INTERNAL",
|
|
578
|
-
1: "INTERNAL",
|
|
579
|
-
2: "SERVER",
|
|
580
|
-
3: "CLIENT",
|
|
581
|
-
4: "PRODUCER",
|
|
582
|
-
5: "CONSUMER",
|
|
583
|
-
SPAN_KIND_INTERNAL: "INTERNAL",
|
|
584
|
-
SPAN_KIND_SERVER: "SERVER",
|
|
585
|
-
SPAN_KIND_CLIENT: "CLIENT",
|
|
586
|
-
SPAN_KIND_PRODUCER: "PRODUCER",
|
|
587
|
-
SPAN_KIND_CONSUMER: "CONSUMER"
|
|
588
|
-
};
|
|
589
|
-
function normalizeHexId(id) {
|
|
590
|
-
if (!id) return "";
|
|
591
|
-
const isBase64Like = /^[A-Za-z0-9+/=]+$/.test(id) && !/^[0-9a-f]+$/i.test(id);
|
|
592
|
-
const isLikelyBase64Id = isBase64Like && (id.length === 12 || id.length === 24 || id.length === 28 || id.length === 44 || id.length === 48);
|
|
593
|
-
if (isLikelyBase64Id) {
|
|
594
|
-
try {
|
|
595
|
-
const bytes = Buffer.from(id, "base64");
|
|
596
|
-
return bytes.toString("hex");
|
|
597
|
-
} catch {
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return id;
|
|
601
|
-
}
|
|
602
|
-
function parseOtlpTraces(payload) {
|
|
603
|
-
if (!payload || typeof payload !== "object") return [];
|
|
604
|
-
const { resourceSpans } = payload;
|
|
605
|
-
if (!Array.isArray(resourceSpans) || resourceSpans.length === 0) return [];
|
|
606
|
-
const traceMap = /* @__PURE__ */ new Map();
|
|
607
|
-
for (const rs of resourceSpans) {
|
|
608
|
-
const resourceAttrs = flattenAttributes(rs.resource?.attributes);
|
|
609
|
-
const service = String(resourceAttrs["service.name"] || "unknown");
|
|
610
|
-
const scopeSpans = rs.scopeSpans || [];
|
|
611
|
-
for (const ss of scopeSpans) {
|
|
612
|
-
const scope = ss.scope?.name ? { name: ss.scope.name, version: ss.scope.version || void 0 } : void 0;
|
|
613
|
-
for (const span of ss.spans || []) {
|
|
614
|
-
const traceId = normalizeHexId(span.traceId);
|
|
615
|
-
if (!traceId) continue;
|
|
616
|
-
const startMs = nanoToMs(span.startTimeUnixNano);
|
|
617
|
-
const endMs = nanoToMs(span.endTimeUnixNano);
|
|
618
|
-
const statusCode = span.status?.code;
|
|
619
|
-
let status = "UNSET";
|
|
620
|
-
if (statusCode === 1 || statusCode === "STATUS_CODE_OK") status = "OK";
|
|
621
|
-
if (statusCode === 2 || statusCode === "STATUS_CODE_ERROR") status = "ERROR";
|
|
622
|
-
const spanData = {
|
|
623
|
-
traceId,
|
|
624
|
-
spanId: normalizeHexId(span.spanId),
|
|
625
|
-
parentSpanId: normalizeHexId(span.parentSpanId) || void 0,
|
|
626
|
-
name: span.name || "unknown",
|
|
627
|
-
kind: SPAN_KIND_MAP[span.kind ?? 0] || "INTERNAL",
|
|
628
|
-
startTime: startMs,
|
|
629
|
-
endTime: endMs,
|
|
630
|
-
duration: endMs - startMs,
|
|
631
|
-
attributes: { ...resourceAttrs, ...flattenAttributes(span.attributes) },
|
|
632
|
-
status: { code: status, message: span.status?.message },
|
|
633
|
-
events: (span.events || []).map((e) => ({
|
|
634
|
-
name: e.name || "",
|
|
635
|
-
timestamp: nanoToMs(e.timeUnixNano),
|
|
636
|
-
attributes: flattenAttributes(e.attributes)
|
|
637
|
-
})),
|
|
638
|
-
links: (span.links || []).map((l) => ({
|
|
639
|
-
traceId: normalizeHexId(l.traceId),
|
|
640
|
-
spanId: normalizeHexId(l.spanId),
|
|
641
|
-
attributes: flattenAttributes(l.attributes)
|
|
642
|
-
})),
|
|
643
|
-
scope
|
|
644
|
-
};
|
|
645
|
-
const existing = traceMap.get(traceId);
|
|
646
|
-
if (existing) {
|
|
647
|
-
existing.spans.push(spanData);
|
|
648
|
-
} else {
|
|
649
|
-
traceMap.set(traceId, { spans: [spanData], service });
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
const traces = [];
|
|
655
|
-
for (const [traceId, { spans, service }] of traceMap) {
|
|
656
|
-
const sorted = spans.sort((a, b) => a.startTime - b.startTime);
|
|
657
|
-
const rootSpan = sorted.find((s) => !s.parentSpanId) || sorted[0];
|
|
658
|
-
const startTime = Math.min(...sorted.map((s) => s.startTime));
|
|
659
|
-
const endTime = Math.max(...sorted.map((s) => s.endTime));
|
|
660
|
-
const hasError = sorted.some((s) => s.status.code === "ERROR");
|
|
661
|
-
traces.push({
|
|
662
|
-
traceId,
|
|
663
|
-
correlationId: traceId.slice(0, 16),
|
|
664
|
-
rootSpan,
|
|
665
|
-
spans: sorted,
|
|
666
|
-
startTime,
|
|
667
|
-
endTime,
|
|
668
|
-
duration: endTime - startTime,
|
|
669
|
-
status: hasError ? "ERROR" : "OK",
|
|
670
|
-
service
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
return traces;
|
|
674
|
-
}
|
|
675
|
-
function parseOtlpLogs(payload) {
|
|
676
|
-
if (!payload || typeof payload !== "object") return [];
|
|
677
|
-
const { resourceLogs } = payload;
|
|
678
|
-
if (!Array.isArray(resourceLogs)) return [];
|
|
679
|
-
const logs = [];
|
|
680
|
-
for (const rl of resourceLogs) {
|
|
681
|
-
const resourceAttrs = flattenAttributes(rl.resource?.attributes);
|
|
682
|
-
for (const sl of rl.scopeLogs || []) {
|
|
683
|
-
for (const rec of sl.logRecords || []) {
|
|
684
|
-
const timestamp = nanoToMs(rec.timeUnixNano || rec.observedTimeUnixNano);
|
|
685
|
-
const traceId = normalizeHexId(rec.traceId) || void 0;
|
|
686
|
-
const spanId = normalizeHexId(rec.spanId) || void 0;
|
|
687
|
-
const body = rec.body ? resolveOtlpValue(rec.body) : "";
|
|
688
|
-
logs.push({
|
|
689
|
-
id: `${traceId || "no-trace"}:${spanId || "no-span"}:${timestamp}:${rec.severityNumber || 0}`,
|
|
690
|
-
traceId,
|
|
691
|
-
spanId,
|
|
692
|
-
resourceName: getResourceName(resourceAttrs),
|
|
693
|
-
severityText: rec.severityText,
|
|
694
|
-
severityNumber: rec.severityNumber,
|
|
695
|
-
body: typeof body === "string" ? body : body,
|
|
696
|
-
timestamp,
|
|
697
|
-
attributes: flattenAttributes(rec.attributes),
|
|
698
|
-
resource: resourceAttrs
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
return logs;
|
|
704
|
-
}
|
|
705
|
-
function countOtlpMetrics(payload) {
|
|
706
|
-
if (!payload || typeof payload !== "object") return 0;
|
|
707
|
-
const { resourceMetrics } = payload;
|
|
708
|
-
if (!Array.isArray(resourceMetrics)) return 0;
|
|
709
|
-
let count = 0;
|
|
710
|
-
for (const rm of resourceMetrics) {
|
|
711
|
-
for (const sm of rm.scopeMetrics || []) {
|
|
712
|
-
count += (sm.metrics || []).length;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
return count;
|
|
716
|
-
}
|
|
717
|
-
async function readJsonBody(req) {
|
|
718
|
-
return new Promise((resolve3, reject) => {
|
|
719
|
-
const chunks = [];
|
|
720
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
721
|
-
req.on("end", () => {
|
|
722
|
-
try {
|
|
723
|
-
resolve3(JSON.parse(Buffer.concat(chunks).toString()));
|
|
724
|
-
} catch {
|
|
725
|
-
reject(new Error("Invalid JSON"));
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
req.on("error", reject);
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
async function readRawBody(req) {
|
|
732
|
-
return new Promise((resolve3, reject) => {
|
|
733
|
-
const chunks = [];
|
|
734
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
735
|
-
req.on("end", () => resolve3(Buffer.concat(chunks)));
|
|
736
|
-
req.on("error", reject);
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
function isProtobufContentType(contentType) {
|
|
740
|
-
if (!contentType) return false;
|
|
741
|
-
const value = contentType.toLowerCase();
|
|
742
|
-
return value.includes("application/x-protobuf") || value.includes("application/protobuf");
|
|
743
|
-
}
|
|
744
|
-
function sendJson(res, status, data) {
|
|
745
|
-
const body = JSON.stringify(data);
|
|
746
|
-
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
|
|
747
|
-
res.end(body);
|
|
748
|
-
}
|
|
749
|
-
var COMMON_PROTO = `
|
|
750
|
-
syntax = "proto3";
|
|
751
|
-
package opentelemetry.proto.common.v1;
|
|
752
|
-
|
|
753
|
-
message AnyValue {
|
|
754
|
-
oneof value {
|
|
755
|
-
string string_value = 1;
|
|
756
|
-
bool bool_value = 2;
|
|
757
|
-
int64 int_value = 3;
|
|
758
|
-
double double_value = 4;
|
|
759
|
-
ArrayValue array_value = 5;
|
|
760
|
-
KeyValueList kvlist_value = 6;
|
|
761
|
-
bytes bytes_value = 7;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
message ArrayValue { repeated AnyValue values = 1; }
|
|
765
|
-
message KeyValueList { repeated KeyValue values = 1; }
|
|
766
|
-
message KeyValue {
|
|
767
|
-
string key = 1;
|
|
768
|
-
AnyValue value = 2;
|
|
769
|
-
}
|
|
770
|
-
message InstrumentationScope {
|
|
771
|
-
string name = 1;
|
|
772
|
-
string version = 2;
|
|
773
|
-
repeated KeyValue attributes = 3;
|
|
774
|
-
uint32 dropped_attributes_count = 4;
|
|
775
|
-
}
|
|
776
|
-
`;
|
|
777
|
-
var RESOURCE_PROTO = `
|
|
778
|
-
syntax = "proto3";
|
|
779
|
-
package opentelemetry.proto.resource.v1;
|
|
780
|
-
|
|
781
|
-
message Resource {
|
|
782
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 1;
|
|
783
|
-
uint32 dropped_attributes_count = 2;
|
|
784
|
-
}
|
|
785
|
-
`;
|
|
786
|
-
var TRACE_PROTO = `
|
|
787
|
-
syntax = "proto3";
|
|
788
|
-
package opentelemetry.proto.trace.v1;
|
|
789
|
-
|
|
790
|
-
message ResourceSpans {
|
|
791
|
-
opentelemetry.proto.resource.v1.Resource resource = 1;
|
|
792
|
-
repeated ScopeSpans scope_spans = 2;
|
|
793
|
-
string schema_url = 3;
|
|
794
|
-
}
|
|
795
|
-
message ScopeSpans {
|
|
796
|
-
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
|
797
|
-
repeated Span spans = 2;
|
|
798
|
-
string schema_url = 3;
|
|
799
|
-
}
|
|
800
|
-
message Span {
|
|
801
|
-
bytes trace_id = 1;
|
|
802
|
-
bytes span_id = 2;
|
|
803
|
-
string trace_state = 3;
|
|
804
|
-
bytes parent_span_id = 4;
|
|
805
|
-
fixed32 flags = 16;
|
|
806
|
-
string name = 5;
|
|
807
|
-
SpanKind kind = 6;
|
|
808
|
-
fixed64 start_time_unix_nano = 7;
|
|
809
|
-
fixed64 end_time_unix_nano = 8;
|
|
810
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 9;
|
|
811
|
-
uint32 dropped_attributes_count = 10;
|
|
812
|
-
repeated Event events = 11;
|
|
813
|
-
uint32 dropped_events_count = 12;
|
|
814
|
-
repeated Link links = 13;
|
|
815
|
-
uint32 dropped_links_count = 14;
|
|
816
|
-
Status status = 15;
|
|
817
|
-
|
|
818
|
-
enum SpanKind {
|
|
819
|
-
SPAN_KIND_UNSPECIFIED = 0;
|
|
820
|
-
SPAN_KIND_INTERNAL = 1;
|
|
821
|
-
SPAN_KIND_SERVER = 2;
|
|
822
|
-
SPAN_KIND_CLIENT = 3;
|
|
823
|
-
SPAN_KIND_PRODUCER = 4;
|
|
824
|
-
SPAN_KIND_CONSUMER = 5;
|
|
825
|
-
}
|
|
826
|
-
message Event {
|
|
827
|
-
fixed64 time_unix_nano = 1;
|
|
828
|
-
string name = 2;
|
|
829
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 3;
|
|
830
|
-
uint32 dropped_attributes_count = 4;
|
|
831
|
-
}
|
|
832
|
-
message Link {
|
|
833
|
-
bytes trace_id = 1;
|
|
834
|
-
bytes span_id = 2;
|
|
835
|
-
string trace_state = 3;
|
|
836
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 4;
|
|
837
|
-
uint32 dropped_attributes_count = 5;
|
|
838
|
-
fixed32 flags = 6;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
message Status {
|
|
842
|
-
reserved 1;
|
|
843
|
-
string message = 2;
|
|
844
|
-
StatusCode code = 3;
|
|
845
|
-
|
|
846
|
-
enum StatusCode {
|
|
847
|
-
STATUS_CODE_UNSET = 0;
|
|
848
|
-
STATUS_CODE_OK = 1;
|
|
849
|
-
STATUS_CODE_ERROR = 2;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
message ExportTraceServiceRequest {
|
|
853
|
-
repeated ResourceSpans resource_spans = 1;
|
|
854
|
-
}
|
|
855
|
-
`;
|
|
856
|
-
var LOGS_PROTO = `
|
|
857
|
-
syntax = "proto3";
|
|
858
|
-
package opentelemetry.proto.logs.v1;
|
|
859
|
-
|
|
860
|
-
enum SeverityNumber {
|
|
861
|
-
SEVERITY_NUMBER_UNSPECIFIED = 0;
|
|
862
|
-
SEVERITY_NUMBER_TRACE = 1;
|
|
863
|
-
SEVERITY_NUMBER_TRACE2 = 2;
|
|
864
|
-
SEVERITY_NUMBER_TRACE3 = 3;
|
|
865
|
-
SEVERITY_NUMBER_TRACE4 = 4;
|
|
866
|
-
SEVERITY_NUMBER_DEBUG = 5;
|
|
867
|
-
SEVERITY_NUMBER_DEBUG2 = 6;
|
|
868
|
-
SEVERITY_NUMBER_DEBUG3 = 7;
|
|
869
|
-
SEVERITY_NUMBER_DEBUG4 = 8;
|
|
870
|
-
SEVERITY_NUMBER_INFO = 9;
|
|
871
|
-
SEVERITY_NUMBER_INFO2 = 10;
|
|
872
|
-
SEVERITY_NUMBER_INFO3 = 11;
|
|
873
|
-
SEVERITY_NUMBER_INFO4 = 12;
|
|
874
|
-
SEVERITY_NUMBER_WARN = 13;
|
|
875
|
-
SEVERITY_NUMBER_WARN2 = 14;
|
|
876
|
-
SEVERITY_NUMBER_WARN3 = 15;
|
|
877
|
-
SEVERITY_NUMBER_WARN4 = 16;
|
|
878
|
-
SEVERITY_NUMBER_ERROR = 17;
|
|
879
|
-
SEVERITY_NUMBER_ERROR2 = 18;
|
|
880
|
-
SEVERITY_NUMBER_ERROR3 = 19;
|
|
881
|
-
SEVERITY_NUMBER_ERROR4 = 20;
|
|
882
|
-
SEVERITY_NUMBER_FATAL = 21;
|
|
883
|
-
SEVERITY_NUMBER_FATAL2 = 22;
|
|
884
|
-
SEVERITY_NUMBER_FATAL3 = 23;
|
|
885
|
-
SEVERITY_NUMBER_FATAL4 = 24;
|
|
886
|
-
}
|
|
887
|
-
message ResourceLogs {
|
|
888
|
-
opentelemetry.proto.resource.v1.Resource resource = 1;
|
|
889
|
-
repeated ScopeLogs scope_logs = 2;
|
|
890
|
-
string schema_url = 3;
|
|
891
|
-
}
|
|
892
|
-
message ScopeLogs {
|
|
893
|
-
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
|
894
|
-
repeated LogRecord log_records = 2;
|
|
895
|
-
string schema_url = 3;
|
|
896
|
-
}
|
|
897
|
-
message LogRecord {
|
|
898
|
-
reserved 4;
|
|
899
|
-
fixed64 time_unix_nano = 1;
|
|
900
|
-
fixed64 observed_time_unix_nano = 11;
|
|
901
|
-
SeverityNumber severity_number = 2;
|
|
902
|
-
string severity_text = 3;
|
|
903
|
-
opentelemetry.proto.common.v1.AnyValue body = 5;
|
|
904
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 6;
|
|
905
|
-
uint32 dropped_attributes_count = 7;
|
|
906
|
-
fixed32 flags = 8;
|
|
907
|
-
bytes trace_id = 9;
|
|
908
|
-
bytes span_id = 10;
|
|
909
|
-
}
|
|
910
|
-
message ExportLogsServiceRequest {
|
|
911
|
-
repeated ResourceLogs resource_logs = 1;
|
|
912
|
-
}
|
|
913
|
-
`;
|
|
914
|
-
var METRICS_PROTO = `
|
|
915
|
-
syntax = "proto3";
|
|
916
|
-
package opentelemetry.proto.metrics.v1;
|
|
917
|
-
|
|
918
|
-
message ResourceMetrics {
|
|
919
|
-
opentelemetry.proto.resource.v1.Resource resource = 1;
|
|
920
|
-
repeated ScopeMetrics scope_metrics = 2;
|
|
921
|
-
string schema_url = 3;
|
|
922
|
-
}
|
|
923
|
-
message ScopeMetrics {
|
|
924
|
-
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
|
925
|
-
repeated Metric metrics = 2;
|
|
926
|
-
string schema_url = 3;
|
|
927
|
-
}
|
|
928
|
-
message Metric {
|
|
929
|
-
string name = 1;
|
|
930
|
-
string description = 2;
|
|
931
|
-
string unit = 3;
|
|
932
|
-
}
|
|
933
|
-
message ExportMetricsServiceRequest {
|
|
934
|
-
repeated ResourceMetrics resource_metrics = 1;
|
|
935
|
-
}
|
|
936
|
-
`;
|
|
937
|
-
var TO_OBJECT_OPTIONS = {
|
|
938
|
-
longs: String,
|
|
939
|
-
bytes: String,
|
|
940
|
-
defaults: false
|
|
941
|
-
};
|
|
942
|
-
var cachedRoot = null;
|
|
943
|
-
function getRoot() {
|
|
944
|
-
if (cachedRoot) return cachedRoot;
|
|
945
|
-
const root = new protobuf__default.default.Root();
|
|
946
|
-
for (const source of [COMMON_PROTO, RESOURCE_PROTO, TRACE_PROTO, LOGS_PROTO, METRICS_PROTO]) {
|
|
947
|
-
protobuf__default.default.parse(source, root, { keepCase: false });
|
|
948
|
-
}
|
|
949
|
-
root.resolveAll();
|
|
950
|
-
cachedRoot = root;
|
|
951
|
-
return root;
|
|
952
|
-
}
|
|
953
|
-
function decodeRequest(typeName, body) {
|
|
954
|
-
const messageType = getRoot().lookupType(typeName);
|
|
955
|
-
const message = messageType.decode(body);
|
|
956
|
-
return messageType.toObject(message, TO_OBJECT_OPTIONS);
|
|
957
|
-
}
|
|
958
|
-
function decodeOtlpTraceRequest(body) {
|
|
959
|
-
return decodeRequest("opentelemetry.proto.trace.v1.ExportTraceServiceRequest", body);
|
|
960
|
-
}
|
|
961
|
-
function decodeOtlpLogsRequest(body) {
|
|
962
|
-
return decodeRequest("opentelemetry.proto.logs.v1.ExportLogsServiceRequest", body);
|
|
963
|
-
}
|
|
964
|
-
function decodeOtlpMetricsRequest(body) {
|
|
965
|
-
return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// src/server/identity.ts
|
|
969
|
-
var DEVTOOLS_IDENTITY = "autotel-devtools";
|
|
970
|
-
async function probePortHolder(host, port, timeoutMs = 500) {
|
|
971
|
-
const authority = host.includes(":") ? `[${host}]` : host;
|
|
972
|
-
const controller = new AbortController();
|
|
973
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
974
|
-
try {
|
|
975
|
-
const res = await fetch(`http://${authority}:${port}/healthz`, {
|
|
976
|
-
signal: controller.signal
|
|
977
|
-
});
|
|
978
|
-
if (res.headers.get("x-autotel-devtools")) return "autotel-devtools";
|
|
979
|
-
try {
|
|
980
|
-
const body = await res.json();
|
|
981
|
-
if (body && body.service === DEVTOOLS_IDENTITY) return "autotel-devtools";
|
|
982
|
-
} catch {
|
|
983
|
-
}
|
|
984
|
-
return "foreign";
|
|
985
|
-
} catch {
|
|
986
|
-
return "none";
|
|
987
|
-
} finally {
|
|
988
|
-
clearTimeout(timer);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// src/server/http.ts
|
|
993
|
-
function sendOtlpError(res, req, e) {
|
|
994
|
-
sendJson(res, 400, {
|
|
995
|
-
error: "Invalid OTLP payload",
|
|
996
|
-
message: e instanceof Error ? e.message : String(e),
|
|
997
|
-
contentType: req.headers["content-type"] ?? null
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
var PROTOBUF_DECODERS = {
|
|
1001
|
-
traces: decodeOtlpTraceRequest,
|
|
1002
|
-
logs: decodeOtlpLogsRequest,
|
|
1003
|
-
metrics: decodeOtlpMetricsRequest
|
|
1004
|
-
};
|
|
1005
|
-
async function readOtlpPayload(req, signal) {
|
|
1006
|
-
if (isProtobufContentType(req.headers["content-type"])) {
|
|
1007
|
-
return PROTOBUF_DECODERS[signal](await readRawBody(req));
|
|
1008
|
-
}
|
|
1009
|
-
return readJsonBody(req);
|
|
1010
|
-
}
|
|
1011
|
-
function findPackageRoot() {
|
|
1012
|
-
let dir = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
1013
|
-
for (let i = 0; i < 5; i++) {
|
|
1014
|
-
if (fs.existsSync(path.resolve(dir, "package.json"))) return dir;
|
|
1015
|
-
dir = path.dirname(dir);
|
|
1016
|
-
}
|
|
1017
|
-
return dir;
|
|
1018
|
-
}
|
|
1019
|
-
var FULLPAGE_HTML = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>autotel-devtools</title><style>*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;width:100%;overflow:hidden}</style></head><body><script src="/widget.js?mode=fullpage"></script></body></html>`;
|
|
1020
|
-
var cachedVersion = null;
|
|
1021
|
-
function getVersion() {
|
|
1022
|
-
if (cachedVersion !== null) return cachedVersion;
|
|
1023
|
-
let version = "unknown";
|
|
1024
|
-
try {
|
|
1025
|
-
const pkg = JSON.parse(fs.readFileSync(path.resolve(findPackageRoot(), "package.json"), "utf8"));
|
|
1026
|
-
if (typeof pkg.version === "string") version = pkg.version;
|
|
1027
|
-
} catch {
|
|
1028
|
-
}
|
|
1029
|
-
cachedVersion = version;
|
|
1030
|
-
return version;
|
|
1031
|
-
}
|
|
1032
|
-
var cachedWidgetJs = null;
|
|
1033
|
-
function getWidgetJs() {
|
|
1034
|
-
if (!cachedWidgetJs) {
|
|
1035
|
-
const pkgRoot = findPackageRoot();
|
|
1036
|
-
const candidates = [
|
|
1037
|
-
path.resolve(pkgRoot, "dist", "widget.global.js"),
|
|
1038
|
-
path.resolve(pkgRoot, "widget.global.js")
|
|
1039
|
-
];
|
|
1040
|
-
for (const candidate of candidates) {
|
|
1041
|
-
try {
|
|
1042
|
-
cachedWidgetJs = fs.readFileSync(candidate, "utf8");
|
|
1043
|
-
break;
|
|
1044
|
-
} catch {
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
if (!cachedWidgetJs) {
|
|
1048
|
-
cachedWidgetJs = "// widget bundle not found - run pnpm build first";
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
return cachedWidgetJs;
|
|
1052
|
-
}
|
|
1053
|
-
function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
|
|
1054
|
-
const loopbackOnly = options.loopbackOnly ?? true;
|
|
1055
|
-
httpServer.on("request", async (req, res) => {
|
|
1056
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1057
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
1058
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1059
|
-
res.setHeader("x-autotel-devtools", getVersion());
|
|
1060
|
-
res.setHeader("Access-Control-Expose-Headers", "x-autotel-devtools");
|
|
1061
|
-
if (req.method === "OPTIONS") {
|
|
1062
|
-
res.writeHead(204);
|
|
1063
|
-
res.end();
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
const url = req.url || "/";
|
|
1067
|
-
if (req.method === "GET" && url === "/") {
|
|
1068
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Length": Buffer.byteLength(FULLPAGE_HTML) });
|
|
1069
|
-
res.end(FULLPAGE_HTML);
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
if (req.method === "GET" && url.startsWith("/widget.js")) {
|
|
1073
|
-
const js = getWidgetJs();
|
|
1074
|
-
res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Content-Length": Buffer.byteLength(js) });
|
|
1075
|
-
res.end(js);
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
if (req.method === "GET" && url === "/healthz") {
|
|
1079
|
-
sendJson(res, 200, {
|
|
1080
|
-
ok: true,
|
|
1081
|
-
service: DEVTOOLS_IDENTITY,
|
|
1082
|
-
version: getVersion(),
|
|
1083
|
-
clients: devtools.clientCount
|
|
1084
|
-
});
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
if (req.method === "GET" && url === "/v1/traces") {
|
|
1088
|
-
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1089
|
-
sendJson(res, 403, { error: "Forbidden" });
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
const data = devtools.getCurrentData();
|
|
1093
|
-
sendJson(res, 200, { traces: data.traces, count: data.traces.length });
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
if (req.method === "DELETE" && url === "/v1/traces") {
|
|
1097
|
-
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1098
|
-
sendJson(res, 403, { error: "Forbidden" });
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
devtools.clearData();
|
|
1102
|
-
sendJson(res, 200, { cleared: true });
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
if (req.method === "POST" && url === "/v1/traces") {
|
|
1106
|
-
try {
|
|
1107
|
-
const payload = await readOtlpPayload(req, "traces");
|
|
1108
|
-
const traces = parseOtlpTraces(payload);
|
|
1109
|
-
devtools.addTraces(traces);
|
|
1110
|
-
sendJson(res, 200, { acceptedTraces: traces.length });
|
|
1111
|
-
} catch (e) {
|
|
1112
|
-
sendOtlpError(res, req, e);
|
|
1113
|
-
}
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
if (req.method === "POST" && url === "/v1/logs") {
|
|
1117
|
-
try {
|
|
1118
|
-
const payload = await readOtlpPayload(req, "logs");
|
|
1119
|
-
const logs = parseOtlpLogs(payload);
|
|
1120
|
-
devtools.addLogs(logs);
|
|
1121
|
-
sendJson(res, 200, { acceptedLogs: logs.length });
|
|
1122
|
-
} catch (e) {
|
|
1123
|
-
sendOtlpError(res, req, e);
|
|
1124
|
-
}
|
|
1125
|
-
return;
|
|
1126
|
-
}
|
|
1127
|
-
if (req.method === "POST" && url === "/v1/metrics") {
|
|
1128
|
-
try {
|
|
1129
|
-
const payload = await readOtlpPayload(req, "metrics");
|
|
1130
|
-
const count = countOtlpMetrics(payload);
|
|
1131
|
-
sendJson(res, 200, { acceptedMetrics: count });
|
|
1132
|
-
} catch (e) {
|
|
1133
|
-
sendOtlpError(res, req, e);
|
|
1134
|
-
}
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
sendJson(res, 404, { error: "Not found" });
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1141
|
-
var DEFAULT_MAX_PORT_TRIES = 20;
|
|
1142
|
-
function formatAddress(host, port) {
|
|
1143
|
-
return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
1144
|
-
}
|
|
1145
|
-
function listenLoopbackDualStack(args) {
|
|
1146
|
-
const { primary, port, host, attachSecondary, maxTries } = args;
|
|
1147
|
-
const maxAttempts = Math.max(1, maxTries ?? DEFAULT_MAX_PORT_TRIES);
|
|
1148
|
-
let sibling;
|
|
1149
|
-
const ready = new Promise(
|
|
1150
|
-
(resolve3, reject) => {
|
|
1151
|
-
const addresses = [];
|
|
1152
|
-
const warnings = [];
|
|
1153
|
-
const primaryHost = host === "localhost" ? "127.0.0.1" : host;
|
|
1154
|
-
let candidate = port;
|
|
1155
|
-
let attempt = 0;
|
|
1156
|
-
const bindFailed = (atPort, msg) => reject(
|
|
1157
|
-
new Error(`could not bind ${formatAddress(primaryHost, atPort)}: ${msg}`)
|
|
1158
|
-
);
|
|
1159
|
-
const onError = (e) => {
|
|
1160
|
-
if (e.code !== "EADDRINUSE") return bindFailed(candidate, e.message);
|
|
1161
|
-
if (++attempt >= maxAttempts) {
|
|
1162
|
-
reject(
|
|
1163
|
-
new Error(
|
|
1164
|
-
`could not bind ${formatAddress(primaryHost, port)}: ${maxAttempts} consecutive ports in use`
|
|
1165
|
-
)
|
|
1166
|
-
);
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
candidate++;
|
|
1170
|
-
listen();
|
|
1171
|
-
};
|
|
1172
|
-
const onListening = () => {
|
|
1173
|
-
primary.removeListener("error", onError);
|
|
1174
|
-
if (candidate !== port) {
|
|
1175
|
-
warnings.push(`port ${port} was busy; using ${candidate} instead`);
|
|
1176
|
-
}
|
|
1177
|
-
const addr = primary.address();
|
|
1178
|
-
const resolvedPort = addr && typeof addr === "object" ? addr.port : candidate;
|
|
1179
|
-
addresses.push(formatAddress(primaryHost, resolvedPort));
|
|
1180
|
-
if (!LOOPBACK.has(host)) {
|
|
1181
|
-
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
const siblingHost = primaryHost === "::1" ? "127.0.0.1" : "::1";
|
|
1185
|
-
const s = http.createServer();
|
|
1186
|
-
attachSecondary(s);
|
|
1187
|
-
const onSiblingError = (se) => {
|
|
1188
|
-
s.close();
|
|
1189
|
-
warnings.push(
|
|
1190
|
-
`could not also bind ${formatAddress(siblingHost, resolvedPort)} (${se.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
|
|
1191
|
-
);
|
|
1192
|
-
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1193
|
-
};
|
|
1194
|
-
s.once("error", onSiblingError);
|
|
1195
|
-
s.listen(resolvedPort, siblingHost, () => {
|
|
1196
|
-
s.off("error", onSiblingError);
|
|
1197
|
-
sibling = s;
|
|
1198
|
-
addresses.push(formatAddress(siblingHost, resolvedPort));
|
|
1199
|
-
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1200
|
-
});
|
|
1201
|
-
};
|
|
1202
|
-
const listen = () => {
|
|
1203
|
-
try {
|
|
1204
|
-
primary.listen(candidate, primaryHost);
|
|
1205
|
-
} catch (e) {
|
|
1206
|
-
primary.removeListener("error", onError);
|
|
1207
|
-
primary.removeListener("listening", onListening);
|
|
1208
|
-
bindFailed(candidate, e.message);
|
|
1209
|
-
}
|
|
1210
|
-
};
|
|
1211
|
-
primary.on("error", onError);
|
|
1212
|
-
primary.once("listening", onListening);
|
|
1213
|
-
listen();
|
|
1214
|
-
}
|
|
1215
|
-
);
|
|
1216
|
-
return {
|
|
1217
|
-
ready,
|
|
1218
|
-
closeSibling: () => new Promise((res) => {
|
|
1219
|
-
if (!sibling) return res();
|
|
1220
|
-
sibling.close(() => res());
|
|
1221
|
-
})
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
// src/cli.ts
|
|
2
|
+
const require_http = require('./http-Yj6iSrMX.cjs');
|
|
3
|
+
const require_listen = require('./listen-BBsxO0wm.cjs');
|
|
4
|
+
let node_http = require("node:http");
|
|
5
|
+
let node_fs = require("node:fs");
|
|
6
|
+
let node_path = require("node:path");
|
|
7
|
+
let node_url = require("node:url");
|
|
8
|
+
|
|
9
|
+
//#region src/cli.ts
|
|
1226
10
|
function printHelp() {
|
|
1227
|
-
|
|
1228
|
-
`autotel-devtools - Standalone OTLP receiver with web devtools UI
|
|
1229
|
-
|
|
1230
|
-
Usage: autotel-devtools [port] [options]
|
|
1231
|
-
|
|
1232
|
-
Arguments:
|
|
1233
|
-
port Port to listen on (shorthand for --port; must be a positive integer)
|
|
1234
|
-
|
|
1235
|
-
Options:
|
|
1236
|
-
-p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).
|
|
1237
|
-
If the port is taken, the next free port is used and a warning is shown.
|
|
1238
|
-
-H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)
|
|
1239
|
-
-t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)
|
|
1240
|
-
Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT
|
|
1241
|
-
-h, --help Show this help message
|
|
1242
|
-
-v, --version Show version number
|
|
1243
|
-
|
|
1244
|
-
Endpoints:
|
|
1245
|
-
GET / Web devtools UI (fullpage)
|
|
1246
|
-
GET /widget.js Widget bundle (embed in your app)
|
|
1247
|
-
POST /v1/traces Receive OTLP JSON trace data
|
|
1248
|
-
GET /v1/traces Read back received traces (verify ingestion in tests)
|
|
1249
|
-
DELETE /v1/traces Clear captured telemetry (test reset)
|
|
1250
|
-
POST /v1/logs Receive OTLP JSON log data
|
|
1251
|
-
POST /v1/metrics Receive OTLP JSON metric data
|
|
1252
|
-
WS /ws WebSocket stream for real-time updates
|
|
1253
|
-
GET /healthz Health check
|
|
1254
|
-
|
|
1255
|
-
Examples:
|
|
1256
|
-
npx autotel-devtools
|
|
1257
|
-
npx autotel-devtools 4319
|
|
1258
|
-
npx autotel-devtools -p 4319 -H 0.0.0.0
|
|
1259
|
-
|
|
1260
|
-
Then point your app:
|
|
1261
|
-
OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js
|
|
1262
|
-
|
|
1263
|
-
View in browser:
|
|
1264
|
-
http://localhost:4318
|
|
1265
|
-
|
|
1266
|
-
Or embed widget in your app:
|
|
1267
|
-
<script src="http://localhost:4318/widget.js"></script>
|
|
1268
|
-
|
|
1269
|
-
`
|
|
1270
|
-
);
|
|
11
|
+
process.stdout.write("autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"><\/script>\n\n");
|
|
1271
12
|
}
|
|
1272
13
|
function printVersion() {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
process.stdout.write("unknown\n");
|
|
1281
|
-
}
|
|
14
|
+
try {
|
|
15
|
+
const pkgPath = (0, node_path.resolve)((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "..", "package.json");
|
|
16
|
+
const pkg = JSON.parse((0, node_fs.readFileSync)(pkgPath, "utf8"));
|
|
17
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
18
|
+
} catch {
|
|
19
|
+
process.stdout.write("unknown\n");
|
|
20
|
+
}
|
|
1282
21
|
}
|
|
1283
22
|
function parseArgs(argv) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
23
|
+
const options = {
|
|
24
|
+
port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || "4318"),
|
|
25
|
+
host: process.env.AUTOTEL_DEVTOOLS_HOST || "127.0.0.1",
|
|
26
|
+
title: process.env.AUTOTEL_DEVTOOLS_TITLE
|
|
27
|
+
};
|
|
28
|
+
let portWasExplicit = false;
|
|
29
|
+
let positionalPortConsumed = false;
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
const next = argv[i + 1];
|
|
33
|
+
if (arg === "--help" || arg === "-h") {
|
|
34
|
+
printHelp();
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (arg === "--version" || arg === "-v") {
|
|
38
|
+
printVersion();
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if ((arg === "--port" || arg === "-p") && next) {
|
|
42
|
+
options.port = parsePort(next);
|
|
43
|
+
portWasExplicit = true;
|
|
44
|
+
i++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if ((arg === "--host" || arg === "-H") && next) {
|
|
48
|
+
options.host = next;
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if ((arg === "--title" || arg === "-t") && next) {
|
|
53
|
+
options.title = next;
|
|
54
|
+
i++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (/^\d+$/.test(arg) && !positionalPortConsumed) {
|
|
58
|
+
if (!portWasExplicit) options.port = parsePort(arg);
|
|
59
|
+
positionalPortConsumed = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return options;
|
|
1325
64
|
}
|
|
1326
65
|
function parsePort(value) {
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
return n;
|
|
66
|
+
const n = Number(value);
|
|
67
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
68
|
+
process.stderr.write(`[autotel-devtools] invalid port: ${value}\n`);
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
return n;
|
|
1334
72
|
}
|
|
1335
73
|
async function main() {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
);
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
`);
|
|
1369
|
-
|
|
1370
|
-
`);
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
`);
|
|
1378
|
-
process.stdout.write(
|
|
1379
|
-
` Embed in your app \u2014 paste into your HTML; a floating panel appears automatically:
|
|
1380
|
-
`
|
|
1381
|
-
);
|
|
1382
|
-
process.stdout.write(` <script src="${uiBase}/widget.js"></script>
|
|
1383
|
-
|
|
1384
|
-
`);
|
|
1385
|
-
process.stdout.write(
|
|
1386
|
-
` full screen instead: <script src="${uiBase}/widget.js?mode=fullpage"></script>
|
|
1387
|
-
`
|
|
1388
|
-
);
|
|
1389
|
-
process.stdout.write(
|
|
1390
|
-
` choose where it goes: add <autotel-devtools></autotel-devtools> to your markup
|
|
1391
|
-
|
|
1392
|
-
`
|
|
1393
|
-
);
|
|
1394
|
-
process.stdout.write(` Or point any OTLP exporter at this receiver:
|
|
1395
|
-
`);
|
|
1396
|
-
process.stdout.write(` OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
|
1397
|
-
`);
|
|
1398
|
-
process.stdout.write(` OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}
|
|
1399
|
-
|
|
1400
|
-
`);
|
|
1401
|
-
process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces
|
|
1402
|
-
|
|
1403
|
-
`);
|
|
1404
|
-
for (const w of warnings) {
|
|
1405
|
-
process.stdout.write(` \u26A0 ${w}
|
|
1406
|
-
`);
|
|
1407
|
-
}
|
|
1408
|
-
if (warnings.length > 0) process.stdout.write("\n");
|
|
1409
|
-
const shutdown = () => {
|
|
1410
|
-
Promise.all([wsServer.close(), listeners.closeSibling()]).then(
|
|
1411
|
-
() => process.exit(0)
|
|
1412
|
-
);
|
|
1413
|
-
};
|
|
1414
|
-
process.on("SIGINT", shutdown);
|
|
1415
|
-
process.on("SIGTERM", shutdown);
|
|
74
|
+
const options = parseArgs(process.argv.slice(2));
|
|
75
|
+
if (!options) process.exit(0);
|
|
76
|
+
const httpServer = (0, node_http.createServer)();
|
|
77
|
+
const loopbackOnly = require_http.hostHeaderIsLoopback(options.host);
|
|
78
|
+
const wsServer = new require_http.DevtoolsServer({
|
|
79
|
+
server: httpServer,
|
|
80
|
+
host: options.host,
|
|
81
|
+
verbose: true
|
|
82
|
+
});
|
|
83
|
+
require_http.attachDevtoolsRoutes(httpServer, wsServer, { loopbackOnly });
|
|
84
|
+
const listeners = require_listen.listenLoopbackDualStack({
|
|
85
|
+
primary: httpServer,
|
|
86
|
+
port: options.port,
|
|
87
|
+
host: options.host,
|
|
88
|
+
attachSecondary: (s) => require_http.attachDevtoolsRoutes(s, wsServer, { loopbackOnly })
|
|
89
|
+
});
|
|
90
|
+
const { addresses, warnings, port: boundPort } = await listeners.ready;
|
|
91
|
+
if (boundPort !== options.port) if (await require_http.probePortHolder(options.host, options.port) === "autotel-devtools") warnings.push(`another autotel-devtools is already running on port ${options.port}; this instance is on ${boundPort}. Use the existing one, or stop it and restart here.`);
|
|
92
|
+
else warnings.push(`port ${options.port} is held by another process that is NOT autotel-devtools. Anything exporting OTLP to :${options.port} is reaching that process, not this devtools. Point your exporter at :${boundPort}, or free :${options.port} and restart.`);
|
|
93
|
+
const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`;
|
|
94
|
+
const title = options.title || "autotel-devtools";
|
|
95
|
+
process.stdout.write(`\n ${title}\n\n`);
|
|
96
|
+
process.stdout.write(` Listening: ${addresses.join(" + ")}\n`);
|
|
97
|
+
process.stdout.write(` UI: ${uiBase} (open in a browser)\n`);
|
|
98
|
+
process.stdout.write(` OTLP: ${uiBase}/v1/traces\n`);
|
|
99
|
+
process.stdout.write(` WebSocket: ${uiBase.replace("http", "ws")}/ws\n\n`);
|
|
100
|
+
process.stdout.write(` Embed in your app — paste into your HTML; a floating panel appears automatically:\n`);
|
|
101
|
+
process.stdout.write(` <script src="${uiBase}/widget.js"><\/script>\n\n`);
|
|
102
|
+
process.stdout.write(` full screen instead: <script src="${uiBase}/widget.js?mode=fullpage"><\/script>\n`);
|
|
103
|
+
process.stdout.write(` choose where it goes: add <autotel-devtools></autotel-devtools> to your markup\n\n`);
|
|
104
|
+
process.stdout.write(` Or point any OTLP exporter at this receiver:\n`);
|
|
105
|
+
process.stdout.write(` OTEL_EXPORTER_OTLP_PROTOCOL=http/json\n`);
|
|
106
|
+
process.stdout.write(` OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}\n\n`);
|
|
107
|
+
process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces\n\n`);
|
|
108
|
+
for (const w of warnings) process.stdout.write(` ⚠ ${w}\n`);
|
|
109
|
+
if (warnings.length > 0) process.stdout.write("\n");
|
|
110
|
+
const shutdown = () => {
|
|
111
|
+
Promise.all([wsServer.close(), listeners.closeSibling()]).then(() => process.exit(0));
|
|
112
|
+
};
|
|
113
|
+
process.on("SIGINT", shutdown);
|
|
114
|
+
process.on("SIGTERM", shutdown);
|
|
1416
115
|
}
|
|
1417
116
|
main().catch((error) => {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
process.exit(1);
|
|
117
|
+
process.stderr.write(`[autotel-devtools] failed to start: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
118
|
+
process.exit(1);
|
|
1421
119
|
});
|
|
1422
|
-
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
1423
122
|
//# sourceMappingURL=cli.cjs.map
|