autotel-devtools 5.1.0 → 6.0.1
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 +8 -3
- package/dist/cli.cjs +121 -26
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +121 -26
- package/dist/cli.js.map +1 -1
- package/dist/{error-aggregator-BkO0l8ak.d.ts → error-aggregator-CAk_pt3Z.d.ts} +1 -1
- package/dist/{error-aggregator-CtZmjm-k.d.cts → error-aggregator-CbLiuot4.d.cts} +1 -1
- package/dist/{exporter-qIQPDw29.d.cts → exporter-DjLkU621.d.cts} +17 -0
- package/dist/{exporter-qIQPDw29.d.ts → exporter-DjLkU621.d.ts} +17 -0
- package/dist/genai/index.d.cts +9 -0
- package/dist/genai/index.d.ts +9 -0
- package/dist/index.cjs +83 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +83 -15
- package/dist/index.js.map +1 -1
- package/dist/server/exporter.cjs +12 -1
- package/dist/server/exporter.cjs.map +1 -1
- package/dist/server/exporter.d.cts +1 -1
- package/dist/server/exporter.d.ts +1 -1
- package/dist/server/exporter.js +12 -1
- package/dist/server/exporter.js.map +1 -1
- package/dist/server/index.cjs +35 -3
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +3 -3
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +35 -3
- package/dist/server/index.js.map +1 -1
- package/dist/widget.global.js +13 -2
- package/package.json +12 -14
- package/skills/autotel-devtools/SKILL.md +5 -3
package/dist/cli.js
CHANGED
|
@@ -358,12 +358,17 @@ var DevtoolsServer = class {
|
|
|
358
358
|
limits;
|
|
359
359
|
verbose;
|
|
360
360
|
_port;
|
|
361
|
+
onData;
|
|
361
362
|
constructor(options = {}) {
|
|
362
363
|
this.limits = resolveTelemetryLimits(options);
|
|
363
364
|
this.verbose = options.verbose ?? false;
|
|
364
365
|
this._port = options.port ?? 4318;
|
|
366
|
+
this.onData = options.onData;
|
|
365
367
|
this.httpServer = options.server ?? createServer();
|
|
366
368
|
this.wss = new WebSocketServer({ server: this.httpServer, path: options.path ?? "/ws" });
|
|
369
|
+
this.wss.on("error", (err) => {
|
|
370
|
+
if (this.httpServer.listening) throw err;
|
|
371
|
+
});
|
|
367
372
|
this.wss.on("connection", (ws) => {
|
|
368
373
|
this.clients.add(ws);
|
|
369
374
|
this.log(`Client connected (${this.clients.size} total)`);
|
|
@@ -455,6 +460,12 @@ var DevtoolsServer = class {
|
|
|
455
460
|
client.send(msg);
|
|
456
461
|
}
|
|
457
462
|
}
|
|
463
|
+
if (this.onData) {
|
|
464
|
+
try {
|
|
465
|
+
this.onData(data);
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
}
|
|
458
469
|
}
|
|
459
470
|
log(message) {
|
|
460
471
|
if (this.verbose) console.log(`[autotel-devtools] ${message}`);
|
|
@@ -508,7 +519,10 @@ function flattenAttributes(attrs) {
|
|
|
508
519
|
}
|
|
509
520
|
function nanoToMs(nano) {
|
|
510
521
|
if (!nano) return 0;
|
|
511
|
-
|
|
522
|
+
const ns = BigInt(nano);
|
|
523
|
+
const ms = ns / 1000000n;
|
|
524
|
+
const remNs = ns % 1000000n;
|
|
525
|
+
return Number(ms) + Number(remNs) / 1e6;
|
|
512
526
|
}
|
|
513
527
|
var SPAN_KIND_MAP = {
|
|
514
528
|
0: "INTERNAL",
|
|
@@ -546,6 +560,7 @@ function parseOtlpTraces(payload) {
|
|
|
546
560
|
const service = String(resourceAttrs["service.name"] || "unknown");
|
|
547
561
|
const scopeSpans = rs.scopeSpans || [];
|
|
548
562
|
for (const ss of scopeSpans) {
|
|
563
|
+
const scope = ss.scope?.name ? { name: ss.scope.name, version: ss.scope.version || void 0 } : void 0;
|
|
549
564
|
for (const span of ss.spans || []) {
|
|
550
565
|
const traceId = normalizeHexId(span.traceId);
|
|
551
566
|
if (!traceId) continue;
|
|
@@ -570,7 +585,13 @@ function parseOtlpTraces(payload) {
|
|
|
570
585
|
name: e.name || "",
|
|
571
586
|
timestamp: nanoToMs(e.timeUnixNano),
|
|
572
587
|
attributes: flattenAttributes(e.attributes)
|
|
573
|
-
}))
|
|
588
|
+
})),
|
|
589
|
+
links: (span.links || []).map((l) => ({
|
|
590
|
+
traceId: normalizeHexId(l.traceId),
|
|
591
|
+
spanId: normalizeHexId(l.spanId),
|
|
592
|
+
attributes: flattenAttributes(l.attributes)
|
|
593
|
+
})),
|
|
594
|
+
scope
|
|
574
595
|
};
|
|
575
596
|
const existing = traceMap.get(traceId);
|
|
576
597
|
if (existing) {
|
|
@@ -1009,43 +1030,79 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
1009
1030
|
});
|
|
1010
1031
|
}
|
|
1011
1032
|
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1033
|
+
var DEFAULT_MAX_PORT_TRIES = 20;
|
|
1012
1034
|
function formatAddress(host, port) {
|
|
1013
1035
|
return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
1014
1036
|
}
|
|
1015
1037
|
function listenLoopbackDualStack(args) {
|
|
1016
|
-
const { primary, port, host, attachSecondary } = args;
|
|
1038
|
+
const { primary, port, host, attachSecondary, maxTries } = args;
|
|
1039
|
+
const maxAttempts = Math.max(1, maxTries ?? DEFAULT_MAX_PORT_TRIES);
|
|
1017
1040
|
let sibling;
|
|
1018
1041
|
const ready = new Promise(
|
|
1019
|
-
(resolve3) => {
|
|
1042
|
+
(resolve3, reject) => {
|
|
1020
1043
|
const addresses = [];
|
|
1021
1044
|
const warnings = [];
|
|
1022
1045
|
const primaryHost = host === "localhost" ? "127.0.0.1" : host;
|
|
1023
|
-
|
|
1046
|
+
let candidate = port;
|
|
1047
|
+
let attempt = 0;
|
|
1048
|
+
const bindFailed = (atPort, msg) => reject(
|
|
1049
|
+
new Error(`could not bind ${formatAddress(primaryHost, atPort)}: ${msg}`)
|
|
1050
|
+
);
|
|
1051
|
+
const onError = (e) => {
|
|
1052
|
+
if (e.code !== "EADDRINUSE") return bindFailed(candidate, e.message);
|
|
1053
|
+
if (++attempt >= maxAttempts) {
|
|
1054
|
+
reject(
|
|
1055
|
+
new Error(
|
|
1056
|
+
`could not bind ${formatAddress(primaryHost, port)}: ${maxAttempts} consecutive ports in use`
|
|
1057
|
+
)
|
|
1058
|
+
);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
candidate++;
|
|
1062
|
+
listen();
|
|
1063
|
+
};
|
|
1064
|
+
const onListening = () => {
|
|
1065
|
+
primary.removeListener("error", onError);
|
|
1066
|
+
if (candidate !== port) {
|
|
1067
|
+
warnings.push(`port ${port} was busy; using ${candidate} instead`);
|
|
1068
|
+
}
|
|
1024
1069
|
const addr = primary.address();
|
|
1025
|
-
const resolvedPort = addr && typeof addr === "object" ? addr.port :
|
|
1070
|
+
const resolvedPort = addr && typeof addr === "object" ? addr.port : candidate;
|
|
1026
1071
|
addresses.push(formatAddress(primaryHost, resolvedPort));
|
|
1027
1072
|
if (!LOOPBACK.has(host)) {
|
|
1028
|
-
resolve3({ addresses, warnings });
|
|
1073
|
+
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1029
1074
|
return;
|
|
1030
1075
|
}
|
|
1031
1076
|
const siblingHost = primaryHost === "::1" ? "127.0.0.1" : "::1";
|
|
1032
1077
|
const s = createServer();
|
|
1033
1078
|
attachSecondary(s);
|
|
1034
|
-
const
|
|
1079
|
+
const onSiblingError = (se) => {
|
|
1035
1080
|
s.close();
|
|
1036
1081
|
warnings.push(
|
|
1037
|
-
`could not also bind ${formatAddress(siblingHost, resolvedPort)} (${
|
|
1082
|
+
`could not also bind ${formatAddress(siblingHost, resolvedPort)} (${se.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
|
|
1038
1083
|
);
|
|
1039
|
-
resolve3({ addresses, warnings });
|
|
1084
|
+
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1040
1085
|
};
|
|
1041
|
-
s.once("error",
|
|
1086
|
+
s.once("error", onSiblingError);
|
|
1042
1087
|
s.listen(resolvedPort, siblingHost, () => {
|
|
1043
|
-
s.off("error",
|
|
1088
|
+
s.off("error", onSiblingError);
|
|
1044
1089
|
sibling = s;
|
|
1045
1090
|
addresses.push(formatAddress(siblingHost, resolvedPort));
|
|
1046
|
-
resolve3({ addresses, warnings });
|
|
1091
|
+
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1047
1092
|
});
|
|
1048
|
-
}
|
|
1093
|
+
};
|
|
1094
|
+
const listen = () => {
|
|
1095
|
+
try {
|
|
1096
|
+
primary.listen(candidate, primaryHost);
|
|
1097
|
+
} catch (e) {
|
|
1098
|
+
primary.removeListener("error", onError);
|
|
1099
|
+
primary.removeListener("listening", onListening);
|
|
1100
|
+
bindFailed(candidate, e.message);
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
primary.on("error", onError);
|
|
1104
|
+
primary.once("listening", onListening);
|
|
1105
|
+
listen();
|
|
1049
1106
|
}
|
|
1050
1107
|
);
|
|
1051
1108
|
return {
|
|
@@ -1062,10 +1119,14 @@ function printHelp() {
|
|
|
1062
1119
|
process.stdout.write(
|
|
1063
1120
|
`autotel-devtools - Standalone OTLP receiver with web devtools UI
|
|
1064
1121
|
|
|
1065
|
-
Usage: autotel-devtools [options]
|
|
1122
|
+
Usage: autotel-devtools [port] [options]
|
|
1123
|
+
|
|
1124
|
+
Arguments:
|
|
1125
|
+
port Port to listen on (shorthand for --port; must be a positive integer)
|
|
1066
1126
|
|
|
1067
1127
|
Options:
|
|
1068
|
-
-p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT)
|
|
1128
|
+
-p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).
|
|
1129
|
+
If the port is taken, the next free port is used and a warning is shown.
|
|
1069
1130
|
-H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)
|
|
1070
1131
|
-t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)
|
|
1071
1132
|
Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT
|
|
@@ -1085,7 +1146,8 @@ Endpoints:
|
|
|
1085
1146
|
|
|
1086
1147
|
Examples:
|
|
1087
1148
|
npx autotel-devtools
|
|
1088
|
-
npx autotel-devtools
|
|
1149
|
+
npx autotel-devtools 4319
|
|
1150
|
+
npx autotel-devtools -p 4319 -H 0.0.0.0
|
|
1089
1151
|
|
|
1090
1152
|
Then point your app:
|
|
1091
1153
|
OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js
|
|
@@ -1112,10 +1174,12 @@ function printVersion() {
|
|
|
1112
1174
|
}
|
|
1113
1175
|
function parseArgs(argv) {
|
|
1114
1176
|
const options = {
|
|
1115
|
-
port:
|
|
1177
|
+
port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || "4318"),
|
|
1116
1178
|
host: process.env.AUTOTEL_DEVTOOLS_HOST || "127.0.0.1",
|
|
1117
1179
|
title: process.env.AUTOTEL_DEVTOOLS_TITLE
|
|
1118
1180
|
};
|
|
1181
|
+
let portWasExplicit = false;
|
|
1182
|
+
let positionalPortConsumed = false;
|
|
1119
1183
|
for (let i = 0; i < argv.length; i++) {
|
|
1120
1184
|
const arg = argv[i];
|
|
1121
1185
|
const next = argv[i + 1];
|
|
@@ -1128,7 +1192,8 @@ function parseArgs(argv) {
|
|
|
1128
1192
|
return null;
|
|
1129
1193
|
}
|
|
1130
1194
|
if ((arg === "--port" || arg === "-p") && next) {
|
|
1131
|
-
options.port =
|
|
1195
|
+
options.port = parsePort(next);
|
|
1196
|
+
portWasExplicit = true;
|
|
1132
1197
|
i++;
|
|
1133
1198
|
continue;
|
|
1134
1199
|
}
|
|
@@ -1142,9 +1207,23 @@ function parseArgs(argv) {
|
|
|
1142
1207
|
i++;
|
|
1143
1208
|
continue;
|
|
1144
1209
|
}
|
|
1210
|
+
if (/^\d+$/.test(arg) && !positionalPortConsumed) {
|
|
1211
|
+
if (!portWasExplicit) options.port = parsePort(arg);
|
|
1212
|
+
positionalPortConsumed = true;
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1145
1215
|
}
|
|
1146
1216
|
return options;
|
|
1147
1217
|
}
|
|
1218
|
+
function parsePort(value) {
|
|
1219
|
+
const n = Number(value);
|
|
1220
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
1221
|
+
process.stderr.write(`[autotel-devtools] invalid port: ${value}
|
|
1222
|
+
`);
|
|
1223
|
+
process.exit(2);
|
|
1224
|
+
}
|
|
1225
|
+
return n;
|
|
1226
|
+
}
|
|
1148
1227
|
async function main() {
|
|
1149
1228
|
const options = parseArgs(process.argv.slice(2));
|
|
1150
1229
|
if (!options) {
|
|
@@ -1159,8 +1238,8 @@ async function main() {
|
|
|
1159
1238
|
host: options.host,
|
|
1160
1239
|
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
|
|
1161
1240
|
});
|
|
1162
|
-
const { addresses, warnings } = await listeners.ready;
|
|
1163
|
-
const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${
|
|
1241
|
+
const { addresses, warnings, port: boundPort } = await listeners.ready;
|
|
1242
|
+
const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`;
|
|
1164
1243
|
const title = options.title || "autotel-devtools";
|
|
1165
1244
|
process.stdout.write(`
|
|
1166
1245
|
${title}
|
|
@@ -1168,18 +1247,34 @@ async function main() {
|
|
|
1168
1247
|
`);
|
|
1169
1248
|
process.stdout.write(` Listening: ${addresses.join(" + ")}
|
|
1170
1249
|
`);
|
|
1171
|
-
process.stdout.write(` UI: ${uiBase}
|
|
1250
|
+
process.stdout.write(` UI: ${uiBase} (open in a browser)
|
|
1172
1251
|
`);
|
|
1173
|
-
process.stdout.write(`
|
|
1252
|
+
process.stdout.write(` OTLP: ${uiBase}/v1/traces
|
|
1174
1253
|
`);
|
|
1175
1254
|
process.stdout.write(` WebSocket: ${uiBase.replace("http", "ws")}/ws
|
|
1255
|
+
|
|
1176
1256
|
`);
|
|
1177
|
-
process.stdout.write(
|
|
1257
|
+
process.stdout.write(
|
|
1258
|
+
` Embed in your app \u2014 paste into your HTML; a floating panel appears automatically:
|
|
1259
|
+
`
|
|
1260
|
+
);
|
|
1261
|
+
process.stdout.write(` <script src="${uiBase}/widget.js"></script>
|
|
1178
1262
|
|
|
1179
1263
|
`);
|
|
1180
|
-
process.stdout.write(
|
|
1264
|
+
process.stdout.write(
|
|
1265
|
+
` full screen instead: <script src="${uiBase}/widget.js?mode=fullpage"></script>
|
|
1266
|
+
`
|
|
1267
|
+
);
|
|
1268
|
+
process.stdout.write(
|
|
1269
|
+
` choose where it goes: add <autotel-devtools></autotel-devtools> to your markup
|
|
1270
|
+
|
|
1271
|
+
`
|
|
1272
|
+
);
|
|
1273
|
+
process.stdout.write(` Or point any OTLP exporter at this receiver:
|
|
1274
|
+
`);
|
|
1275
|
+
process.stdout.write(` OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
|
1181
1276
|
`);
|
|
1182
|
-
process.stdout.write(`
|
|
1277
|
+
process.stdout.write(` OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}
|
|
1183
1278
|
|
|
1184
1279
|
`);
|
|
1185
1280
|
process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces
|