autotel-devtools 0.1.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/README.md +156 -0
- package/dist/cli.cjs +889 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +886 -0
- package/dist/cli.js.map +1 -0
- package/dist/error-aggregator-BkO0l8ak.d.ts +147 -0
- package/dist/error-aggregator-CtZmjm-k.d.cts +147 -0
- package/dist/exporter-qIQPDw29.d.cts +159 -0
- package/dist/exporter-qIQPDw29.d.ts +159 -0
- package/dist/index.cjs +1242 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1234 -0
- package/dist/index.js.map +1 -0
- package/dist/server/exporter.cjs +154 -0
- package/dist/server/exporter.cjs.map +1 -0
- package/dist/server/exporter.d.cts +4 -0
- package/dist/server/exporter.d.ts +4 -0
- package/dist/server/exporter.js +152 -0
- package/dist/server/exporter.js.map +1 -0
- package/dist/server/index.cjs +1237 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +38 -0
- package/dist/server/index.d.ts +38 -0
- package/dist/server/index.js +1222 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/log-exporter.cjs +111 -0
- package/dist/server/log-exporter.cjs.map +1 -0
- package/dist/server/log-exporter.d.cts +50 -0
- package/dist/server/log-exporter.d.ts +50 -0
- package/dist/server/log-exporter.js +109 -0
- package/dist/server/log-exporter.js.map +1 -0
- package/dist/server/remote-exporter.cjs +219 -0
- package/dist/server/remote-exporter.cjs.map +1 -0
- package/dist/server/remote-exporter.d.cts +87 -0
- package/dist/server/remote-exporter.d.ts +87 -0
- package/dist/server/remote-exporter.js +217 -0
- package/dist/server/remote-exporter.js.map +1 -0
- package/dist/widget.global.js +2 -0
- package/package.json +96 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { dirname, resolve } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
|
+
|
|
8
|
+
// src/server/error-aggregator.ts
|
|
9
|
+
var ErrorAggregator = class {
|
|
10
|
+
errorGroups = /* @__PURE__ */ new Map();
|
|
11
|
+
options;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
maxGroups: options.maxGroups ?? 100,
|
|
15
|
+
maxAffectedTraces: options.maxAffectedTraces ?? 10,
|
|
16
|
+
maxAffectedSpans: options.maxAffectedSpans ?? 5,
|
|
17
|
+
stackFramesForFingerprint: options.stackFramesForFingerprint ?? 5
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Add an error occurrence to the aggregator
|
|
22
|
+
*/
|
|
23
|
+
addError(occurrence) {
|
|
24
|
+
const fingerprint = this.generateFingerprint(occurrence);
|
|
25
|
+
const existing = this.errorGroups.get(fingerprint);
|
|
26
|
+
if (existing) {
|
|
27
|
+
existing.count++;
|
|
28
|
+
existing.lastSeen = occurrence.timestamp;
|
|
29
|
+
if (!existing.affectedTraces.includes(occurrence.traceId)) {
|
|
30
|
+
existing.affectedTraces.push(occurrence.traceId);
|
|
31
|
+
if (existing.affectedTraces.length > this.options.maxAffectedTraces) {
|
|
32
|
+
existing.affectedTraces.shift();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!existing.affectedSpans.includes(occurrence.spanName)) {
|
|
36
|
+
existing.affectedSpans.push(occurrence.spanName);
|
|
37
|
+
if (existing.affectedSpans.length > this.options.maxAffectedSpans) {
|
|
38
|
+
existing.affectedSpans.shift();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
const newGroup = {
|
|
44
|
+
fingerprint,
|
|
45
|
+
type: occurrence.error.type,
|
|
46
|
+
message: occurrence.error.message,
|
|
47
|
+
stackTrace: this.normalizeStackTrace(occurrence.error.stackTrace),
|
|
48
|
+
count: 1,
|
|
49
|
+
firstSeen: occurrence.timestamp,
|
|
50
|
+
lastSeen: occurrence.timestamp,
|
|
51
|
+
affectedTraces: [occurrence.traceId],
|
|
52
|
+
affectedSpans: [occurrence.spanName],
|
|
53
|
+
service: occurrence.service,
|
|
54
|
+
attributes: occurrence.attributes
|
|
55
|
+
};
|
|
56
|
+
if (this.errorGroups.size >= this.options.maxGroups) {
|
|
57
|
+
this.evictOldestGroup();
|
|
58
|
+
}
|
|
59
|
+
this.errorGroups.set(fingerprint, newGroup);
|
|
60
|
+
return newGroup;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Extract errors from a trace and add them to the aggregator
|
|
64
|
+
*/
|
|
65
|
+
addErrorsFromTrace(trace) {
|
|
66
|
+
const addedGroups = [];
|
|
67
|
+
for (const span of trace.spans) {
|
|
68
|
+
if (span.status.code === "ERROR") {
|
|
69
|
+
const occurrence = this.extractErrorFromSpan(span, trace);
|
|
70
|
+
if (occurrence) {
|
|
71
|
+
const group = this.addError(occurrence);
|
|
72
|
+
addedGroups.push(group);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return addedGroups;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Extract error occurrence from a span
|
|
80
|
+
*/
|
|
81
|
+
extractErrorFromSpan(span, trace) {
|
|
82
|
+
const exceptionEvent = span.events?.find((e) => e.name === "exception");
|
|
83
|
+
const errorType = span.attributes["exception.type"] || span.attributes["error.type"] || exceptionEvent?.attributes?.["exception.type"] || "Error";
|
|
84
|
+
const errorMessage = span.status.message || span.attributes["exception.message"] || span.attributes["error.message"] || "Unknown error";
|
|
85
|
+
const stackTrace = span.attributes["exception.stacktrace"] || span.attributes["exception.stack"] || this.extractStackFromEvents(span);
|
|
86
|
+
return {
|
|
87
|
+
traceId: trace.traceId,
|
|
88
|
+
spanId: span.spanId,
|
|
89
|
+
spanName: span.name,
|
|
90
|
+
service: trace.service,
|
|
91
|
+
timestamp: span.endTime,
|
|
92
|
+
error: {
|
|
93
|
+
type: errorType,
|
|
94
|
+
message: errorMessage,
|
|
95
|
+
stackTrace
|
|
96
|
+
},
|
|
97
|
+
attributes: this.extractRelevantAttributes(span.attributes)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extract stack trace from span events (exception events)
|
|
102
|
+
*/
|
|
103
|
+
extractStackFromEvents(span) {
|
|
104
|
+
if (!span.events) return void 0;
|
|
105
|
+
const exceptionEvent = span.events.find((e) => e.name === "exception");
|
|
106
|
+
if (exceptionEvent?.attributes) {
|
|
107
|
+
return exceptionEvent.attributes["exception.stacktrace"] || exceptionEvent.attributes["exception.stack"];
|
|
108
|
+
}
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Extract relevant attributes for error context
|
|
113
|
+
*/
|
|
114
|
+
extractRelevantAttributes(attributes) {
|
|
115
|
+
const relevant = {};
|
|
116
|
+
const keepKeys = [
|
|
117
|
+
"http.method",
|
|
118
|
+
"http.url",
|
|
119
|
+
"http.route",
|
|
120
|
+
"http.status_code",
|
|
121
|
+
"db.system",
|
|
122
|
+
"db.operation",
|
|
123
|
+
"rpc.method",
|
|
124
|
+
"rpc.service",
|
|
125
|
+
"code.function",
|
|
126
|
+
"code.filepath",
|
|
127
|
+
"user.id",
|
|
128
|
+
"operation.name"
|
|
129
|
+
];
|
|
130
|
+
for (const key of keepKeys) {
|
|
131
|
+
if (key in attributes) {
|
|
132
|
+
relevant[key] = attributes[key];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return relevant;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Generate a fingerprint for error grouping
|
|
139
|
+
*
|
|
140
|
+
* Uses error type + first N stack frames (normalized)
|
|
141
|
+
*/
|
|
142
|
+
generateFingerprint(occurrence) {
|
|
143
|
+
const parts = [occurrence.error.type];
|
|
144
|
+
if (occurrence.error.stackTrace) {
|
|
145
|
+
const frames = this.extractStackFrames(
|
|
146
|
+
occurrence.error.stackTrace,
|
|
147
|
+
this.options.stackFramesForFingerprint
|
|
148
|
+
);
|
|
149
|
+
parts.push(...frames);
|
|
150
|
+
} else {
|
|
151
|
+
parts.push(this.normalizeMessage(occurrence.error.message));
|
|
152
|
+
}
|
|
153
|
+
return this.simpleHash(parts.join("|"));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Extract and normalize stack frames from a stack trace
|
|
157
|
+
*/
|
|
158
|
+
extractStackFrames(stackTrace, count) {
|
|
159
|
+
const lines = stackTrace.split("\n");
|
|
160
|
+
const frames = [];
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
if (frames.length >= count) break;
|
|
163
|
+
const trimmed = line.trim();
|
|
164
|
+
const nodeMatch = trimmed.match(/^at\s+(.+?)\s+\((.+?):(\d+):\d+\)$/);
|
|
165
|
+
if (nodeMatch) {
|
|
166
|
+
frames.push(`${nodeMatch[1]}@${this.normalizeFilePath(nodeMatch[2])}`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const anonMatch = trimmed.match(/^at\s+(.+?):(\d+):\d+$/);
|
|
170
|
+
if (anonMatch) {
|
|
171
|
+
frames.push(`anonymous@${this.normalizeFilePath(anonMatch[1])}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const browserMatch = trimmed.match(/^(.+?)@(.+?):(\d+):\d+$/);
|
|
175
|
+
if (browserMatch) {
|
|
176
|
+
frames.push(
|
|
177
|
+
`${browserMatch[1]}@${this.normalizeFilePath(browserMatch[2])}`
|
|
178
|
+
);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return frames;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Normalize file path by removing absolute path prefixes and node_modules paths
|
|
186
|
+
*/
|
|
187
|
+
normalizeFilePath(filePath) {
|
|
188
|
+
const nodeModulesMatch = filePath.match(
|
|
189
|
+
/node_modules\/(@[^/]+\/[^/]+|[^/]+)/
|
|
190
|
+
);
|
|
191
|
+
if (nodeModulesMatch) {
|
|
192
|
+
return `[npm]/${nodeModulesMatch[1]}`;
|
|
193
|
+
}
|
|
194
|
+
return filePath.replace(/^.*?\/src\//, "src/").replace(/^.*?\/dist\//, "dist/").replace(/^.*?\/lib\//, "lib/").replace(/^file:\/\//, "");
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Normalize error message by removing dynamic parts
|
|
198
|
+
*/
|
|
199
|
+
normalizeMessage(message) {
|
|
200
|
+
return message.replaceAll(
|
|
201
|
+
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
|
|
202
|
+
"[UUID]"
|
|
203
|
+
).replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\b\d+\b/g, "[N]").replaceAll(/"[^"]*"/g, '"[STR]"').replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Normalize stack trace for display
|
|
207
|
+
*/
|
|
208
|
+
normalizeStackTrace(stackTrace) {
|
|
209
|
+
if (!stackTrace) return void 0;
|
|
210
|
+
const lines = stackTrace.split("\n").slice(0, 10);
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Simple hash function for fingerprinting
|
|
215
|
+
*/
|
|
216
|
+
simpleHash(str) {
|
|
217
|
+
let hash = 0;
|
|
218
|
+
for (let i = 0; i < str.length; i++) {
|
|
219
|
+
const char = str.charCodeAt(i);
|
|
220
|
+
hash = (hash << 5) - hash + char;
|
|
221
|
+
hash = hash & hash;
|
|
222
|
+
}
|
|
223
|
+
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Evict the oldest error group
|
|
227
|
+
*/
|
|
228
|
+
evictOldestGroup() {
|
|
229
|
+
let oldest = null;
|
|
230
|
+
for (const [fingerprint, group] of this.errorGroups) {
|
|
231
|
+
if (!oldest || group.lastSeen < oldest.lastSeen) {
|
|
232
|
+
oldest = { fingerprint, lastSeen: group.lastSeen };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (oldest) {
|
|
236
|
+
this.errorGroups.delete(oldest.fingerprint);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get all error groups, sorted by most recent
|
|
241
|
+
*/
|
|
242
|
+
getErrorGroups() {
|
|
243
|
+
return [...this.errorGroups.values()].sort(
|
|
244
|
+
(a, b) => b.lastSeen - a.lastSeen
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get error groups sorted by count (most frequent first)
|
|
249
|
+
*/
|
|
250
|
+
getErrorGroupsByFrequency() {
|
|
251
|
+
return [...this.errorGroups.values()].sort(
|
|
252
|
+
(a, b) => b.count - a.count
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get a specific error group by fingerprint
|
|
257
|
+
*/
|
|
258
|
+
getErrorGroup(fingerprint) {
|
|
259
|
+
return this.errorGroups.get(fingerprint);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get error groups for a specific service
|
|
263
|
+
*/
|
|
264
|
+
getErrorGroupsByService(service) {
|
|
265
|
+
return this.getErrorGroups().filter((g) => g.service === service);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get total error count across all groups
|
|
269
|
+
*/
|
|
270
|
+
getTotalErrorCount() {
|
|
271
|
+
let total = 0;
|
|
272
|
+
for (const group of this.errorGroups.values()) {
|
|
273
|
+
total += group.count;
|
|
274
|
+
}
|
|
275
|
+
return total;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get error statistics
|
|
279
|
+
*/
|
|
280
|
+
getStats() {
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
const oneHourAgo = now - 60 * 60 * 1e3;
|
|
283
|
+
let recentErrors = 0;
|
|
284
|
+
const typeCount = /* @__PURE__ */ new Map();
|
|
285
|
+
for (const group of this.errorGroups.values()) {
|
|
286
|
+
if (group.lastSeen > oneHourAgo) {
|
|
287
|
+
recentErrors += group.count;
|
|
288
|
+
}
|
|
289
|
+
typeCount.set(group.type, (typeCount.get(group.type) || 0) + group.count);
|
|
290
|
+
}
|
|
291
|
+
const topErrorTypes = [...typeCount.entries()].map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count).slice(0, 5);
|
|
292
|
+
return {
|
|
293
|
+
totalGroups: this.errorGroups.size,
|
|
294
|
+
totalErrors: this.getTotalErrorCount(),
|
|
295
|
+
recentErrors,
|
|
296
|
+
topErrorTypes
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Clear all error groups
|
|
301
|
+
*/
|
|
302
|
+
clear() {
|
|
303
|
+
this.errorGroups.clear();
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Clear old error groups (not seen in given time window)
|
|
307
|
+
*/
|
|
308
|
+
clearOlderThan(maxAgeMs) {
|
|
309
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
310
|
+
let cleared = 0;
|
|
311
|
+
for (const [fingerprint, group] of this.errorGroups) {
|
|
312
|
+
if (group.lastSeen < cutoff) {
|
|
313
|
+
this.errorGroups.delete(fingerprint);
|
|
314
|
+
cleared++;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return cleared;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/server/telemetry-limits.ts
|
|
322
|
+
var defaultLimit = 100;
|
|
323
|
+
function parseLimit(value) {
|
|
324
|
+
if (!value) return void 0;
|
|
325
|
+
const parsed = Number.parseInt(value, 10);
|
|
326
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
327
|
+
}
|
|
328
|
+
function resolveTelemetryLimits(args = {}) {
|
|
329
|
+
const env = args.env ?? process.env;
|
|
330
|
+
const fallback = args.maxHistory ?? defaultLimit;
|
|
331
|
+
return {
|
|
332
|
+
maxTraceCount: args.maxTraceCount ?? parseLimit(env.AUTOTEL_MAX_TRACE_COUNT) ?? fallback,
|
|
333
|
+
maxLogCount: args.maxLogCount ?? parseLimit(env.AUTOTEL_MAX_LOG_COUNT) ?? fallback,
|
|
334
|
+
maxMetricCount: args.maxMetricCount ?? parseLimit(env.AUTOTEL_MAX_METRIC_COUNT) ?? fallback
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function appendWithLimit(items, item, limit) {
|
|
338
|
+
if (limit <= 0) return [];
|
|
339
|
+
const next = [...items, item];
|
|
340
|
+
return next.length > limit ? next.slice(next.length - limit) : next;
|
|
341
|
+
}
|
|
342
|
+
function appendManyWithLimit(items, incoming, limit) {
|
|
343
|
+
if (limit <= 0 || incoming.length === 0) return limit <= 0 ? [] : items;
|
|
344
|
+
const next = [...items, ...incoming];
|
|
345
|
+
return next.length > limit ? next.slice(next.length - limit) : next;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/server/server.ts
|
|
349
|
+
var DevtoolsServer = class {
|
|
350
|
+
wss;
|
|
351
|
+
clients = /* @__PURE__ */ new Set();
|
|
352
|
+
httpServer;
|
|
353
|
+
traces = [];
|
|
354
|
+
logs = [];
|
|
355
|
+
metrics = [];
|
|
356
|
+
errorAggregator = new ErrorAggregator();
|
|
357
|
+
limits;
|
|
358
|
+
verbose;
|
|
359
|
+
_port;
|
|
360
|
+
constructor(options = {}) {
|
|
361
|
+
this.limits = resolveTelemetryLimits(options);
|
|
362
|
+
this.verbose = options.verbose ?? false;
|
|
363
|
+
this._port = options.port ?? 4318;
|
|
364
|
+
this.httpServer = options.server ?? createServer();
|
|
365
|
+
this.wss = new WebSocketServer({ server: this.httpServer, path: options.path ?? "/ws" });
|
|
366
|
+
this.wss.on("connection", (ws) => {
|
|
367
|
+
this.clients.add(ws);
|
|
368
|
+
this.log(`Client connected (${this.clients.size} total)`);
|
|
369
|
+
const data = this.getCurrentData();
|
|
370
|
+
if (data.traces.length > 0 || data.logs.length > 0 || data.errors.length > 0) {
|
|
371
|
+
ws.send(JSON.stringify(data));
|
|
372
|
+
}
|
|
373
|
+
ws.on("close", () => {
|
|
374
|
+
this.clients.delete(ws);
|
|
375
|
+
this.log(`Client disconnected (${this.clients.size} total)`);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
if (!options.server) {
|
|
379
|
+
this.httpServer.listen(this._port, () => {
|
|
380
|
+
const addr = this.httpServer.address();
|
|
381
|
+
if (addr && typeof addr === "object") this._port = addr.port;
|
|
382
|
+
this.log(`WebSocket server listening on port ${this._port}`);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
get port() {
|
|
387
|
+
const addr = this.httpServer.address();
|
|
388
|
+
if (addr && typeof addr === "object") return addr.port;
|
|
389
|
+
return this._port;
|
|
390
|
+
}
|
|
391
|
+
get clientCount() {
|
|
392
|
+
return this.clients.size;
|
|
393
|
+
}
|
|
394
|
+
addTrace(trace) {
|
|
395
|
+
const existing = this.traces.find((t) => t.traceId === trace.traceId);
|
|
396
|
+
if (existing) {
|
|
397
|
+
const existingSpanIds = new Set(existing.spans.map((s) => s.spanId));
|
|
398
|
+
for (const span of trace.spans) {
|
|
399
|
+
if (!existingSpanIds.has(span.spanId)) {
|
|
400
|
+
existing.spans.push(span);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
existing.startTime = Math.min(existing.startTime, trace.startTime);
|
|
404
|
+
existing.endTime = Math.max(existing.endTime, trace.endTime);
|
|
405
|
+
existing.duration = existing.endTime - existing.startTime;
|
|
406
|
+
if (trace.status === "ERROR") existing.status = "ERROR";
|
|
407
|
+
} else {
|
|
408
|
+
this.traces = appendWithLimit(
|
|
409
|
+
this.traces,
|
|
410
|
+
trace,
|
|
411
|
+
this.limits.maxTraceCount
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
this.errorAggregator.addErrorsFromTrace(trace);
|
|
415
|
+
this.broadcast({ traces: [trace], metrics: [], logs: [], errors: this.errorAggregator.getErrorGroups() });
|
|
416
|
+
}
|
|
417
|
+
addTraces(traces) {
|
|
418
|
+
for (const trace of traces) this.addTrace(trace);
|
|
419
|
+
}
|
|
420
|
+
addLog(log) {
|
|
421
|
+
this.logs = appendWithLimit(this.logs, log, this.limits.maxLogCount);
|
|
422
|
+
this.broadcast({ traces: [], metrics: [], logs: [log], errors: [] });
|
|
423
|
+
}
|
|
424
|
+
addLogs(logs) {
|
|
425
|
+
this.logs = appendManyWithLimit(this.logs, logs, this.limits.maxLogCount);
|
|
426
|
+
this.broadcast({ traces: [], metrics: [], logs, errors: [] });
|
|
427
|
+
}
|
|
428
|
+
addMetric(metric) {
|
|
429
|
+
this.metrics = appendWithLimit(
|
|
430
|
+
this.metrics,
|
|
431
|
+
metric,
|
|
432
|
+
this.limits.maxMetricCount
|
|
433
|
+
);
|
|
434
|
+
this.broadcast({ traces: [], metrics: [metric], logs: [], errors: [] });
|
|
435
|
+
}
|
|
436
|
+
getCurrentData() {
|
|
437
|
+
return {
|
|
438
|
+
traces: this.traces,
|
|
439
|
+
metrics: this.metrics,
|
|
440
|
+
logs: this.logs,
|
|
441
|
+
errors: this.errorAggregator.getErrorGroups()
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
clearData() {
|
|
445
|
+
this.traces = [];
|
|
446
|
+
this.logs = [];
|
|
447
|
+
this.metrics = [];
|
|
448
|
+
this.errorAggregator.clear();
|
|
449
|
+
}
|
|
450
|
+
broadcast(data) {
|
|
451
|
+
const msg = JSON.stringify(data);
|
|
452
|
+
for (const client of this.clients) {
|
|
453
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
454
|
+
client.send(msg);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
log(message) {
|
|
459
|
+
if (this.verbose) console.log(`[autotel-devtools] ${message}`);
|
|
460
|
+
}
|
|
461
|
+
async close() {
|
|
462
|
+
for (const client of this.clients) client.close();
|
|
463
|
+
this.clients.clear();
|
|
464
|
+
this.wss.close();
|
|
465
|
+
await new Promise((resolve3) => this.httpServer.close(() => resolve3()));
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/server/resource-utils.ts
|
|
470
|
+
function getResourceName(resource, fallback = "unknown") {
|
|
471
|
+
if (!resource) return fallback;
|
|
472
|
+
const candidates = [
|
|
473
|
+
resource["service.name"],
|
|
474
|
+
resource["service.namespace"],
|
|
475
|
+
resource["deployment.environment.name"],
|
|
476
|
+
resource["host.name"],
|
|
477
|
+
resource["container.name"],
|
|
478
|
+
resource["process.executable.name"]
|
|
479
|
+
];
|
|
480
|
+
for (const candidate of candidates) {
|
|
481
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
482
|
+
return candidate;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return fallback;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/server/otlp.ts
|
|
489
|
+
function resolveOtlpValue(v) {
|
|
490
|
+
if (!v) return void 0;
|
|
491
|
+
if (v.stringValue !== void 0) return v.stringValue;
|
|
492
|
+
if (v.boolValue !== void 0) return v.boolValue;
|
|
493
|
+
if (v.intValue !== void 0) return typeof v.intValue === "string" ? Number(v.intValue) : v.intValue;
|
|
494
|
+
if (v.doubleValue !== void 0) return v.doubleValue;
|
|
495
|
+
if (v.bytesValue !== void 0) return v.bytesValue;
|
|
496
|
+
if (v.arrayValue?.values) return v.arrayValue.values.map(resolveOtlpValue);
|
|
497
|
+
if (v.kvlistValue?.values) return flattenAttributes(v.kvlistValue.values);
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
500
|
+
function flattenAttributes(attrs) {
|
|
501
|
+
const out = {};
|
|
502
|
+
if (!attrs) return out;
|
|
503
|
+
for (const { key, value } of attrs) {
|
|
504
|
+
out[key] = resolveOtlpValue(value);
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
function nanoToMs(nano) {
|
|
509
|
+
if (!nano) return 0;
|
|
510
|
+
return Number(BigInt(nano) / 1000000n);
|
|
511
|
+
}
|
|
512
|
+
var SPAN_KIND_MAP = {
|
|
513
|
+
0: "INTERNAL",
|
|
514
|
+
1: "INTERNAL",
|
|
515
|
+
2: "SERVER",
|
|
516
|
+
3: "CLIENT",
|
|
517
|
+
4: "PRODUCER",
|
|
518
|
+
5: "CONSUMER",
|
|
519
|
+
SPAN_KIND_INTERNAL: "INTERNAL",
|
|
520
|
+
SPAN_KIND_SERVER: "SERVER",
|
|
521
|
+
SPAN_KIND_CLIENT: "CLIENT",
|
|
522
|
+
SPAN_KIND_PRODUCER: "PRODUCER",
|
|
523
|
+
SPAN_KIND_CONSUMER: "CONSUMER"
|
|
524
|
+
};
|
|
525
|
+
function normalizeHexId(id) {
|
|
526
|
+
if (!id) return "";
|
|
527
|
+
const isBase64Like = /^[A-Za-z0-9+/=]+$/.test(id) && !/^[0-9a-f]+$/i.test(id);
|
|
528
|
+
const isLikelyBase64Id = isBase64Like && (id.length === 24 || id.length === 28 || id.length === 44 || id.length === 48);
|
|
529
|
+
if (isLikelyBase64Id) {
|
|
530
|
+
try {
|
|
531
|
+
const bytes = Buffer.from(id, "base64");
|
|
532
|
+
return bytes.toString("hex");
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return id;
|
|
537
|
+
}
|
|
538
|
+
function parseOtlpTraces(payload) {
|
|
539
|
+
if (!payload || typeof payload !== "object") return [];
|
|
540
|
+
const { resourceSpans } = payload;
|
|
541
|
+
if (!Array.isArray(resourceSpans) || resourceSpans.length === 0) return [];
|
|
542
|
+
const traceMap = /* @__PURE__ */ new Map();
|
|
543
|
+
for (const rs of resourceSpans) {
|
|
544
|
+
const resourceAttrs = flattenAttributes(rs.resource?.attributes);
|
|
545
|
+
const service = String(resourceAttrs["service.name"] || "unknown");
|
|
546
|
+
const scopeSpans = rs.scopeSpans || [];
|
|
547
|
+
for (const ss of scopeSpans) {
|
|
548
|
+
for (const span of ss.spans || []) {
|
|
549
|
+
const traceId = normalizeHexId(span.traceId);
|
|
550
|
+
if (!traceId) continue;
|
|
551
|
+
const startMs = nanoToMs(span.startTimeUnixNano);
|
|
552
|
+
const endMs = nanoToMs(span.endTimeUnixNano);
|
|
553
|
+
const statusCode = span.status?.code;
|
|
554
|
+
let status = "UNSET";
|
|
555
|
+
if (statusCode === 1 || statusCode === "STATUS_CODE_OK") status = "OK";
|
|
556
|
+
if (statusCode === 2 || statusCode === "STATUS_CODE_ERROR") status = "ERROR";
|
|
557
|
+
const spanData = {
|
|
558
|
+
traceId,
|
|
559
|
+
spanId: normalizeHexId(span.spanId),
|
|
560
|
+
parentSpanId: normalizeHexId(span.parentSpanId) || void 0,
|
|
561
|
+
name: span.name || "unknown",
|
|
562
|
+
kind: SPAN_KIND_MAP[span.kind ?? 0] || "INTERNAL",
|
|
563
|
+
startTime: startMs,
|
|
564
|
+
endTime: endMs,
|
|
565
|
+
duration: endMs - startMs,
|
|
566
|
+
attributes: { ...resourceAttrs, ...flattenAttributes(span.attributes) },
|
|
567
|
+
status: { code: status, message: span.status?.message },
|
|
568
|
+
events: (span.events || []).map((e) => ({
|
|
569
|
+
name: e.name || "",
|
|
570
|
+
timestamp: nanoToMs(e.timeUnixNano),
|
|
571
|
+
attributes: flattenAttributes(e.attributes)
|
|
572
|
+
}))
|
|
573
|
+
};
|
|
574
|
+
const existing = traceMap.get(traceId);
|
|
575
|
+
if (existing) {
|
|
576
|
+
existing.spans.push(spanData);
|
|
577
|
+
} else {
|
|
578
|
+
traceMap.set(traceId, { spans: [spanData], service });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const traces = [];
|
|
584
|
+
for (const [traceId, { spans, service }] of traceMap) {
|
|
585
|
+
const sorted = spans.sort((a, b) => a.startTime - b.startTime);
|
|
586
|
+
const rootSpan = sorted.find((s) => !s.parentSpanId) || sorted[0];
|
|
587
|
+
const startTime = Math.min(...sorted.map((s) => s.startTime));
|
|
588
|
+
const endTime = Math.max(...sorted.map((s) => s.endTime));
|
|
589
|
+
const hasError = sorted.some((s) => s.status.code === "ERROR");
|
|
590
|
+
traces.push({
|
|
591
|
+
traceId,
|
|
592
|
+
correlationId: traceId.slice(0, 16),
|
|
593
|
+
rootSpan,
|
|
594
|
+
spans: sorted,
|
|
595
|
+
startTime,
|
|
596
|
+
endTime,
|
|
597
|
+
duration: endTime - startTime,
|
|
598
|
+
status: hasError ? "ERROR" : "OK",
|
|
599
|
+
service
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
return traces;
|
|
603
|
+
}
|
|
604
|
+
function parseOtlpLogs(payload) {
|
|
605
|
+
if (!payload || typeof payload !== "object") return [];
|
|
606
|
+
const { resourceLogs } = payload;
|
|
607
|
+
if (!Array.isArray(resourceLogs)) return [];
|
|
608
|
+
const logs = [];
|
|
609
|
+
for (const rl of resourceLogs) {
|
|
610
|
+
const resourceAttrs = flattenAttributes(rl.resource?.attributes);
|
|
611
|
+
for (const sl of rl.scopeLogs || []) {
|
|
612
|
+
for (const rec of sl.logRecords || []) {
|
|
613
|
+
const timestamp = nanoToMs(rec.timeUnixNano || rec.observedTimeUnixNano);
|
|
614
|
+
const traceId = normalizeHexId(rec.traceId) || void 0;
|
|
615
|
+
const spanId = normalizeHexId(rec.spanId) || void 0;
|
|
616
|
+
const body = rec.body ? resolveOtlpValue(rec.body) : "";
|
|
617
|
+
logs.push({
|
|
618
|
+
id: `${traceId || "no-trace"}:${spanId || "no-span"}:${timestamp}:${rec.severityNumber || 0}`,
|
|
619
|
+
traceId,
|
|
620
|
+
spanId,
|
|
621
|
+
resourceName: getResourceName(resourceAttrs),
|
|
622
|
+
severityText: rec.severityText,
|
|
623
|
+
severityNumber: rec.severityNumber,
|
|
624
|
+
body: typeof body === "string" ? body : body,
|
|
625
|
+
timestamp,
|
|
626
|
+
attributes: flattenAttributes(rec.attributes),
|
|
627
|
+
resource: resourceAttrs
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return logs;
|
|
633
|
+
}
|
|
634
|
+
function countOtlpMetrics(payload) {
|
|
635
|
+
if (!payload || typeof payload !== "object") return 0;
|
|
636
|
+
const { resourceMetrics } = payload;
|
|
637
|
+
if (!Array.isArray(resourceMetrics)) return 0;
|
|
638
|
+
let count = 0;
|
|
639
|
+
for (const rm of resourceMetrics) {
|
|
640
|
+
for (const sm of rm.scopeMetrics || []) {
|
|
641
|
+
count += (sm.metrics || []).length;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return count;
|
|
645
|
+
}
|
|
646
|
+
async function readJsonBody(req) {
|
|
647
|
+
return new Promise((resolve3, reject) => {
|
|
648
|
+
const chunks = [];
|
|
649
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
650
|
+
req.on("end", () => {
|
|
651
|
+
try {
|
|
652
|
+
resolve3(JSON.parse(Buffer.concat(chunks).toString()));
|
|
653
|
+
} catch {
|
|
654
|
+
reject(new Error("Invalid JSON"));
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
req.on("error", reject);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function sendJson(res, status, data) {
|
|
661
|
+
const body = JSON.stringify(data);
|
|
662
|
+
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
|
|
663
|
+
res.end(body);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/server/http.ts
|
|
667
|
+
function findPackageRoot() {
|
|
668
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
669
|
+
for (let i = 0; i < 5; i++) {
|
|
670
|
+
if (existsSync(resolve(dir, "package.json"))) return dir;
|
|
671
|
+
dir = dirname(dir);
|
|
672
|
+
}
|
|
673
|
+
return dir;
|
|
674
|
+
}
|
|
675
|
+
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>`;
|
|
676
|
+
var cachedWidgetJs = null;
|
|
677
|
+
function getWidgetJs() {
|
|
678
|
+
if (!cachedWidgetJs) {
|
|
679
|
+
const pkgRoot = findPackageRoot();
|
|
680
|
+
const candidates = [
|
|
681
|
+
resolve(pkgRoot, "dist", "widget.global.js"),
|
|
682
|
+
resolve(pkgRoot, "widget.global.js")
|
|
683
|
+
];
|
|
684
|
+
for (const candidate of candidates) {
|
|
685
|
+
try {
|
|
686
|
+
cachedWidgetJs = readFileSync(candidate, "utf8");
|
|
687
|
+
break;
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (!cachedWidgetJs) {
|
|
692
|
+
cachedWidgetJs = "// widget bundle not found - run pnpm build first";
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return cachedWidgetJs;
|
|
696
|
+
}
|
|
697
|
+
function attachDevtoolsRoutes(httpServer, devtools) {
|
|
698
|
+
httpServer.on("request", async (req, res) => {
|
|
699
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
700
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
701
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
702
|
+
if (req.method === "OPTIONS") {
|
|
703
|
+
res.writeHead(204);
|
|
704
|
+
res.end();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const url = req.url || "/";
|
|
708
|
+
if (req.method === "GET" && url === "/") {
|
|
709
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Length": Buffer.byteLength(FULLPAGE_HTML) });
|
|
710
|
+
res.end(FULLPAGE_HTML);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (req.method === "GET" && url.startsWith("/widget.js")) {
|
|
714
|
+
const js = getWidgetJs();
|
|
715
|
+
res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Content-Length": Buffer.byteLength(js) });
|
|
716
|
+
res.end(js);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (req.method === "GET" && url === "/healthz") {
|
|
720
|
+
sendJson(res, 200, { ok: true, clients: devtools.clientCount });
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (req.method === "POST" && url === "/v1/traces") {
|
|
724
|
+
try {
|
|
725
|
+
const payload = await readJsonBody(req);
|
|
726
|
+
const traces = parseOtlpTraces(payload);
|
|
727
|
+
devtools.addTraces(traces);
|
|
728
|
+
sendJson(res, 200, { acceptedTraces: traces.length });
|
|
729
|
+
} catch (e) {
|
|
730
|
+
sendJson(res, 400, { error: "Invalid OTLP JSON", message: e instanceof Error ? e.message : String(e) });
|
|
731
|
+
}
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (req.method === "POST" && url === "/v1/logs") {
|
|
735
|
+
try {
|
|
736
|
+
const payload = await readJsonBody(req);
|
|
737
|
+
const logs = parseOtlpLogs(payload);
|
|
738
|
+
devtools.addLogs(logs);
|
|
739
|
+
sendJson(res, 200, { acceptedLogs: logs.length });
|
|
740
|
+
} catch (e) {
|
|
741
|
+
sendJson(res, 400, { error: "Invalid OTLP JSON", message: e instanceof Error ? e.message : String(e) });
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (req.method === "POST" && url === "/v1/metrics") {
|
|
746
|
+
try {
|
|
747
|
+
const payload = await readJsonBody(req);
|
|
748
|
+
const count = countOtlpMetrics(payload);
|
|
749
|
+
sendJson(res, 200, { acceptedMetrics: count });
|
|
750
|
+
} catch (e) {
|
|
751
|
+
sendJson(res, 400, { error: "Invalid OTLP JSON", message: e instanceof Error ? e.message : String(e) });
|
|
752
|
+
}
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
sendJson(res, 404, { error: "Not found" });
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/cli.ts
|
|
760
|
+
function printHelp() {
|
|
761
|
+
process.stdout.write(
|
|
762
|
+
`autotel-devtools - Standalone OTLP receiver with web devtools UI
|
|
763
|
+
|
|
764
|
+
Usage: autotel-devtools [options]
|
|
765
|
+
|
|
766
|
+
Options:
|
|
767
|
+
-p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT)
|
|
768
|
+
-H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)
|
|
769
|
+
-t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)
|
|
770
|
+
Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT
|
|
771
|
+
-h, --help Show this help message
|
|
772
|
+
-v, --version Show version number
|
|
773
|
+
|
|
774
|
+
Endpoints:
|
|
775
|
+
GET / Web devtools UI (fullpage)
|
|
776
|
+
GET /widget.js Widget bundle (embed in your app)
|
|
777
|
+
POST /v1/traces Receive OTLP JSON trace data
|
|
778
|
+
POST /v1/logs Receive OTLP JSON log data
|
|
779
|
+
POST /v1/metrics Receive OTLP JSON metric data
|
|
780
|
+
WS /ws WebSocket stream for real-time updates
|
|
781
|
+
GET /healthz Health check
|
|
782
|
+
|
|
783
|
+
Examples:
|
|
784
|
+
npx autotel-devtools
|
|
785
|
+
npx autotel-devtools -p 4319
|
|
786
|
+
|
|
787
|
+
Then point your app:
|
|
788
|
+
OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js
|
|
789
|
+
|
|
790
|
+
View in browser:
|
|
791
|
+
http://localhost:4318
|
|
792
|
+
|
|
793
|
+
Or embed widget in your app:
|
|
794
|
+
<script src="http://localhost:4318/widget.js"></script>
|
|
795
|
+
|
|
796
|
+
`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
function printVersion() {
|
|
800
|
+
try {
|
|
801
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
802
|
+
const pkgPath = resolve(dir, "..", "package.json");
|
|
803
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
804
|
+
process.stdout.write(`${pkg.version}
|
|
805
|
+
`);
|
|
806
|
+
} catch {
|
|
807
|
+
process.stdout.write("unknown\n");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function parseArgs(argv) {
|
|
811
|
+
const options = {
|
|
812
|
+
port: Number(process.env.AUTOTEL_DEVTOOLS_PORT || 4318),
|
|
813
|
+
host: process.env.AUTOTEL_DEVTOOLS_HOST || "127.0.0.1",
|
|
814
|
+
title: process.env.AUTOTEL_DEVTOOLS_TITLE
|
|
815
|
+
};
|
|
816
|
+
for (let i = 0; i < argv.length; i++) {
|
|
817
|
+
const arg = argv[i];
|
|
818
|
+
const next = argv[i + 1];
|
|
819
|
+
if (arg === "--help" || arg === "-h") {
|
|
820
|
+
printHelp();
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
if (arg === "--version" || arg === "-v") {
|
|
824
|
+
printVersion();
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
if ((arg === "--port" || arg === "-p") && next) {
|
|
828
|
+
options.port = Number(next);
|
|
829
|
+
i++;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if ((arg === "--host" || arg === "-H") && next) {
|
|
833
|
+
options.host = next;
|
|
834
|
+
i++;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if ((arg === "--title" || arg === "-t") && next) {
|
|
838
|
+
options.title = next;
|
|
839
|
+
i++;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return options;
|
|
844
|
+
}
|
|
845
|
+
async function main() {
|
|
846
|
+
const options = parseArgs(process.argv.slice(2));
|
|
847
|
+
if (!options) {
|
|
848
|
+
process.exit(0);
|
|
849
|
+
}
|
|
850
|
+
const httpServer = createServer();
|
|
851
|
+
const wsServer = new DevtoolsServer({ server: httpServer, verbose: true });
|
|
852
|
+
attachDevtoolsRoutes(httpServer, wsServer);
|
|
853
|
+
httpServer.listen(options.port, options.host, () => {
|
|
854
|
+
const title = options.title || "autotel-devtools";
|
|
855
|
+
process.stdout.write(`
|
|
856
|
+
${title}
|
|
857
|
+
|
|
858
|
+
`);
|
|
859
|
+
process.stdout.write(` UI: http://${options.host}:${options.port}
|
|
860
|
+
`);
|
|
861
|
+
process.stdout.write(` Widget: <script src="http://${options.host}:${options.port}/widget.js"></script>
|
|
862
|
+
`);
|
|
863
|
+
process.stdout.write(` WebSocket: ws://${options.host}:${options.port}/ws
|
|
864
|
+
`);
|
|
865
|
+
process.stdout.write(` OTLP: http://${options.host}:${options.port}/v1/traces
|
|
866
|
+
|
|
867
|
+
`);
|
|
868
|
+
process.stdout.write(` Set OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
|
869
|
+
`);
|
|
870
|
+
process.stdout.write(` Set OTEL_EXPORTER_OTLP_ENDPOINT=http://${options.host}:${options.port}
|
|
871
|
+
|
|
872
|
+
`);
|
|
873
|
+
});
|
|
874
|
+
const shutdown = () => {
|
|
875
|
+
wsServer.close().then(() => process.exit(0));
|
|
876
|
+
};
|
|
877
|
+
process.on("SIGINT", shutdown);
|
|
878
|
+
process.on("SIGTERM", shutdown);
|
|
879
|
+
}
|
|
880
|
+
main().catch((error) => {
|
|
881
|
+
process.stderr.write(`[autotel-devtools] failed to start: ${error instanceof Error ? error.message : String(error)}
|
|
882
|
+
`);
|
|
883
|
+
process.exit(1);
|
|
884
|
+
});
|
|
885
|
+
//# sourceMappingURL=cli.js.map
|
|
886
|
+
//# sourceMappingURL=cli.js.map
|