autotel-devtools 6.0.1 → 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 CHANGED
@@ -111,6 +111,25 @@ const myFunction = trace((ctx) => async () => {
111
111
 
112
112
  - **DevtoolsServer** - WebSocket server + in-memory data store
113
113
  - **HTTP Routes** - OTLP receivers for traces/logs/metrics (JSON + protobuf)
114
+
115
+ #### Detecting the receiver
116
+
117
+ Every response carries an `x-autotel-devtools: <version>` header, and `GET /healthz`
118
+ returns `{ ok, service: "autotel-devtools", version, clients }`. Use either to confirm
119
+ you are talking to autotel-devtools rather than another OTLP collector that happens to
120
+ share the port — for example before pointing an exporter at `:4318`:
121
+
122
+ ```ts
123
+ import { probePortHolder } from 'autotel-devtools/server'
124
+
125
+ // 'autotel-devtools' | 'foreign' | 'none'
126
+ const holder = await probePortHolder('127.0.0.1', 4318)
127
+ ```
128
+
129
+ If you start the receiver and the requested port is held by a *foreign* process (some
130
+ IDEs run their own OTLP collector on `:4318`), the CLI falls forward to the next free
131
+ port and warns that exporters still aimed at the busy port are reaching that other
132
+ process — point them at the bound port, or free the original.
114
133
  - **Exporters** - OpenTelemetry span/log exporters
115
134
 
116
135
  ### Widget (Svelte 5)
package/dist/cli.cjs CHANGED
@@ -923,7 +923,38 @@ function decodeOtlpMetricsRequest(body) {
923
923
  return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
924
924
  }
925
925
 
926
+ // src/server/identity.ts
927
+ var DEVTOOLS_IDENTITY = "autotel-devtools";
928
+ async function probePortHolder(host, port, timeoutMs = 500) {
929
+ const authority = host.includes(":") ? `[${host}]` : host;
930
+ const controller = new AbortController();
931
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
932
+ try {
933
+ const res = await fetch(`http://${authority}:${port}/healthz`, {
934
+ signal: controller.signal
935
+ });
936
+ if (res.headers.get("x-autotel-devtools")) return "autotel-devtools";
937
+ try {
938
+ const body = await res.json();
939
+ if (body && body.service === DEVTOOLS_IDENTITY) return "autotel-devtools";
940
+ } catch {
941
+ }
942
+ return "foreign";
943
+ } catch {
944
+ return "none";
945
+ } finally {
946
+ clearTimeout(timer);
947
+ }
948
+ }
949
+
926
950
  // src/server/http.ts
951
+ function sendOtlpError(res, req, e) {
952
+ sendJson(res, 400, {
953
+ error: "Invalid OTLP payload",
954
+ message: e instanceof Error ? e.message : String(e),
955
+ contentType: req.headers["content-type"] ?? null
956
+ });
957
+ }
927
958
  var PROTOBUF_DECODERS = {
928
959
  traces: decodeOtlpTraceRequest,
929
960
  logs: decodeOtlpLogsRequest,
@@ -944,6 +975,18 @@ function findPackageRoot() {
944
975
  return dir;
945
976
  }
946
977
  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>`;
978
+ var cachedVersion = null;
979
+ function getVersion() {
980
+ if (cachedVersion !== null) return cachedVersion;
981
+ let version = "unknown";
982
+ try {
983
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(findPackageRoot(), "package.json"), "utf8"));
984
+ if (typeof pkg.version === "string") version = pkg.version;
985
+ } catch {
986
+ }
987
+ cachedVersion = version;
988
+ return version;
989
+ }
947
990
  var cachedWidgetJs = null;
948
991
  function getWidgetJs() {
949
992
  if (!cachedWidgetJs) {
@@ -970,6 +1013,8 @@ function attachDevtoolsRoutes(httpServer, devtools) {
970
1013
  res.setHeader("Access-Control-Allow-Origin", "*");
971
1014
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
972
1015
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1016
+ res.setHeader("x-autotel-devtools", getVersion());
1017
+ res.setHeader("Access-Control-Expose-Headers", "x-autotel-devtools");
973
1018
  if (req.method === "OPTIONS") {
974
1019
  res.writeHead(204);
975
1020
  res.end();
@@ -988,7 +1033,12 @@ function attachDevtoolsRoutes(httpServer, devtools) {
988
1033
  return;
989
1034
  }
990
1035
  if (req.method === "GET" && url === "/healthz") {
991
- sendJson(res, 200, { ok: true, clients: devtools.clientCount });
1036
+ sendJson(res, 200, {
1037
+ ok: true,
1038
+ service: DEVTOOLS_IDENTITY,
1039
+ version: getVersion(),
1040
+ clients: devtools.clientCount
1041
+ });
992
1042
  return;
993
1043
  }
994
1044
  if (req.method === "GET" && url === "/v1/traces") {
@@ -1008,7 +1058,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1008
1058
  devtools.addTraces(traces);
1009
1059
  sendJson(res, 200, { acceptedTraces: traces.length });
1010
1060
  } catch (e) {
1011
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1061
+ sendOtlpError(res, req, e);
1012
1062
  }
1013
1063
  return;
1014
1064
  }
@@ -1019,7 +1069,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1019
1069
  devtools.addLogs(logs);
1020
1070
  sendJson(res, 200, { acceptedLogs: logs.length });
1021
1071
  } catch (e) {
1022
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1072
+ sendOtlpError(res, req, e);
1023
1073
  }
1024
1074
  return;
1025
1075
  }
@@ -1029,7 +1079,7 @@ function attachDevtoolsRoutes(httpServer, devtools) {
1029
1079
  const count = countOtlpMetrics(payload);
1030
1080
  sendJson(res, 200, { acceptedMetrics: count });
1031
1081
  } catch (e) {
1032
- sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
1082
+ sendOtlpError(res, req, e);
1033
1083
  }
1034
1084
  return;
1035
1085
  }
@@ -1246,6 +1296,18 @@ async function main() {
1246
1296
  attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
1247
1297
  });
1248
1298
  const { addresses, warnings, port: boundPort } = await listeners.ready;
1299
+ if (boundPort !== options.port) {
1300
+ const holder = await probePortHolder(options.host, options.port);
1301
+ if (holder === "autotel-devtools") {
1302
+ warnings.push(
1303
+ `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.`
1304
+ );
1305
+ } else {
1306
+ warnings.push(
1307
+ `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.`
1308
+ );
1309
+ }
1310
+ }
1249
1311
  const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`;
1250
1312
  const title = options.title || "autotel-devtools";
1251
1313
  process.stdout.write(`