autotel-devtools 6.0.0 → 6.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 +25 -1
- package/dist/cli.cjs +146 -23
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +146 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +84 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +84 -16
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +59 -4
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +17 -1
- package/dist/server/index.d.ts +17 -1
- package/dist/server/index.js +58 -5
- package/dist/server/index.js.map +1 -1
- package/dist/widget.global.js +1 -1
- package/package.json +2 -2
- package/skills/autotel-devtools/SKILL.md +4 -2
package/dist/cli.js
CHANGED
|
@@ -366,6 +366,9 @@ var DevtoolsServer = class {
|
|
|
366
366
|
this.onData = options.onData;
|
|
367
367
|
this.httpServer = options.server ?? createServer();
|
|
368
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
|
+
});
|
|
369
372
|
this.wss.on("connection", (ws) => {
|
|
370
373
|
this.clients.add(ws);
|
|
371
374
|
this.log(`Client connected (${this.clients.size} total)`);
|
|
@@ -913,7 +916,38 @@ function decodeOtlpMetricsRequest(body) {
|
|
|
913
916
|
return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
|
|
914
917
|
}
|
|
915
918
|
|
|
919
|
+
// src/server/identity.ts
|
|
920
|
+
var DEVTOOLS_IDENTITY = "autotel-devtools";
|
|
921
|
+
async function probePortHolder(host, port, timeoutMs = 500) {
|
|
922
|
+
const authority = host.includes(":") ? `[${host}]` : host;
|
|
923
|
+
const controller = new AbortController();
|
|
924
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
925
|
+
try {
|
|
926
|
+
const res = await fetch(`http://${authority}:${port}/healthz`, {
|
|
927
|
+
signal: controller.signal
|
|
928
|
+
});
|
|
929
|
+
if (res.headers.get("x-autotel-devtools")) return "autotel-devtools";
|
|
930
|
+
try {
|
|
931
|
+
const body = await res.json();
|
|
932
|
+
if (body && body.service === DEVTOOLS_IDENTITY) return "autotel-devtools";
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
return "foreign";
|
|
936
|
+
} catch {
|
|
937
|
+
return "none";
|
|
938
|
+
} finally {
|
|
939
|
+
clearTimeout(timer);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
916
943
|
// src/server/http.ts
|
|
944
|
+
function sendOtlpError(res, req, e) {
|
|
945
|
+
sendJson(res, 400, {
|
|
946
|
+
error: "Invalid OTLP payload",
|
|
947
|
+
message: e instanceof Error ? e.message : String(e),
|
|
948
|
+
contentType: req.headers["content-type"] ?? null
|
|
949
|
+
});
|
|
950
|
+
}
|
|
917
951
|
var PROTOBUF_DECODERS = {
|
|
918
952
|
traces: decodeOtlpTraceRequest,
|
|
919
953
|
logs: decodeOtlpLogsRequest,
|
|
@@ -934,6 +968,18 @@ function findPackageRoot() {
|
|
|
934
968
|
return dir;
|
|
935
969
|
}
|
|
936
970
|
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>`;
|
|
971
|
+
var cachedVersion = null;
|
|
972
|
+
function getVersion() {
|
|
973
|
+
if (cachedVersion !== null) return cachedVersion;
|
|
974
|
+
let version = "unknown";
|
|
975
|
+
try {
|
|
976
|
+
const pkg = JSON.parse(readFileSync(resolve(findPackageRoot(), "package.json"), "utf8"));
|
|
977
|
+
if (typeof pkg.version === "string") version = pkg.version;
|
|
978
|
+
} catch {
|
|
979
|
+
}
|
|
980
|
+
cachedVersion = version;
|
|
981
|
+
return version;
|
|
982
|
+
}
|
|
937
983
|
var cachedWidgetJs = null;
|
|
938
984
|
function getWidgetJs() {
|
|
939
985
|
if (!cachedWidgetJs) {
|
|
@@ -960,6 +1006,8 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
960
1006
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
961
1007
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
962
1008
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1009
|
+
res.setHeader("x-autotel-devtools", getVersion());
|
|
1010
|
+
res.setHeader("Access-Control-Expose-Headers", "x-autotel-devtools");
|
|
963
1011
|
if (req.method === "OPTIONS") {
|
|
964
1012
|
res.writeHead(204);
|
|
965
1013
|
res.end();
|
|
@@ -978,7 +1026,12 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
978
1026
|
return;
|
|
979
1027
|
}
|
|
980
1028
|
if (req.method === "GET" && url === "/healthz") {
|
|
981
|
-
sendJson(res, 200, {
|
|
1029
|
+
sendJson(res, 200, {
|
|
1030
|
+
ok: true,
|
|
1031
|
+
service: DEVTOOLS_IDENTITY,
|
|
1032
|
+
version: getVersion(),
|
|
1033
|
+
clients: devtools.clientCount
|
|
1034
|
+
});
|
|
982
1035
|
return;
|
|
983
1036
|
}
|
|
984
1037
|
if (req.method === "GET" && url === "/v1/traces") {
|
|
@@ -998,7 +1051,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
998
1051
|
devtools.addTraces(traces);
|
|
999
1052
|
sendJson(res, 200, { acceptedTraces: traces.length });
|
|
1000
1053
|
} catch (e) {
|
|
1001
|
-
|
|
1054
|
+
sendOtlpError(res, req, e);
|
|
1002
1055
|
}
|
|
1003
1056
|
return;
|
|
1004
1057
|
}
|
|
@@ -1009,7 +1062,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
1009
1062
|
devtools.addLogs(logs);
|
|
1010
1063
|
sendJson(res, 200, { acceptedLogs: logs.length });
|
|
1011
1064
|
} catch (e) {
|
|
1012
|
-
|
|
1065
|
+
sendOtlpError(res, req, e);
|
|
1013
1066
|
}
|
|
1014
1067
|
return;
|
|
1015
1068
|
}
|
|
@@ -1019,7 +1072,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
1019
1072
|
const count = countOtlpMetrics(payload);
|
|
1020
1073
|
sendJson(res, 200, { acceptedMetrics: count });
|
|
1021
1074
|
} catch (e) {
|
|
1022
|
-
|
|
1075
|
+
sendOtlpError(res, req, e);
|
|
1023
1076
|
}
|
|
1024
1077
|
return;
|
|
1025
1078
|
}
|
|
@@ -1027,43 +1080,79 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
1027
1080
|
});
|
|
1028
1081
|
}
|
|
1029
1082
|
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1083
|
+
var DEFAULT_MAX_PORT_TRIES = 20;
|
|
1030
1084
|
function formatAddress(host, port) {
|
|
1031
1085
|
return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
1032
1086
|
}
|
|
1033
1087
|
function listenLoopbackDualStack(args) {
|
|
1034
|
-
const { primary, port, host, attachSecondary } = args;
|
|
1088
|
+
const { primary, port, host, attachSecondary, maxTries } = args;
|
|
1089
|
+
const maxAttempts = Math.max(1, maxTries ?? DEFAULT_MAX_PORT_TRIES);
|
|
1035
1090
|
let sibling;
|
|
1036
1091
|
const ready = new Promise(
|
|
1037
|
-
(resolve3) => {
|
|
1092
|
+
(resolve3, reject) => {
|
|
1038
1093
|
const addresses = [];
|
|
1039
1094
|
const warnings = [];
|
|
1040
1095
|
const primaryHost = host === "localhost" ? "127.0.0.1" : host;
|
|
1041
|
-
|
|
1096
|
+
let candidate = port;
|
|
1097
|
+
let attempt = 0;
|
|
1098
|
+
const bindFailed = (atPort, msg) => reject(
|
|
1099
|
+
new Error(`could not bind ${formatAddress(primaryHost, atPort)}: ${msg}`)
|
|
1100
|
+
);
|
|
1101
|
+
const onError = (e) => {
|
|
1102
|
+
if (e.code !== "EADDRINUSE") return bindFailed(candidate, e.message);
|
|
1103
|
+
if (++attempt >= maxAttempts) {
|
|
1104
|
+
reject(
|
|
1105
|
+
new Error(
|
|
1106
|
+
`could not bind ${formatAddress(primaryHost, port)}: ${maxAttempts} consecutive ports in use`
|
|
1107
|
+
)
|
|
1108
|
+
);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
candidate++;
|
|
1112
|
+
listen();
|
|
1113
|
+
};
|
|
1114
|
+
const onListening = () => {
|
|
1115
|
+
primary.removeListener("error", onError);
|
|
1116
|
+
if (candidate !== port) {
|
|
1117
|
+
warnings.push(`port ${port} was busy; using ${candidate} instead`);
|
|
1118
|
+
}
|
|
1042
1119
|
const addr = primary.address();
|
|
1043
|
-
const resolvedPort = addr && typeof addr === "object" ? addr.port :
|
|
1120
|
+
const resolvedPort = addr && typeof addr === "object" ? addr.port : candidate;
|
|
1044
1121
|
addresses.push(formatAddress(primaryHost, resolvedPort));
|
|
1045
1122
|
if (!LOOPBACK.has(host)) {
|
|
1046
|
-
resolve3({ addresses, warnings });
|
|
1123
|
+
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1047
1124
|
return;
|
|
1048
1125
|
}
|
|
1049
1126
|
const siblingHost = primaryHost === "::1" ? "127.0.0.1" : "::1";
|
|
1050
1127
|
const s = createServer();
|
|
1051
1128
|
attachSecondary(s);
|
|
1052
|
-
const
|
|
1129
|
+
const onSiblingError = (se) => {
|
|
1053
1130
|
s.close();
|
|
1054
1131
|
warnings.push(
|
|
1055
|
-
`could not also bind ${formatAddress(siblingHost, resolvedPort)} (${
|
|
1132
|
+
`could not also bind ${formatAddress(siblingHost, resolvedPort)} (${se.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
|
|
1056
1133
|
);
|
|
1057
|
-
resolve3({ addresses, warnings });
|
|
1134
|
+
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1058
1135
|
};
|
|
1059
|
-
s.once("error",
|
|
1136
|
+
s.once("error", onSiblingError);
|
|
1060
1137
|
s.listen(resolvedPort, siblingHost, () => {
|
|
1061
|
-
s.off("error",
|
|
1138
|
+
s.off("error", onSiblingError);
|
|
1062
1139
|
sibling = s;
|
|
1063
1140
|
addresses.push(formatAddress(siblingHost, resolvedPort));
|
|
1064
|
-
resolve3({ addresses, warnings });
|
|
1141
|
+
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1065
1142
|
});
|
|
1066
|
-
}
|
|
1143
|
+
};
|
|
1144
|
+
const listen = () => {
|
|
1145
|
+
try {
|
|
1146
|
+
primary.listen(candidate, primaryHost);
|
|
1147
|
+
} catch (e) {
|
|
1148
|
+
primary.removeListener("error", onError);
|
|
1149
|
+
primary.removeListener("listening", onListening);
|
|
1150
|
+
bindFailed(candidate, e.message);
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
primary.on("error", onError);
|
|
1154
|
+
primary.once("listening", onListening);
|
|
1155
|
+
listen();
|
|
1067
1156
|
}
|
|
1068
1157
|
);
|
|
1069
1158
|
return {
|
|
@@ -1080,10 +1169,14 @@ function printHelp() {
|
|
|
1080
1169
|
process.stdout.write(
|
|
1081
1170
|
`autotel-devtools - Standalone OTLP receiver with web devtools UI
|
|
1082
1171
|
|
|
1083
|
-
Usage: autotel-devtools [options]
|
|
1172
|
+
Usage: autotel-devtools [port] [options]
|
|
1173
|
+
|
|
1174
|
+
Arguments:
|
|
1175
|
+
port Port to listen on (shorthand for --port; must be a positive integer)
|
|
1084
1176
|
|
|
1085
1177
|
Options:
|
|
1086
|
-
-p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT)
|
|
1178
|
+
-p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).
|
|
1179
|
+
If the port is taken, the next free port is used and a warning is shown.
|
|
1087
1180
|
-H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)
|
|
1088
1181
|
-t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)
|
|
1089
1182
|
Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT
|
|
@@ -1103,7 +1196,8 @@ Endpoints:
|
|
|
1103
1196
|
|
|
1104
1197
|
Examples:
|
|
1105
1198
|
npx autotel-devtools
|
|
1106
|
-
npx autotel-devtools
|
|
1199
|
+
npx autotel-devtools 4319
|
|
1200
|
+
npx autotel-devtools -p 4319 -H 0.0.0.0
|
|
1107
1201
|
|
|
1108
1202
|
Then point your app:
|
|
1109
1203
|
OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js
|
|
@@ -1130,10 +1224,12 @@ function printVersion() {
|
|
|
1130
1224
|
}
|
|
1131
1225
|
function parseArgs(argv) {
|
|
1132
1226
|
const options = {
|
|
1133
|
-
port:
|
|
1227
|
+
port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || "4318"),
|
|
1134
1228
|
host: process.env.AUTOTEL_DEVTOOLS_HOST || "127.0.0.1",
|
|
1135
1229
|
title: process.env.AUTOTEL_DEVTOOLS_TITLE
|
|
1136
1230
|
};
|
|
1231
|
+
let portWasExplicit = false;
|
|
1232
|
+
let positionalPortConsumed = false;
|
|
1137
1233
|
for (let i = 0; i < argv.length; i++) {
|
|
1138
1234
|
const arg = argv[i];
|
|
1139
1235
|
const next = argv[i + 1];
|
|
@@ -1146,7 +1242,8 @@ function parseArgs(argv) {
|
|
|
1146
1242
|
return null;
|
|
1147
1243
|
}
|
|
1148
1244
|
if ((arg === "--port" || arg === "-p") && next) {
|
|
1149
|
-
options.port =
|
|
1245
|
+
options.port = parsePort(next);
|
|
1246
|
+
portWasExplicit = true;
|
|
1150
1247
|
i++;
|
|
1151
1248
|
continue;
|
|
1152
1249
|
}
|
|
@@ -1160,9 +1257,23 @@ function parseArgs(argv) {
|
|
|
1160
1257
|
i++;
|
|
1161
1258
|
continue;
|
|
1162
1259
|
}
|
|
1260
|
+
if (/^\d+$/.test(arg) && !positionalPortConsumed) {
|
|
1261
|
+
if (!portWasExplicit) options.port = parsePort(arg);
|
|
1262
|
+
positionalPortConsumed = true;
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1163
1265
|
}
|
|
1164
1266
|
return options;
|
|
1165
1267
|
}
|
|
1268
|
+
function parsePort(value) {
|
|
1269
|
+
const n = Number(value);
|
|
1270
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
1271
|
+
process.stderr.write(`[autotel-devtools] invalid port: ${value}
|
|
1272
|
+
`);
|
|
1273
|
+
process.exit(2);
|
|
1274
|
+
}
|
|
1275
|
+
return n;
|
|
1276
|
+
}
|
|
1166
1277
|
async function main() {
|
|
1167
1278
|
const options = parseArgs(process.argv.slice(2));
|
|
1168
1279
|
if (!options) {
|
|
@@ -1177,8 +1288,20 @@ async function main() {
|
|
|
1177
1288
|
host: options.host,
|
|
1178
1289
|
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
|
|
1179
1290
|
});
|
|
1180
|
-
const { addresses, warnings } = await listeners.ready;
|
|
1181
|
-
|
|
1291
|
+
const { addresses, warnings, port: boundPort } = await listeners.ready;
|
|
1292
|
+
if (boundPort !== options.port) {
|
|
1293
|
+
const holder = await probePortHolder(options.host, options.port);
|
|
1294
|
+
if (holder === "autotel-devtools") {
|
|
1295
|
+
warnings.push(
|
|
1296
|
+
`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.`
|
|
1297
|
+
);
|
|
1298
|
+
} else {
|
|
1299
|
+
warnings.push(
|
|
1300
|
+
`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.`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`;
|
|
1182
1305
|
const title = options.title || "autotel-devtools";
|
|
1183
1306
|
process.stdout.write(`
|
|
1184
1307
|
${title}
|