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/README.md +69 -4
- package/dist/cli.cjs +367 -26
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +347 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +341 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +321 -9
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +285 -8
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +14 -1
- package/dist/server/index.d.ts +14 -1
- package/dist/server/index.js +262 -9
- package/dist/server/index.js.map +1 -1
- package/dist/widget.global.js +2 -2
- package/package.json +22 -20
- package/skills/autotel-devtools/SKILL.md +9 -5
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@ Standalone OTLP receiver with web UI for local development. Think TanStack Devto
|
|
|
16
16
|
│ npx autotel-devtools │
|
|
17
17
|
│ ┌───────────────────────────────────────┐ │
|
|
18
18
|
│ │ HTTP Server (port 4318) │ │
|
|
19
|
-
│ │ ├── POST /v1/traces ← OTLP JSON
|
|
20
|
-
│ │ ├── POST /v1/logs ← OTLP JSON
|
|
21
|
-
│ │ ├── POST /v1/metrics ← OTLP JSON
|
|
19
|
+
│ │ ├── POST /v1/traces ← OTLP JSON/PB │ │
|
|
20
|
+
│ │ ├── POST /v1/logs ← OTLP JSON/PB │ │
|
|
21
|
+
│ │ ├── POST /v1/metrics ← OTLP JSON/PB │ │
|
|
22
22
|
│ │ ├── GET / → Full page UI │ │
|
|
23
23
|
│ │ ├── GET /widget.js → Widget bundle │ │
|
|
24
24
|
│ │ ├── GET /healthz → Health check │ │
|
|
@@ -41,6 +41,16 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
|
|
|
41
41
|
node app.js
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
The endpoints accept **both OTLP/JSON and OTLP/protobuf** (`application/x-protobuf`),
|
|
45
|
+
selected automatically from the request `Content-Type`. That means SDKs that default
|
|
46
|
+
to protobuf over OTLP HTTP — including the Python, Java, and Go OpenTelemetry SDKs —
|
|
47
|
+
work without any extra configuration:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Python / Java / Go SDKs default to http/protobuf — just point them at the receiver
|
|
51
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 python app.py
|
|
52
|
+
```
|
|
53
|
+
|
|
44
54
|
Open http://localhost:4318 to see traces, logs, and metrics.
|
|
45
55
|
|
|
46
56
|
### Embedded Widget
|
|
@@ -100,7 +110,7 @@ const myFunction = trace((ctx) => async () => {
|
|
|
100
110
|
### Server (Node.js)
|
|
101
111
|
|
|
102
112
|
- **DevtoolsServer** - WebSocket server + in-memory data store
|
|
103
|
-
- **HTTP Routes** - OTLP receivers for traces/logs/metrics
|
|
113
|
+
- **HTTP Routes** - OTLP receivers for traces/logs/metrics (JSON + protobuf)
|
|
104
114
|
- **Exporters** - OpenTelemetry span/log exporters
|
|
105
115
|
|
|
106
116
|
### Widget (Preact)
|
|
@@ -151,6 +161,61 @@ Options:
|
|
|
151
161
|
- `--host, -H` - Host to bind to (default: 127.0.0.1)
|
|
152
162
|
- `--title, -t` - Dashboard title
|
|
153
163
|
|
|
164
|
+
When bound to a loopback host, the receiver listens on **both** `127.0.0.1`
|
|
165
|
+
and `::1`, so a client connecting via `localhost` reaches it regardless of how
|
|
166
|
+
the OS resolves `localhost` (macOS prefers IPv6 `::1`). The startup banner
|
|
167
|
+
prints every address it bound; if a family can't be bound you get a warning,
|
|
168
|
+
not a silent black hole.
|
|
169
|
+
|
|
170
|
+
## Behind a dev-server proxy
|
|
171
|
+
|
|
172
|
+
If your app's dev server proxies `/v1/traces` to the receiver, two classic
|
|
173
|
+
bugs make spans vanish with **no error** — both worth knowing:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// Express / http-proxy-middleware
|
|
177
|
+
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
178
|
+
|
|
179
|
+
app.use(
|
|
180
|
+
'/v1/traces',
|
|
181
|
+
createProxyMiddleware({
|
|
182
|
+
// (a) Express strips the mount prefix before calling middleware, so the
|
|
183
|
+
// proxy would otherwise forward "/" instead of "/v1/traces".
|
|
184
|
+
pathRewrite: () => '/v1/traces',
|
|
185
|
+
// (b) Use 127.0.0.1, NOT localhost. On macOS `localhost` resolves to ::1;
|
|
186
|
+
// pin the family so you reach the receiver deterministically.
|
|
187
|
+
target: 'http://127.0.0.1:4318',
|
|
188
|
+
changeOrigin: true,
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
> Symptom of either bug: the browser shows the request leaving (200/no error),
|
|
194
|
+
> but the receiver stays empty. Always verify on the **receiver** side (below),
|
|
195
|
+
> not just that the browser tried to send.
|
|
196
|
+
|
|
197
|
+
## Verifying ingestion in tests
|
|
198
|
+
|
|
199
|
+
The receiver exposes an HTTP read-back so a test can assert the collector
|
|
200
|
+
**actually received** spans — instead of asserting "the client tried to send",
|
|
201
|
+
which a browser-level route intercept can fake (it fulfils the request before
|
|
202
|
+
it reaches any server):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
GET /v1/traces # → { traces: [...], count: N } what the receiver has
|
|
206
|
+
DELETE /v1/traces # clear captured telemetry (reset between tests)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
// Playwright / integration test — bypass any page.route() intercept and ask
|
|
211
|
+
// the collector directly.
|
|
212
|
+
await fetch(`${RECEIVER}/v1/traces`, { method: 'DELETE' }); // reset
|
|
213
|
+
await runTheUserFlow(); // app emits spans
|
|
214
|
+
await expect
|
|
215
|
+
.poll(async () => (await (await fetch(`${RECEIVER}/v1/traces`)).json()).count)
|
|
216
|
+
.toBeGreaterThan(0);
|
|
217
|
+
```
|
|
218
|
+
|
|
154
219
|
## License
|
|
155
220
|
|
|
156
221
|
MIT
|
package/dist/cli.cjs
CHANGED
|
@@ -6,8 +6,29 @@ var fs = require('fs');
|
|
|
6
6
|
var path = require('path');
|
|
7
7
|
var url = require('url');
|
|
8
8
|
var ws = require('ws');
|
|
9
|
+
var protobuf = require('protobufjs');
|
|
9
10
|
|
|
10
11
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
12
|
+
function _interopNamespace(e) {
|
|
13
|
+
if (e && e.__esModule) return e;
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var protobuf__namespace = /*#__PURE__*/_interopNamespace(protobuf);
|
|
31
|
+
|
|
11
32
|
// src/server/error-aggregator.ts
|
|
12
33
|
var ErrorAggregator = class {
|
|
13
34
|
errorGroups = /* @__PURE__ */ new Map();
|
|
@@ -528,7 +549,7 @@ var SPAN_KIND_MAP = {
|
|
|
528
549
|
function normalizeHexId(id) {
|
|
529
550
|
if (!id) return "";
|
|
530
551
|
const isBase64Like = /^[A-Za-z0-9+/=]+$/.test(id) && !/^[0-9a-f]+$/i.test(id);
|
|
531
|
-
const isLikelyBase64Id = isBase64Like && (id.length === 24 || id.length === 28 || id.length === 44 || id.length === 48);
|
|
552
|
+
const isLikelyBase64Id = isBase64Like && (id.length === 12 || id.length === 24 || id.length === 28 || id.length === 44 || id.length === 48);
|
|
532
553
|
if (isLikelyBase64Id) {
|
|
533
554
|
try {
|
|
534
555
|
const bytes = Buffer.from(id, "base64");
|
|
@@ -660,13 +681,255 @@ async function readJsonBody(req) {
|
|
|
660
681
|
req.on("error", reject);
|
|
661
682
|
});
|
|
662
683
|
}
|
|
684
|
+
async function readRawBody(req) {
|
|
685
|
+
return new Promise((resolve3, reject) => {
|
|
686
|
+
const chunks = [];
|
|
687
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
688
|
+
req.on("end", () => resolve3(Buffer.concat(chunks)));
|
|
689
|
+
req.on("error", reject);
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
function isProtobufContentType(contentType) {
|
|
693
|
+
if (!contentType) return false;
|
|
694
|
+
const value = contentType.toLowerCase();
|
|
695
|
+
return value.includes("application/x-protobuf") || value.includes("application/protobuf");
|
|
696
|
+
}
|
|
663
697
|
function sendJson(res, status, data) {
|
|
664
698
|
const body = JSON.stringify(data);
|
|
665
699
|
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
|
|
666
700
|
res.end(body);
|
|
667
701
|
}
|
|
702
|
+
var COMMON_PROTO = `
|
|
703
|
+
syntax = "proto3";
|
|
704
|
+
package opentelemetry.proto.common.v1;
|
|
705
|
+
|
|
706
|
+
message AnyValue {
|
|
707
|
+
oneof value {
|
|
708
|
+
string string_value = 1;
|
|
709
|
+
bool bool_value = 2;
|
|
710
|
+
int64 int_value = 3;
|
|
711
|
+
double double_value = 4;
|
|
712
|
+
ArrayValue array_value = 5;
|
|
713
|
+
KeyValueList kvlist_value = 6;
|
|
714
|
+
bytes bytes_value = 7;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
message ArrayValue { repeated AnyValue values = 1; }
|
|
718
|
+
message KeyValueList { repeated KeyValue values = 1; }
|
|
719
|
+
message KeyValue {
|
|
720
|
+
string key = 1;
|
|
721
|
+
AnyValue value = 2;
|
|
722
|
+
}
|
|
723
|
+
message InstrumentationScope {
|
|
724
|
+
string name = 1;
|
|
725
|
+
string version = 2;
|
|
726
|
+
repeated KeyValue attributes = 3;
|
|
727
|
+
uint32 dropped_attributes_count = 4;
|
|
728
|
+
}
|
|
729
|
+
`;
|
|
730
|
+
var RESOURCE_PROTO = `
|
|
731
|
+
syntax = "proto3";
|
|
732
|
+
package opentelemetry.proto.resource.v1;
|
|
733
|
+
|
|
734
|
+
message Resource {
|
|
735
|
+
repeated opentelemetry.proto.common.v1.KeyValue attributes = 1;
|
|
736
|
+
uint32 dropped_attributes_count = 2;
|
|
737
|
+
}
|
|
738
|
+
`;
|
|
739
|
+
var TRACE_PROTO = `
|
|
740
|
+
syntax = "proto3";
|
|
741
|
+
package opentelemetry.proto.trace.v1;
|
|
742
|
+
|
|
743
|
+
message ResourceSpans {
|
|
744
|
+
opentelemetry.proto.resource.v1.Resource resource = 1;
|
|
745
|
+
repeated ScopeSpans scope_spans = 2;
|
|
746
|
+
string schema_url = 3;
|
|
747
|
+
}
|
|
748
|
+
message ScopeSpans {
|
|
749
|
+
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
|
750
|
+
repeated Span spans = 2;
|
|
751
|
+
string schema_url = 3;
|
|
752
|
+
}
|
|
753
|
+
message Span {
|
|
754
|
+
bytes trace_id = 1;
|
|
755
|
+
bytes span_id = 2;
|
|
756
|
+
string trace_state = 3;
|
|
757
|
+
bytes parent_span_id = 4;
|
|
758
|
+
fixed32 flags = 16;
|
|
759
|
+
string name = 5;
|
|
760
|
+
SpanKind kind = 6;
|
|
761
|
+
fixed64 start_time_unix_nano = 7;
|
|
762
|
+
fixed64 end_time_unix_nano = 8;
|
|
763
|
+
repeated opentelemetry.proto.common.v1.KeyValue attributes = 9;
|
|
764
|
+
uint32 dropped_attributes_count = 10;
|
|
765
|
+
repeated Event events = 11;
|
|
766
|
+
uint32 dropped_events_count = 12;
|
|
767
|
+
repeated Link links = 13;
|
|
768
|
+
uint32 dropped_links_count = 14;
|
|
769
|
+
Status status = 15;
|
|
770
|
+
|
|
771
|
+
enum SpanKind {
|
|
772
|
+
SPAN_KIND_UNSPECIFIED = 0;
|
|
773
|
+
SPAN_KIND_INTERNAL = 1;
|
|
774
|
+
SPAN_KIND_SERVER = 2;
|
|
775
|
+
SPAN_KIND_CLIENT = 3;
|
|
776
|
+
SPAN_KIND_PRODUCER = 4;
|
|
777
|
+
SPAN_KIND_CONSUMER = 5;
|
|
778
|
+
}
|
|
779
|
+
message Event {
|
|
780
|
+
fixed64 time_unix_nano = 1;
|
|
781
|
+
string name = 2;
|
|
782
|
+
repeated opentelemetry.proto.common.v1.KeyValue attributes = 3;
|
|
783
|
+
uint32 dropped_attributes_count = 4;
|
|
784
|
+
}
|
|
785
|
+
message Link {
|
|
786
|
+
bytes trace_id = 1;
|
|
787
|
+
bytes span_id = 2;
|
|
788
|
+
string trace_state = 3;
|
|
789
|
+
repeated opentelemetry.proto.common.v1.KeyValue attributes = 4;
|
|
790
|
+
uint32 dropped_attributes_count = 5;
|
|
791
|
+
fixed32 flags = 6;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
message Status {
|
|
795
|
+
reserved 1;
|
|
796
|
+
string message = 2;
|
|
797
|
+
StatusCode code = 3;
|
|
798
|
+
|
|
799
|
+
enum StatusCode {
|
|
800
|
+
STATUS_CODE_UNSET = 0;
|
|
801
|
+
STATUS_CODE_OK = 1;
|
|
802
|
+
STATUS_CODE_ERROR = 2;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
message ExportTraceServiceRequest {
|
|
806
|
+
repeated ResourceSpans resource_spans = 1;
|
|
807
|
+
}
|
|
808
|
+
`;
|
|
809
|
+
var LOGS_PROTO = `
|
|
810
|
+
syntax = "proto3";
|
|
811
|
+
package opentelemetry.proto.logs.v1;
|
|
812
|
+
|
|
813
|
+
enum SeverityNumber {
|
|
814
|
+
SEVERITY_NUMBER_UNSPECIFIED = 0;
|
|
815
|
+
SEVERITY_NUMBER_TRACE = 1;
|
|
816
|
+
SEVERITY_NUMBER_TRACE2 = 2;
|
|
817
|
+
SEVERITY_NUMBER_TRACE3 = 3;
|
|
818
|
+
SEVERITY_NUMBER_TRACE4 = 4;
|
|
819
|
+
SEVERITY_NUMBER_DEBUG = 5;
|
|
820
|
+
SEVERITY_NUMBER_DEBUG2 = 6;
|
|
821
|
+
SEVERITY_NUMBER_DEBUG3 = 7;
|
|
822
|
+
SEVERITY_NUMBER_DEBUG4 = 8;
|
|
823
|
+
SEVERITY_NUMBER_INFO = 9;
|
|
824
|
+
SEVERITY_NUMBER_INFO2 = 10;
|
|
825
|
+
SEVERITY_NUMBER_INFO3 = 11;
|
|
826
|
+
SEVERITY_NUMBER_INFO4 = 12;
|
|
827
|
+
SEVERITY_NUMBER_WARN = 13;
|
|
828
|
+
SEVERITY_NUMBER_WARN2 = 14;
|
|
829
|
+
SEVERITY_NUMBER_WARN3 = 15;
|
|
830
|
+
SEVERITY_NUMBER_WARN4 = 16;
|
|
831
|
+
SEVERITY_NUMBER_ERROR = 17;
|
|
832
|
+
SEVERITY_NUMBER_ERROR2 = 18;
|
|
833
|
+
SEVERITY_NUMBER_ERROR3 = 19;
|
|
834
|
+
SEVERITY_NUMBER_ERROR4 = 20;
|
|
835
|
+
SEVERITY_NUMBER_FATAL = 21;
|
|
836
|
+
SEVERITY_NUMBER_FATAL2 = 22;
|
|
837
|
+
SEVERITY_NUMBER_FATAL3 = 23;
|
|
838
|
+
SEVERITY_NUMBER_FATAL4 = 24;
|
|
839
|
+
}
|
|
840
|
+
message ResourceLogs {
|
|
841
|
+
opentelemetry.proto.resource.v1.Resource resource = 1;
|
|
842
|
+
repeated ScopeLogs scope_logs = 2;
|
|
843
|
+
string schema_url = 3;
|
|
844
|
+
}
|
|
845
|
+
message ScopeLogs {
|
|
846
|
+
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
|
847
|
+
repeated LogRecord log_records = 2;
|
|
848
|
+
string schema_url = 3;
|
|
849
|
+
}
|
|
850
|
+
message LogRecord {
|
|
851
|
+
reserved 4;
|
|
852
|
+
fixed64 time_unix_nano = 1;
|
|
853
|
+
fixed64 observed_time_unix_nano = 11;
|
|
854
|
+
SeverityNumber severity_number = 2;
|
|
855
|
+
string severity_text = 3;
|
|
856
|
+
opentelemetry.proto.common.v1.AnyValue body = 5;
|
|
857
|
+
repeated opentelemetry.proto.common.v1.KeyValue attributes = 6;
|
|
858
|
+
uint32 dropped_attributes_count = 7;
|
|
859
|
+
fixed32 flags = 8;
|
|
860
|
+
bytes trace_id = 9;
|
|
861
|
+
bytes span_id = 10;
|
|
862
|
+
}
|
|
863
|
+
message ExportLogsServiceRequest {
|
|
864
|
+
repeated ResourceLogs resource_logs = 1;
|
|
865
|
+
}
|
|
866
|
+
`;
|
|
867
|
+
var METRICS_PROTO = `
|
|
868
|
+
syntax = "proto3";
|
|
869
|
+
package opentelemetry.proto.metrics.v1;
|
|
870
|
+
|
|
871
|
+
message ResourceMetrics {
|
|
872
|
+
opentelemetry.proto.resource.v1.Resource resource = 1;
|
|
873
|
+
repeated ScopeMetrics scope_metrics = 2;
|
|
874
|
+
string schema_url = 3;
|
|
875
|
+
}
|
|
876
|
+
message ScopeMetrics {
|
|
877
|
+
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
|
878
|
+
repeated Metric metrics = 2;
|
|
879
|
+
string schema_url = 3;
|
|
880
|
+
}
|
|
881
|
+
message Metric {
|
|
882
|
+
string name = 1;
|
|
883
|
+
string description = 2;
|
|
884
|
+
string unit = 3;
|
|
885
|
+
}
|
|
886
|
+
message ExportMetricsServiceRequest {
|
|
887
|
+
repeated ResourceMetrics resource_metrics = 1;
|
|
888
|
+
}
|
|
889
|
+
`;
|
|
890
|
+
var TO_OBJECT_OPTIONS = {
|
|
891
|
+
longs: String,
|
|
892
|
+
bytes: String,
|
|
893
|
+
defaults: false
|
|
894
|
+
};
|
|
895
|
+
var cachedRoot = null;
|
|
896
|
+
function getRoot() {
|
|
897
|
+
if (cachedRoot) return cachedRoot;
|
|
898
|
+
const root = new protobuf__namespace.Root();
|
|
899
|
+
for (const source of [COMMON_PROTO, RESOURCE_PROTO, TRACE_PROTO, LOGS_PROTO, METRICS_PROTO]) {
|
|
900
|
+
protobuf__namespace.parse(source, root, { keepCase: false });
|
|
901
|
+
}
|
|
902
|
+
root.resolveAll();
|
|
903
|
+
cachedRoot = root;
|
|
904
|
+
return root;
|
|
905
|
+
}
|
|
906
|
+
function decodeRequest(typeName, body) {
|
|
907
|
+
const messageType = getRoot().lookupType(typeName);
|
|
908
|
+
const message = messageType.decode(body);
|
|
909
|
+
return messageType.toObject(message, TO_OBJECT_OPTIONS);
|
|
910
|
+
}
|
|
911
|
+
function decodeOtlpTraceRequest(body) {
|
|
912
|
+
return decodeRequest("opentelemetry.proto.trace.v1.ExportTraceServiceRequest", body);
|
|
913
|
+
}
|
|
914
|
+
function decodeOtlpLogsRequest(body) {
|
|
915
|
+
return decodeRequest("opentelemetry.proto.logs.v1.ExportLogsServiceRequest", body);
|
|
916
|
+
}
|
|
917
|
+
function decodeOtlpMetricsRequest(body) {
|
|
918
|
+
return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
|
|
919
|
+
}
|
|
668
920
|
|
|
669
921
|
// src/server/http.ts
|
|
922
|
+
var PROTOBUF_DECODERS = {
|
|
923
|
+
traces: decodeOtlpTraceRequest,
|
|
924
|
+
logs: decodeOtlpLogsRequest,
|
|
925
|
+
metrics: decodeOtlpMetricsRequest
|
|
926
|
+
};
|
|
927
|
+
async function readOtlpPayload(req, signal) {
|
|
928
|
+
if (isProtobufContentType(req.headers["content-type"])) {
|
|
929
|
+
return PROTOBUF_DECODERS[signal](await readRawBody(req));
|
|
930
|
+
}
|
|
931
|
+
return readJsonBody(req);
|
|
932
|
+
}
|
|
670
933
|
function findPackageRoot() {
|
|
671
934
|
let dir = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
672
935
|
for (let i = 0; i < 5; i++) {
|
|
@@ -700,7 +963,7 @@ function getWidgetJs() {
|
|
|
700
963
|
function attachDevtoolsRoutes(httpServer, devtools) {
|
|
701
964
|
httpServer.on("request", async (req, res) => {
|
|
702
965
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
703
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
966
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
704
967
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
705
968
|
if (req.method === "OPTIONS") {
|
|
706
969
|
res.writeHead(204);
|
|
@@ -723,41 +986,99 @@ function attachDevtoolsRoutes(httpServer, devtools) {
|
|
|
723
986
|
sendJson(res, 200, { ok: true, clients: devtools.clientCount });
|
|
724
987
|
return;
|
|
725
988
|
}
|
|
989
|
+
if (req.method === "GET" && url === "/v1/traces") {
|
|
990
|
+
const data = devtools.getCurrentData();
|
|
991
|
+
sendJson(res, 200, { traces: data.traces, count: data.traces.length });
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (req.method === "DELETE" && url === "/v1/traces") {
|
|
995
|
+
devtools.clearData();
|
|
996
|
+
sendJson(res, 200, { cleared: true });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
726
999
|
if (req.method === "POST" && url === "/v1/traces") {
|
|
727
1000
|
try {
|
|
728
|
-
const payload = await
|
|
1001
|
+
const payload = await readOtlpPayload(req, "traces");
|
|
729
1002
|
const traces = parseOtlpTraces(payload);
|
|
730
1003
|
devtools.addTraces(traces);
|
|
731
1004
|
sendJson(res, 200, { acceptedTraces: traces.length });
|
|
732
1005
|
} catch (e) {
|
|
733
|
-
sendJson(res, 400, { error: "Invalid OTLP
|
|
1006
|
+
sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
|
|
734
1007
|
}
|
|
735
1008
|
return;
|
|
736
1009
|
}
|
|
737
1010
|
if (req.method === "POST" && url === "/v1/logs") {
|
|
738
1011
|
try {
|
|
739
|
-
const payload = await
|
|
1012
|
+
const payload = await readOtlpPayload(req, "logs");
|
|
740
1013
|
const logs = parseOtlpLogs(payload);
|
|
741
1014
|
devtools.addLogs(logs);
|
|
742
1015
|
sendJson(res, 200, { acceptedLogs: logs.length });
|
|
743
1016
|
} catch (e) {
|
|
744
|
-
sendJson(res, 400, { error: "Invalid OTLP
|
|
1017
|
+
sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
|
|
745
1018
|
}
|
|
746
1019
|
return;
|
|
747
1020
|
}
|
|
748
1021
|
if (req.method === "POST" && url === "/v1/metrics") {
|
|
749
1022
|
try {
|
|
750
|
-
const payload = await
|
|
1023
|
+
const payload = await readOtlpPayload(req, "metrics");
|
|
751
1024
|
const count = countOtlpMetrics(payload);
|
|
752
1025
|
sendJson(res, 200, { acceptedMetrics: count });
|
|
753
1026
|
} catch (e) {
|
|
754
|
-
sendJson(res, 400, { error: "Invalid OTLP
|
|
1027
|
+
sendJson(res, 400, { error: "Invalid OTLP payload", message: e instanceof Error ? e.message : String(e) });
|
|
755
1028
|
}
|
|
756
1029
|
return;
|
|
757
1030
|
}
|
|
758
1031
|
sendJson(res, 404, { error: "Not found" });
|
|
759
1032
|
});
|
|
760
1033
|
}
|
|
1034
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1035
|
+
function formatAddress(host, port) {
|
|
1036
|
+
return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
|
|
1037
|
+
}
|
|
1038
|
+
function listenLoopbackDualStack(args) {
|
|
1039
|
+
const { primary, port, host, attachSecondary } = args;
|
|
1040
|
+
let sibling;
|
|
1041
|
+
const ready = new Promise(
|
|
1042
|
+
(resolve3) => {
|
|
1043
|
+
const addresses = [];
|
|
1044
|
+
const warnings = [];
|
|
1045
|
+
const primaryHost = host === "localhost" ? "127.0.0.1" : host;
|
|
1046
|
+
primary.listen(port, primaryHost, () => {
|
|
1047
|
+
const addr = primary.address();
|
|
1048
|
+
const resolvedPort = addr && typeof addr === "object" ? addr.port : port;
|
|
1049
|
+
addresses.push(formatAddress(primaryHost, resolvedPort));
|
|
1050
|
+
if (!LOOPBACK.has(host)) {
|
|
1051
|
+
resolve3({ addresses, warnings });
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const siblingHost = primaryHost === "::1" ? "127.0.0.1" : "::1";
|
|
1055
|
+
const s = http.createServer();
|
|
1056
|
+
attachSecondary(s);
|
|
1057
|
+
const onError = (e) => {
|
|
1058
|
+
s.close();
|
|
1059
|
+
warnings.push(
|
|
1060
|
+
`could not also bind ${formatAddress(siblingHost, resolvedPort)} (${e.message}); clients using the ${siblingHost === "::1" ? "IPv6" : "IPv4"} form of "localhost" may not connect.`
|
|
1061
|
+
);
|
|
1062
|
+
resolve3({ addresses, warnings });
|
|
1063
|
+
};
|
|
1064
|
+
s.once("error", onError);
|
|
1065
|
+
s.listen(resolvedPort, siblingHost, () => {
|
|
1066
|
+
s.off("error", onError);
|
|
1067
|
+
sibling = s;
|
|
1068
|
+
addresses.push(formatAddress(siblingHost, resolvedPort));
|
|
1069
|
+
resolve3({ addresses, warnings });
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
);
|
|
1074
|
+
return {
|
|
1075
|
+
ready,
|
|
1076
|
+
closeSibling: () => new Promise((res) => {
|
|
1077
|
+
if (!sibling) return res();
|
|
1078
|
+
sibling.close(() => res());
|
|
1079
|
+
})
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
761
1082
|
|
|
762
1083
|
// src/cli.ts
|
|
763
1084
|
function printHelp() {
|
|
@@ -775,13 +1096,15 @@ Options:
|
|
|
775
1096
|
-v, --version Show version number
|
|
776
1097
|
|
|
777
1098
|
Endpoints:
|
|
778
|
-
GET
|
|
779
|
-
GET
|
|
780
|
-
POST
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1099
|
+
GET / Web devtools UI (fullpage)
|
|
1100
|
+
GET /widget.js Widget bundle (embed in your app)
|
|
1101
|
+
POST /v1/traces Receive OTLP JSON trace data
|
|
1102
|
+
GET /v1/traces Read back received traces (verify ingestion in tests)
|
|
1103
|
+
DELETE /v1/traces Clear captured telemetry (test reset)
|
|
1104
|
+
POST /v1/logs Receive OTLP JSON log data
|
|
1105
|
+
POST /v1/metrics Receive OTLP JSON metric data
|
|
1106
|
+
WS /ws WebSocket stream for real-time updates
|
|
1107
|
+
GET /healthz Health check
|
|
785
1108
|
|
|
786
1109
|
Examples:
|
|
787
1110
|
npx autotel-devtools
|
|
@@ -853,29 +1176,47 @@ async function main() {
|
|
|
853
1176
|
const httpServer = http.createServer();
|
|
854
1177
|
const wsServer = new DevtoolsServer({ server: httpServer, verbose: true });
|
|
855
1178
|
attachDevtoolsRoutes(httpServer, wsServer);
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1179
|
+
const listeners = listenLoopbackDualStack({
|
|
1180
|
+
primary: httpServer,
|
|
1181
|
+
port: options.port,
|
|
1182
|
+
host: options.host,
|
|
1183
|
+
attachSecondary: (s) => attachDevtoolsRoutes(s, wsServer)
|
|
1184
|
+
});
|
|
1185
|
+
const { addresses, warnings } = await listeners.ready;
|
|
1186
|
+
const uiBase = `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${options.port}`;
|
|
1187
|
+
const title = options.title || "autotel-devtools";
|
|
1188
|
+
process.stdout.write(`
|
|
859
1189
|
${title}
|
|
860
1190
|
|
|
861
1191
|
`);
|
|
862
|
-
|
|
1192
|
+
process.stdout.write(` Listening: ${addresses.join(" + ")}
|
|
1193
|
+
`);
|
|
1194
|
+
process.stdout.write(` UI: ${uiBase}
|
|
863
1195
|
`);
|
|
864
|
-
|
|
1196
|
+
process.stdout.write(` Widget: <script src="${uiBase}/widget.js"></script>
|
|
865
1197
|
`);
|
|
866
|
-
|
|
1198
|
+
process.stdout.write(` WebSocket: ${uiBase.replace("http", "ws")}/ws
|
|
867
1199
|
`);
|
|
868
|
-
|
|
1200
|
+
process.stdout.write(` OTLP: ${uiBase}/v1/traces
|
|
869
1201
|
|
|
870
1202
|
`);
|
|
871
|
-
|
|
1203
|
+
process.stdout.write(` Set OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
|
872
1204
|
`);
|
|
873
|
-
|
|
1205
|
+
process.stdout.write(` Set OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}
|
|
874
1206
|
|
|
875
1207
|
`);
|
|
876
|
-
}
|
|
1208
|
+
process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces
|
|
1209
|
+
|
|
1210
|
+
`);
|
|
1211
|
+
for (const w of warnings) {
|
|
1212
|
+
process.stdout.write(` \u26A0 ${w}
|
|
1213
|
+
`);
|
|
1214
|
+
}
|
|
1215
|
+
if (warnings.length > 0) process.stdout.write("\n");
|
|
877
1216
|
const shutdown = () => {
|
|
878
|
-
wsServer.close().
|
|
1217
|
+
Promise.all([wsServer.close(), listeners.closeSibling()]).then(
|
|
1218
|
+
() => process.exit(0)
|
|
1219
|
+
);
|
|
879
1220
|
};
|
|
880
1221
|
process.on("SIGINT", shutdown);
|
|
881
1222
|
process.on("SIGTERM", shutdown);
|