autotel-devtools 4.0.0 → 5.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/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { WebSocketServer, WebSocket } from 'ws';
3
3
  import { readFileSync, existsSync } from 'fs';
4
4
  import { resolve, dirname } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
+ import * as protobuf from 'protobufjs';
6
7
  import { ExportResultCode } from '@opentelemetry/core';
7
8
 
8
9
  // src/index.ts
@@ -527,7 +528,7 @@ var SPAN_KIND_MAP = {
527
528
  function normalizeHexId(id) {
528
529
  if (!id) return "";
529
530
  const isBase64Like = /^[A-Za-z0-9+/=]+$/.test(id) && !/^[0-9a-f]+$/i.test(id);
530
- const isLikelyBase64Id = isBase64Like && (id.length === 24 || id.length === 28 || id.length === 44 || id.length === 48);
531
+ const isLikelyBase64Id = isBase64Like && (id.length === 12 || id.length === 24 || id.length === 28 || id.length === 44 || id.length === 48);
531
532
  if (isLikelyBase64Id) {
532
533
  try {
533
534
  const bytes = Buffer.from(id, "base64");
@@ -659,13 +660,255 @@ async function readJsonBody(req) {
659
660
  req.on("error", reject);
660
661
  });
661
662
  }
663
+ async function readRawBody(req) {
664
+ return new Promise((resolve2, reject) => {
665
+ const chunks = [];
666
+ req.on("data", (chunk) => chunks.push(chunk));
667
+ req.on("end", () => resolve2(Buffer.concat(chunks)));
668
+ req.on("error", reject);
669
+ });
670
+ }
671
+ function isProtobufContentType(contentType) {
672
+ if (!contentType) return false;
673
+ const value = contentType.toLowerCase();
674
+ return value.includes("application/x-protobuf") || value.includes("application/protobuf");
675
+ }
662
676
  function sendJson(res, status, data) {
663
677
  const body = JSON.stringify(data);
664
678
  res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
665
679
  res.end(body);
666
680
  }
681
+ var COMMON_PROTO = `
682
+ syntax = "proto3";
683
+ package opentelemetry.proto.common.v1;
684
+
685
+ message AnyValue {
686
+ oneof value {
687
+ string string_value = 1;
688
+ bool bool_value = 2;
689
+ int64 int_value = 3;
690
+ double double_value = 4;
691
+ ArrayValue array_value = 5;
692
+ KeyValueList kvlist_value = 6;
693
+ bytes bytes_value = 7;
694
+ }
695
+ }
696
+ message ArrayValue { repeated AnyValue values = 1; }
697
+ message KeyValueList { repeated KeyValue values = 1; }
698
+ message KeyValue {
699
+ string key = 1;
700
+ AnyValue value = 2;
701
+ }
702
+ message InstrumentationScope {
703
+ string name = 1;
704
+ string version = 2;
705
+ repeated KeyValue attributes = 3;
706
+ uint32 dropped_attributes_count = 4;
707
+ }
708
+ `;
709
+ var RESOURCE_PROTO = `
710
+ syntax = "proto3";
711
+ package opentelemetry.proto.resource.v1;
712
+
713
+ message Resource {
714
+ repeated opentelemetry.proto.common.v1.KeyValue attributes = 1;
715
+ uint32 dropped_attributes_count = 2;
716
+ }
717
+ `;
718
+ var TRACE_PROTO = `
719
+ syntax = "proto3";
720
+ package opentelemetry.proto.trace.v1;
721
+
722
+ message ResourceSpans {
723
+ opentelemetry.proto.resource.v1.Resource resource = 1;
724
+ repeated ScopeSpans scope_spans = 2;
725
+ string schema_url = 3;
726
+ }
727
+ message ScopeSpans {
728
+ opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
729
+ repeated Span spans = 2;
730
+ string schema_url = 3;
731
+ }
732
+ message Span {
733
+ bytes trace_id = 1;
734
+ bytes span_id = 2;
735
+ string trace_state = 3;
736
+ bytes parent_span_id = 4;
737
+ fixed32 flags = 16;
738
+ string name = 5;
739
+ SpanKind kind = 6;
740
+ fixed64 start_time_unix_nano = 7;
741
+ fixed64 end_time_unix_nano = 8;
742
+ repeated opentelemetry.proto.common.v1.KeyValue attributes = 9;
743
+ uint32 dropped_attributes_count = 10;
744
+ repeated Event events = 11;
745
+ uint32 dropped_events_count = 12;
746
+ repeated Link links = 13;
747
+ uint32 dropped_links_count = 14;
748
+ Status status = 15;
749
+
750
+ enum SpanKind {
751
+ SPAN_KIND_UNSPECIFIED = 0;
752
+ SPAN_KIND_INTERNAL = 1;
753
+ SPAN_KIND_SERVER = 2;
754
+ SPAN_KIND_CLIENT = 3;
755
+ SPAN_KIND_PRODUCER = 4;
756
+ SPAN_KIND_CONSUMER = 5;
757
+ }
758
+ message Event {
759
+ fixed64 time_unix_nano = 1;
760
+ string name = 2;
761
+ repeated opentelemetry.proto.common.v1.KeyValue attributes = 3;
762
+ uint32 dropped_attributes_count = 4;
763
+ }
764
+ message Link {
765
+ bytes trace_id = 1;
766
+ bytes span_id = 2;
767
+ string trace_state = 3;
768
+ repeated opentelemetry.proto.common.v1.KeyValue attributes = 4;
769
+ uint32 dropped_attributes_count = 5;
770
+ fixed32 flags = 6;
771
+ }
772
+ }
773
+ message Status {
774
+ reserved 1;
775
+ string message = 2;
776
+ StatusCode code = 3;
777
+
778
+ enum StatusCode {
779
+ STATUS_CODE_UNSET = 0;
780
+ STATUS_CODE_OK = 1;
781
+ STATUS_CODE_ERROR = 2;
782
+ }
783
+ }
784
+ message ExportTraceServiceRequest {
785
+ repeated ResourceSpans resource_spans = 1;
786
+ }
787
+ `;
788
+ var LOGS_PROTO = `
789
+ syntax = "proto3";
790
+ package opentelemetry.proto.logs.v1;
791
+
792
+ enum SeverityNumber {
793
+ SEVERITY_NUMBER_UNSPECIFIED = 0;
794
+ SEVERITY_NUMBER_TRACE = 1;
795
+ SEVERITY_NUMBER_TRACE2 = 2;
796
+ SEVERITY_NUMBER_TRACE3 = 3;
797
+ SEVERITY_NUMBER_TRACE4 = 4;
798
+ SEVERITY_NUMBER_DEBUG = 5;
799
+ SEVERITY_NUMBER_DEBUG2 = 6;
800
+ SEVERITY_NUMBER_DEBUG3 = 7;
801
+ SEVERITY_NUMBER_DEBUG4 = 8;
802
+ SEVERITY_NUMBER_INFO = 9;
803
+ SEVERITY_NUMBER_INFO2 = 10;
804
+ SEVERITY_NUMBER_INFO3 = 11;
805
+ SEVERITY_NUMBER_INFO4 = 12;
806
+ SEVERITY_NUMBER_WARN = 13;
807
+ SEVERITY_NUMBER_WARN2 = 14;
808
+ SEVERITY_NUMBER_WARN3 = 15;
809
+ SEVERITY_NUMBER_WARN4 = 16;
810
+ SEVERITY_NUMBER_ERROR = 17;
811
+ SEVERITY_NUMBER_ERROR2 = 18;
812
+ SEVERITY_NUMBER_ERROR3 = 19;
813
+ SEVERITY_NUMBER_ERROR4 = 20;
814
+ SEVERITY_NUMBER_FATAL = 21;
815
+ SEVERITY_NUMBER_FATAL2 = 22;
816
+ SEVERITY_NUMBER_FATAL3 = 23;
817
+ SEVERITY_NUMBER_FATAL4 = 24;
818
+ }
819
+ message ResourceLogs {
820
+ opentelemetry.proto.resource.v1.Resource resource = 1;
821
+ repeated ScopeLogs scope_logs = 2;
822
+ string schema_url = 3;
823
+ }
824
+ message ScopeLogs {
825
+ opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
826
+ repeated LogRecord log_records = 2;
827
+ string schema_url = 3;
828
+ }
829
+ message LogRecord {
830
+ reserved 4;
831
+ fixed64 time_unix_nano = 1;
832
+ fixed64 observed_time_unix_nano = 11;
833
+ SeverityNumber severity_number = 2;
834
+ string severity_text = 3;
835
+ opentelemetry.proto.common.v1.AnyValue body = 5;
836
+ repeated opentelemetry.proto.common.v1.KeyValue attributes = 6;
837
+ uint32 dropped_attributes_count = 7;
838
+ fixed32 flags = 8;
839
+ bytes trace_id = 9;
840
+ bytes span_id = 10;
841
+ }
842
+ message ExportLogsServiceRequest {
843
+ repeated ResourceLogs resource_logs = 1;
844
+ }
845
+ `;
846
+ var METRICS_PROTO = `
847
+ syntax = "proto3";
848
+ package opentelemetry.proto.metrics.v1;
849
+
850
+ message ResourceMetrics {
851
+ opentelemetry.proto.resource.v1.Resource resource = 1;
852
+ repeated ScopeMetrics scope_metrics = 2;
853
+ string schema_url = 3;
854
+ }
855
+ message ScopeMetrics {
856
+ opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
857
+ repeated Metric metrics = 2;
858
+ string schema_url = 3;
859
+ }
860
+ message Metric {
861
+ string name = 1;
862
+ string description = 2;
863
+ string unit = 3;
864
+ }
865
+ message ExportMetricsServiceRequest {
866
+ repeated ResourceMetrics resource_metrics = 1;
867
+ }
868
+ `;
869
+ var TO_OBJECT_OPTIONS = {
870
+ longs: String,
871
+ bytes: String,
872
+ defaults: false
873
+ };
874
+ var cachedRoot = null;
875
+ function getRoot() {
876
+ if (cachedRoot) return cachedRoot;
877
+ const root = new protobuf.Root();
878
+ for (const source of [COMMON_PROTO, RESOURCE_PROTO, TRACE_PROTO, LOGS_PROTO, METRICS_PROTO]) {
879
+ protobuf.parse(source, root, { keepCase: false });
880
+ }
881
+ root.resolveAll();
882
+ cachedRoot = root;
883
+ return root;
884
+ }
885
+ function decodeRequest(typeName, body) {
886
+ const messageType = getRoot().lookupType(typeName);
887
+ const message = messageType.decode(body);
888
+ return messageType.toObject(message, TO_OBJECT_OPTIONS);
889
+ }
890
+ function decodeOtlpTraceRequest(body) {
891
+ return decodeRequest("opentelemetry.proto.trace.v1.ExportTraceServiceRequest", body);
892
+ }
893
+ function decodeOtlpLogsRequest(body) {
894
+ return decodeRequest("opentelemetry.proto.logs.v1.ExportLogsServiceRequest", body);
895
+ }
896
+ function decodeOtlpMetricsRequest(body) {
897
+ return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
898
+ }
667
899
 
668
900
  // src/server/http.ts
901
+ var PROTOBUF_DECODERS = {
902
+ traces: decodeOtlpTraceRequest,
903
+ logs: decodeOtlpLogsRequest,
904
+ metrics: decodeOtlpMetricsRequest
905
+ };
906
+ async function readOtlpPayload(req, signal) {
907
+ if (isProtobufContentType(req.headers["content-type"])) {
908
+ return PROTOBUF_DECODERS[signal](await readRawBody(req));
909
+ }
910
+ return readJsonBody(req);
911
+ }
669
912
  function findPackageRoot() {
670
913
  let dir = dirname(fileURLToPath(import.meta.url));
671
914
  for (let i = 0; i < 5; i++) {
@@ -699,7 +942,7 @@ function getWidgetJs() {
699
942
  function attachDevtoolsRoutes(httpServer, devtools) {
700
943
  httpServer.on("request", async (req, res) => {
701
944
  res.setHeader("Access-Control-Allow-Origin", "*");
702
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
945
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
703
946
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
704
947
  if (req.method === "OPTIONS") {
705
948
  res.writeHead(204);
@@ -722,41 +965,99 @@ function attachDevtoolsRoutes(httpServer, devtools) {
722
965
  sendJson(res, 200, { ok: true, clients: devtools.clientCount });
723
966
  return;
724
967
  }
968
+ if (req.method === "GET" && url === "/v1/traces") {
969
+ const data = devtools.getCurrentData();
970
+ sendJson(res, 200, { traces: data.traces, count: data.traces.length });
971
+ return;
972
+ }
973
+ if (req.method === "DELETE" && url === "/v1/traces") {
974
+ devtools.clearData();
975
+ sendJson(res, 200, { cleared: true });
976
+ return;
977
+ }
725
978
  if (req.method === "POST" && url === "/v1/traces") {
726
979
  try {
727
- const payload = await readJsonBody(req);
980
+ const payload = await readOtlpPayload(req, "traces");
728
981
  const traces = parseOtlpTraces(payload);
729
982
  devtools.addTraces(traces);
730
983
  sendJson(res, 200, { acceptedTraces: traces.length });
731
984
  } catch (e) {
732
- sendJson(res, 400, { error: "Invalid OTLP JSON", message: e instanceof Error ? e.message : String(e) });
985
+ sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
733
986
  }
734
987
  return;
735
988
  }
736
989
  if (req.method === "POST" && url === "/v1/logs") {
737
990
  try {
738
- const payload = await readJsonBody(req);
991
+ const payload = await readOtlpPayload(req, "logs");
739
992
  const logs = parseOtlpLogs(payload);
740
993
  devtools.addLogs(logs);
741
994
  sendJson(res, 200, { acceptedLogs: logs.length });
742
995
  } catch (e) {
743
- sendJson(res, 400, { error: "Invalid OTLP JSON", message: e instanceof Error ? e.message : String(e) });
996
+ sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
744
997
  }
745
998
  return;
746
999
  }
747
1000
  if (req.method === "POST" && url === "/v1/metrics") {
748
1001
  try {
749
- const payload = await readJsonBody(req);
1002
+ const payload = await readOtlpPayload(req, "metrics");
750
1003
  const count = countOtlpMetrics(payload);
751
1004
  sendJson(res, 200, { acceptedMetrics: count });
752
1005
  } catch (e) {
753
- sendJson(res, 400, { error: "Invalid OTLP JSON", message: e instanceof Error ? e.message : String(e) });
1006
+ sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
754
1007
  }
755
1008
  return;
756
1009
  }
757
1010
  sendJson(res, 404, { error: "Not found" });
758
1011
  });
759
1012
  }
1013
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
1014
+ function formatAddress(host, port) {
1015
+ return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
1016
+ }
1017
+ function listenLoopbackDualStack(args) {
1018
+ const { primary, port, host, attachSecondary } = args;
1019
+ let sibling;
1020
+ const ready = new Promise(
1021
+ (resolve2) => {
1022
+ const addresses = [];
1023
+ const warnings = [];
1024
+ const primaryHost = host === "localhost" ? "127.0.0.1" : host;
1025
+ primary.listen(port, primaryHost, () => {
1026
+ const addr = primary.address();
1027
+ const resolvedPort = addr && typeof addr === "object" ? addr.port : port;
1028
+ addresses.push(formatAddress(primaryHost, resolvedPort));
1029
+ if (!LOOPBACK.has(host)) {
1030
+ resolve2({ addresses, warnings });
1031
+ return;
1032
+ }
1033
+ const siblingHost = primaryHost === "::1" ? "127.0.0.1" : "::1";
1034
+ const s = createServer();
1035
+ attachSecondary(s);
1036
+ const onError = (e) => {
1037
+ s.close();
1038
+ warnings.push(
1039
+ `could not also bind ${formatAddress(siblingHost, resolvedPort)} (${e.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
1040
+ );
1041
+ resolve2({ addresses, warnings });
1042
+ };
1043
+ s.once("error", onError);
1044
+ s.listen(resolvedPort, siblingHost, () => {
1045
+ s.off("error", onError);
1046
+ sibling = s;
1047
+ addresses.push(formatAddress(siblingHost, resolvedPort));
1048
+ resolve2({ addresses, warnings });
1049
+ });
1050
+ });
1051
+ }
1052
+ );
1053
+ return {
1054
+ ready,
1055
+ closeSibling: () => new Promise((res) => {
1056
+ if (!sibling) return res();
1057
+ sibling.close(() => res());
1058
+ })
1059
+ };
1060
+ }
760
1061
 
761
1062
  // src/server/exporter.ts
762
1063
  var DevtoolsSpanExporter = class {
@@ -1219,7 +1520,17 @@ function createDevtools(options = {}) {
1219
1520
  maxMetricCount: options.maxMetricCount
1220
1521
  });
1221
1522
  attachDevtoolsRoutes(httpServer, wsServer);
1222
- httpServer.listen(port, host);
1523
+ const listeners = listenLoopbackDualStack({
1524
+ primary: httpServer,
1525
+ port,
1526
+ host,
1527
+ attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
1528
+ });
1529
+ if (options.verbose) {
1530
+ listeners.ready.then(({ warnings }) => {
1531
+ for (const w of warnings) console.warn(`[autotel-devtools] ${w}`);
1532
+ });
1533
+ }
1223
1534
  const exporter = new DevtoolsSpanExporter(wsServer);
1224
1535
  return {
1225
1536
  server: wsServer,
@@ -1228,6 +1539,7 @@ function createDevtools(options = {}) {
1228
1539
  port,
1229
1540
  close: async () => {
1230
1541
  await wsServer.close();
1542
+ await listeners.closeSibling();
1231
1543
  }
1232
1544
  };
1233
1545
  }