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/README.md
CHANGED
|
@@ -113,10 +113,10 @@ const myFunction = trace((ctx) => async () => {
|
|
|
113
113
|
- **HTTP Routes** - OTLP receivers for traces/logs/metrics (JSON + protobuf)
|
|
114
114
|
- **Exporters** - OpenTelemetry span/log exporters
|
|
115
115
|
|
|
116
|
-
### Widget (
|
|
116
|
+
### Widget (Svelte 5)
|
|
117
117
|
|
|
118
118
|
- **Shadow DOM** - Isolated styles, no conflicts with app CSS
|
|
119
|
-
- **
|
|
119
|
+
- **Svelte 5 runes** - Reactive state via a signal shim that preserves a `.value` API
|
|
120
120
|
- **Views** - Traces, Logs, Metrics, Errors, Resources, Service Map
|
|
121
121
|
|
|
122
122
|
## Features
|
|
@@ -152,12 +152,17 @@ AUTOTEL_DEVTOOLS_TITLE="My App" # Dashboard title (optional)
|
|
|
152
152
|
### CLI Options
|
|
153
153
|
|
|
154
154
|
```bash
|
|
155
|
+
npx autotel-devtools 4319 # port as a bare positional
|
|
155
156
|
npx autotel-devtools --port 4319 --host 0.0.0.0
|
|
156
157
|
```
|
|
157
158
|
|
|
159
|
+
Arguments:
|
|
160
|
+
|
|
161
|
+
- `[port]` - Port to listen on, shorthand for `--port` (an explicit `--port` always wins)
|
|
162
|
+
|
|
158
163
|
Options:
|
|
159
164
|
|
|
160
|
-
- `--port, -p` - Port to listen on (default: 4318)
|
|
165
|
+
- `--port, -p` - Port to listen on (default: 4318). If the port is taken, the receiver walks forward to the next free port and prints a warning.
|
|
161
166
|
- `--host, -H` - Host to bind to (default: 127.0.0.1)
|
|
162
167
|
- `--title, -t` - Dashboard title
|
|
163
168
|
|
package/dist/cli.cjs
CHANGED
|
@@ -365,12 +365,17 @@ var DevtoolsServer = class {
|
|
|
365
365
|
limits;
|
|
366
366
|
verbose;
|
|
367
367
|
_port;
|
|
368
|
+
onData;
|
|
368
369
|
constructor(options = {}) {
|
|
369
370
|
this.limits = resolveTelemetryLimits(options);
|
|
370
371
|
this.verbose = options.verbose ?? false;
|
|
371
372
|
this._port = options.port ?? 4318;
|
|
373
|
+
this.onData = options.onData;
|
|
372
374
|
this.httpServer = options.server ?? http.createServer();
|
|
373
375
|
this.wss = new ws.WebSocketServer({ server: this.httpServer, path: options.path ?? "/ws" });
|
|
376
|
+
this.wss.on("error", (err) => {
|
|
377
|
+
if (this.httpServer.listening) throw err;
|
|
378
|
+
});
|
|
374
379
|
this.wss.on("connection", (ws) => {
|
|
375
380
|
this.clients.add(ws);
|
|
376
381
|
this.log(`Client connected (${this.clients.size} total)`);
|
|
@@ -462,6 +467,12 @@ var DevtoolsServer = class {
|
|
|
462
467
|
client.send(msg);
|
|
463
468
|
}
|
|
464
469
|
}
|
|
470
|
+
if (this.onData) {
|
|
471
|
+
try {
|
|
472
|
+
this.onData(data);
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
}
|
|
465
476
|
}
|
|
466
477
|
log(message) {
|
|
467
478
|
if (this.verbose) console.log(`[autotel-devtools] ${message}`);
|
|
@@ -515,7 +526,10 @@ function flattenAttributes(attrs) {
|
|
|
515
526
|
}
|
|
516
527
|
function nanoToMs(nano) {
|
|
517
528
|
if (!nano) return 0;
|
|
518
|
-
|
|
529
|
+
const ns = BigInt(nano);
|
|
530
|
+
const ms = ns / 1000000n;
|
|
531
|
+
const remNs = ns % 1000000n;
|
|
532
|
+
return Number(ms) + Number(remNs) / 1e6;
|
|
519
533
|
}
|
|
520
534
|
var SPAN_KIND_MAP = {
|
|
521
535
|
0: "INTERNAL",
|
|
@@ -553,6 +567,7 @@ function parseOtlpTraces(payload) {
|
|
|
553
567
|
const service = String(resourceAttrs["service.name"] || "unknown");
|
|
554
568
|
const scopeSpans = rs.scopeSpans || [];
|
|
555
569
|
for (const ss of scopeSpans) {
|
|
570
|
+
const scope = ss.scope?.name ? { name: ss.scope.name, version: ss.scope.version || void 0 } : void 0;
|
|
556
571
|
for (const span of ss.spans || []) {
|
|
557
572
|
const traceId = normalizeHexId(span.traceId);
|
|
558
573
|
if (!traceId) continue;
|
|
@@ -577,7 +592,13 @@ function parseOtlpTraces(payload) {
|
|
|
577
592
|
name: e.name || "",
|
|
578
593
|
timestamp: nanoToMs(e.timeUnixNano),
|
|
579
594
|
attributes: flattenAttributes(e.attributes)
|
|
580
|
-
}))
|
|
595
|
+
})),
|
|
596
|
+
links: (span.links || []).map((l) => ({
|
|
597
|
+
traceId: normalizeHexId(l.traceId),
|
|
598
|
+
spanId: normalizeHexId(l.spanId),
|
|
599
|
+
attributes: flattenAttributes(l.attributes)
|
|
600
|
+
})),
|
|
601
|
+
scope
|
|
581
602
|
};
|
|
582
603
|
const existing = traceMap.get(traceId);
|
|
583
604
|
if (existing) {
|
|
@@ -1016,43 +1037,79 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
1016
1037
|
});
|
|
1017
1038
|
}
|
|
1018
1039
|
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1040
|
+
var DEFAULT_MAX_PORT_TRIES = 20;
|
|
1019
1041
|
function formatAddress(host, port) {
|
|
1020
1042
|
return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
1021
1043
|
}
|
|
1022
1044
|
function listenLoopbackDualStack(args) {
|
|
1023
|
-
const { primary, port, host, attachSecondary } = args;
|
|
1045
|
+
const { primary, port, host, attachSecondary, maxTries } = args;
|
|
1046
|
+
const maxAttempts = Math.max(1, maxTries ?? DEFAULT_MAX_PORT_TRIES);
|
|
1024
1047
|
let sibling;
|
|
1025
1048
|
const ready = new Promise(
|
|
1026
|
-
(resolve3) => {
|
|
1049
|
+
(resolve3, reject) => {
|
|
1027
1050
|
const addresses = [];
|
|
1028
1051
|
const warnings = [];
|
|
1029
1052
|
const primaryHost = host === "localhost" ? "127.0.0.1" : host;
|
|
1030
|
-
|
|
1053
|
+
let candidate = port;
|
|
1054
|
+
let attempt = 0;
|
|
1055
|
+
const bindFailed = (atPort, msg) => reject(
|
|
1056
|
+
new Error(`could not bind ${formatAddress(primaryHost, atPort)}: ${msg}`)
|
|
1057
|
+
);
|
|
1058
|
+
const onError = (e) => {
|
|
1059
|
+
if (e.code !== "EADDRINUSE") return bindFailed(candidate, e.message);
|
|
1060
|
+
if (++attempt >= maxAttempts) {
|
|
1061
|
+
reject(
|
|
1062
|
+
new Error(
|
|
1063
|
+
`could not bind ${formatAddress(primaryHost, port)}: ${maxAttempts} consecutive ports in use`
|
|
1064
|
+
)
|
|
1065
|
+
);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
candidate++;
|
|
1069
|
+
listen();
|
|
1070
|
+
};
|
|
1071
|
+
const onListening = () => {
|
|
1072
|
+
primary.removeListener("error", onError);
|
|
1073
|
+
if (candidate !== port) {
|
|
1074
|
+
warnings.push(`port ${port} was busy; using ${candidate} instead`);
|
|
1075
|
+
}
|
|
1031
1076
|
const addr = primary.address();
|
|
1032
|
-
const resolvedPort = addr && typeof addr === "object" ? addr.port :
|
|
1077
|
+
const resolvedPort = addr && typeof addr === "object" ? addr.port : candidate;
|
|
1033
1078
|
addresses.push(formatAddress(primaryHost, resolvedPort));
|
|
1034
1079
|
if (!LOOPBACK.has(host)) {
|
|
1035
|
-
resolve3({ addresses, warnings });
|
|
1080
|
+
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1036
1081
|
return;
|
|
1037
1082
|
}
|
|
1038
1083
|
const siblingHost = primaryHost === "::1" ? "127.0.0.1" : "::1";
|
|
1039
1084
|
const s = http.createServer();
|
|
1040
1085
|
attachSecondary(s);
|
|
1041
|
-
const
|
|
1086
|
+
const onSiblingError = (se) => {
|
|
1042
1087
|
s.close();
|
|
1043
1088
|
warnings.push(
|
|
1044
|
-
`could not also bind ${formatAddress(siblingHost, resolvedPort)} (${
|
|
1089
|
+
`could not also bind ${formatAddress(siblingHost, resolvedPort)} (${se.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
|
|
1045
1090
|
);
|
|
1046
|
-
resolve3({ addresses, warnings });
|
|
1091
|
+
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1047
1092
|
};
|
|
1048
|
-
s.once("error",
|
|
1093
|
+
s.once("error", onSiblingError);
|
|
1049
1094
|
s.listen(resolvedPort, siblingHost, () => {
|
|
1050
|
-
s.off("error",
|
|
1095
|
+
s.off("error", onSiblingError);
|
|
1051
1096
|
sibling = s;
|
|
1052
1097
|
addresses.push(formatAddress(siblingHost, resolvedPort));
|
|
1053
|
-
resolve3({ addresses, warnings });
|
|
1098
|
+
resolve3({ addresses, port: resolvedPort, warnings });
|
|
1054
1099
|
});
|
|
1055
|
-
}
|
|
1100
|
+
};
|
|
1101
|
+
const listen = () => {
|
|
1102
|
+
try {
|
|
1103
|
+
primary.listen(candidate, primaryHost);
|
|
1104
|
+
} catch (e) {
|
|
1105
|
+
primary.removeListener("error", onError);
|
|
1106
|
+
primary.removeListener("listening", onListening);
|
|
1107
|
+
bindFailed(candidate, e.message);
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
primary.on("error", onError);
|
|
1111
|
+
primary.once("listening", onListening);
|
|
1112
|
+
listen();
|
|
1056
1113
|
}
|
|
1057
1114
|
);
|
|
1058
1115
|
return {
|
|
@@ -1069,10 +1126,14 @@ function printHelp() {
|
|
|
1069
1126
|
process.stdout.write(
|
|
1070
1127
|
`autotel-devtools - Standalone OTLP receiver with web devtools UI
|
|
1071
1128
|
|
|
1072
|
-
Usage: autotel-devtools [options]
|
|
1129
|
+
Usage: autotel-devtools [port] [options]
|
|
1130
|
+
|
|
1131
|
+
Arguments:
|
|
1132
|
+
port Port to listen on (shorthand for --port; must be a positive integer)
|
|
1073
1133
|
|
|
1074
1134
|
Options:
|
|
1075
|
-
-p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT)
|
|
1135
|
+
-p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).
|
|
1136
|
+
If the port is taken, the next free port is used and a warning is shown.
|
|
1076
1137
|
-H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)
|
|
1077
1138
|
-t, --title <title> Dashboard title (env: AUTOTEL_DEVTOOLS_TITLE)
|
|
1078
1139
|
Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT, AUTOTEL_MAX_METRIC_COUNT
|
|
@@ -1092,7 +1153,8 @@ Endpoints:
|
|
|
1092
1153
|
|
|
1093
1154
|
Examples:
|
|
1094
1155
|
npx autotel-devtools
|
|
1095
|
-
npx autotel-devtools
|
|
1156
|
+
npx autotel-devtools 4319
|
|
1157
|
+
npx autotel-devtools -p 4319 -H 0.0.0.0
|
|
1096
1158
|
|
|
1097
1159
|
Then point your app:
|
|
1098
1160
|
OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js
|
|
@@ -1119,10 +1181,12 @@ function printVersion() {
|
|
|
1119
1181
|
}
|
|
1120
1182
|
function parseArgs(argv) {
|
|
1121
1183
|
const options = {
|
|
1122
|
-
port:
|
|
1184
|
+
port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || "4318"),
|
|
1123
1185
|
host: process.env.AUTOTEL_DEVTOOLS_HOST || "127.0.0.1",
|
|
1124
1186
|
title: process.env.AUTOTEL_DEVTOOLS_TITLE
|
|
1125
1187
|
};
|
|
1188
|
+
let portWasExplicit = false;
|
|
1189
|
+
let positionalPortConsumed = false;
|
|
1126
1190
|
for (let i = 0; i < argv.length; i++) {
|
|
1127
1191
|
const arg = argv[i];
|
|
1128
1192
|
const next = argv[i + 1];
|
|
@@ -1135,7 +1199,8 @@ function parseArgs(argv) {
|
|
|
1135
1199
|
return null;
|
|
1136
1200
|
}
|
|
1137
1201
|
if ((arg === "--port" || arg === "-p") && next) {
|
|
1138
|
-
options.port =
|
|
1202
|
+
options.port = parsePort(next);
|
|
1203
|
+
portWasExplicit = true;
|
|
1139
1204
|
i++;
|
|
1140
1205
|
continue;
|
|
1141
1206
|
}
|
|
@@ -1149,9 +1214,23 @@ function parseArgs(argv) {
|
|
|
1149
1214
|
i++;
|
|
1150
1215
|
continue;
|
|
1151
1216
|
}
|
|
1217
|
+
if (/^\d+$/.test(arg) && !positionalPortConsumed) {
|
|
1218
|
+
if (!portWasExplicit) options.port = parsePort(arg);
|
|
1219
|
+
positionalPortConsumed = true;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1152
1222
|
}
|
|
1153
1223
|
return options;
|
|
1154
1224
|
}
|
|
1225
|
+
function parsePort(value) {
|
|
1226
|
+
const n = Number(value);
|
|
1227
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
1228
|
+
process.stderr.write(`[autotel-devtools] invalid port: ${value}
|
|
1229
|
+
`);
|
|
1230
|
+
process.exit(2);
|
|
1231
|
+
}
|
|
1232
|
+
return n;
|
|
1233
|
+
}
|
|
1155
1234
|
async function main() {
|
|
1156
1235
|
const options = parseArgs(process.argv.slice(2));
|
|
1157
1236
|
if (!options) {
|
|
@@ -1166,8 +1245,8 @@ async function main() {
|
|
|
1166
1245
|
host: options.host,
|
|
1167
1246
|
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
|
|
1168
1247
|
});
|
|
1169
|
-
const { addresses, warnings } = await listeners.ready;
|
|
1170
|
-
const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${
|
|
1248
|
+
const { addresses, warnings, port: boundPort } = await listeners.ready;
|
|
1249
|
+
const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`;
|
|
1171
1250
|
const title = options.title || "autotel-devtools";
|
|
1172
1251
|
process.stdout.write(`
|
|
1173
1252
|
${title}
|
|
@@ -1175,18 +1254,34 @@ async function main() {
|
|
|
1175
1254
|
`);
|
|
1176
1255
|
process.stdout.write(` Listening: ${addresses.join(" + ")}
|
|
1177
1256
|
`);
|
|
1178
|
-
process.stdout.write(` UI: ${uiBase}
|
|
1257
|
+
process.stdout.write(` UI: ${uiBase} (open in a browser)
|
|
1179
1258
|
`);
|
|
1180
|
-
process.stdout.write(`
|
|
1259
|
+
process.stdout.write(` OTLP: ${uiBase}/v1/traces
|
|
1181
1260
|
`);
|
|
1182
1261
|
process.stdout.write(` WebSocket: ${uiBase.replace("http", "ws")}/ws
|
|
1262
|
+
|
|
1183
1263
|
`);
|
|
1184
|
-
process.stdout.write(
|
|
1264
|
+
process.stdout.write(
|
|
1265
|
+
` Embed in your app \u2014 paste into your HTML; a floating panel appears automatically:
|
|
1266
|
+
`
|
|
1267
|
+
);
|
|
1268
|
+
process.stdout.write(` <script src="${uiBase}/widget.js"></script>
|
|
1185
1269
|
|
|
1186
1270
|
`);
|
|
1187
|
-
process.stdout.write(
|
|
1271
|
+
process.stdout.write(
|
|
1272
|
+
` full screen instead: <script src="${uiBase}/widget.js?mode=fullpage"></script>
|
|
1273
|
+
`
|
|
1274
|
+
);
|
|
1275
|
+
process.stdout.write(
|
|
1276
|
+
` choose where it goes: add <autotel-devtools></autotel-devtools> to your markup
|
|
1277
|
+
|
|
1278
|
+
`
|
|
1279
|
+
);
|
|
1280
|
+
process.stdout.write(` Or point any OTLP exporter at this receiver:
|
|
1281
|
+
`);
|
|
1282
|
+
process.stdout.write(` OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
|
1188
1283
|
`);
|
|
1189
|
-
process.stdout.write(`
|
|
1284
|
+
process.stdout.write(` OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}
|
|
1190
1285
|
|
|
1191
1286
|
`);
|
|
1192
1287
|
process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces
|