autotel-devtools 22.0.0 → 23.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 +183 -28
- package/dist/cli.cjs +80 -13
- package/dist/cli.js +80 -13
- package/dist/compile-JDFHUCo5.d.cts +177 -0
- package/dist/compile-JDFHUCo5.d.ts +177 -0
- package/dist/{error-aggregator-D8VKciLY.d.ts → error-aggregator-B52JI8jL.d.ts} +1 -1
- package/dist/{error-aggregator-DCEKMOm3.d.cts → error-aggregator-DSwUvgFF.d.cts} +1 -1
- package/dist/exporter-C88W22vY.d.ts +576 -0
- package/dist/exporter-OoWkSMCR.d.cts +576 -0
- package/dist/fullpage.global.js +39 -0
- package/dist/grpc-D0B3P9sI.cjs +82 -0
- package/dist/grpc-DY-C1jSU.js +77 -0
- package/dist/http-1afd_01N.cjs +3991 -0
- package/dist/http-gk1xapnA.js +3825 -0
- package/dist/index.cjs +25 -7
- package/dist/index.d.cts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +25 -7
- package/dist/{listen-DBfsfcdd.js → listen-D-lLgfro.js} +5 -2
- package/dist/{listen-CEJ3nYJf.cjs → listen-l09RRHht.cjs} +5 -2
- package/dist/parse-BRlosZft.cjs +642 -0
- package/dist/parse-D_RmPPQs.js +612 -0
- package/dist/query/index.cjs +8 -0
- package/dist/query/index.d.cts +16 -0
- package/dist/query/index.d.ts +16 -0
- package/dist/query/index.js +3 -0
- package/dist/server/exporter.d.cts +1 -1
- package/dist/server/exporter.d.ts +1 -1
- package/dist/server/index.cjs +6 -2
- package/dist/server/index.d.cts +19 -6
- package/dist/server/index.d.ts +18 -5
- package/dist/server/index.js +3 -2
- package/dist/types-B0tjwFqj.d.cts +107 -0
- package/dist/types-DM8y4A9Z.d.ts +107 -0
- package/dist/widget.global.js +15 -24
- package/dist/wire/index.cjs +7 -0
- package/dist/wire/index.d.cts +26 -0
- package/dist/wire/index.d.ts +26 -0
- package/dist/wire/index.js +3 -0
- package/dist/wire-2Rmfg6IT.js +51 -0
- package/dist/wire-CHU1PkMo.cjs +75 -0
- package/package.json +21 -5
- package/dist/exporter-Dt4kx128.d.cts +0 -207
- package/dist/exporter-Due9Rd4s.d.ts +0 -207
- package/dist/http-CNZMrnzv.js +0 -1453
- package/dist/http-CXSzX4ee.cjs +0 -1607
package/dist/http-CNZMrnzv.js
DELETED
|
@@ -1,1453 +0,0 @@
|
|
|
1
|
-
import { i as asString, o as stringAttr, t as asObject } from "./json-fields-CPjKZ2WH.js";
|
|
2
|
-
import { t as pickRoot } from "./trace-root-EHnvuA7f.js";
|
|
3
|
-
import { t as getResourceName } from "./resource-utils-B4UVvfnH.js";
|
|
4
|
-
import { createServer } from "node:http";
|
|
5
|
-
import { WebSocket, WebSocketServer } from "ws";
|
|
6
|
-
import { ingestAgentEvents, ingestAgentMetrics } from "autotel-agents";
|
|
7
|
-
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
8
|
-
import path, { dirname, resolve } from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import protobuf from "protobufjs";
|
|
11
|
-
|
|
12
|
-
//#region src/server/error-aggregator.ts
|
|
13
|
-
var ErrorAggregator = class {
|
|
14
|
-
errorGroups = /* @__PURE__ */ new Map();
|
|
15
|
-
options;
|
|
16
|
-
constructor(options = {}) {
|
|
17
|
-
this.options = {
|
|
18
|
-
maxGroups: options.maxGroups ?? 100,
|
|
19
|
-
maxAffectedTraces: options.maxAffectedTraces ?? 10,
|
|
20
|
-
maxAffectedSpans: options.maxAffectedSpans ?? 5,
|
|
21
|
-
stackFramesForFingerprint: options.stackFramesForFingerprint ?? 5
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Add an error occurrence to the aggregator
|
|
26
|
-
*/
|
|
27
|
-
addError(occurrence) {
|
|
28
|
-
const fingerprint = this.generateFingerprint(occurrence);
|
|
29
|
-
const existing = this.errorGroups.get(fingerprint);
|
|
30
|
-
if (existing) {
|
|
31
|
-
existing.count++;
|
|
32
|
-
existing.lastSeen = occurrence.timestamp;
|
|
33
|
-
if (!existing.affectedTraces.includes(occurrence.traceId)) {
|
|
34
|
-
existing.affectedTraces.push(occurrence.traceId);
|
|
35
|
-
if (existing.affectedTraces.length > this.options.maxAffectedTraces) existing.affectedTraces.shift();
|
|
36
|
-
}
|
|
37
|
-
if (!existing.affectedSpans.includes(occurrence.spanName)) {
|
|
38
|
-
existing.affectedSpans.push(occurrence.spanName);
|
|
39
|
-
if (existing.affectedSpans.length > this.options.maxAffectedSpans) existing.affectedSpans.shift();
|
|
40
|
-
}
|
|
41
|
-
return existing;
|
|
42
|
-
}
|
|
43
|
-
const newGroup = {
|
|
44
|
-
fingerprint,
|
|
45
|
-
type: occurrence.error.type,
|
|
46
|
-
message: occurrence.error.message,
|
|
47
|
-
stackTrace: this.normalizeStackTrace(occurrence.error.stackTrace),
|
|
48
|
-
count: 1,
|
|
49
|
-
firstSeen: occurrence.timestamp,
|
|
50
|
-
lastSeen: occurrence.timestamp,
|
|
51
|
-
affectedTraces: [occurrence.traceId],
|
|
52
|
-
affectedSpans: [occurrence.spanName],
|
|
53
|
-
service: occurrence.service,
|
|
54
|
-
attributes: occurrence.attributes
|
|
55
|
-
};
|
|
56
|
-
if (this.errorGroups.size >= this.options.maxGroups) this.evictOldestGroup();
|
|
57
|
-
this.errorGroups.set(fingerprint, newGroup);
|
|
58
|
-
return newGroup;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Extract errors from a trace and add them to the aggregator
|
|
62
|
-
*/
|
|
63
|
-
addErrorsFromTrace(trace) {
|
|
64
|
-
const addedGroups = [];
|
|
65
|
-
for (const span of trace.spans) if (span.status.code === "ERROR") {
|
|
66
|
-
const occurrence = this.extractErrorFromSpan(span, trace);
|
|
67
|
-
if (occurrence) {
|
|
68
|
-
const group = this.addError(occurrence);
|
|
69
|
-
addedGroups.push(group);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return addedGroups;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Extract error occurrence from a span
|
|
76
|
-
*/
|
|
77
|
-
extractErrorFromSpan(span, trace) {
|
|
78
|
-
const exceptionEvent = span.events?.find((e) => e.name === "exception");
|
|
79
|
-
const errorType = stringAttr(span.attributes, "exception.type", "error.type") ?? stringAttr(exceptionEvent?.attributes, "exception.type") ?? "Error";
|
|
80
|
-
const errorMessage = span.status.message || stringAttr(span.attributes, "exception.message", "error.message") || "Unknown error";
|
|
81
|
-
const stackTrace = stringAttr(span.attributes, "exception.stacktrace", "exception.stack", "error.stack") ?? this.extractStackFromEvents(span);
|
|
82
|
-
return {
|
|
83
|
-
traceId: trace.traceId,
|
|
84
|
-
spanId: span.spanId,
|
|
85
|
-
spanName: span.name,
|
|
86
|
-
service: trace.service,
|
|
87
|
-
timestamp: span.endTime,
|
|
88
|
-
error: {
|
|
89
|
-
type: errorType,
|
|
90
|
-
message: errorMessage,
|
|
91
|
-
stackTrace,
|
|
92
|
-
fingerprint: stringAttr(span.attributes, "exception.fingerprint") ?? stringAttr(exceptionEvent?.attributes, "exception.fingerprint")
|
|
93
|
-
},
|
|
94
|
-
attributes: this.extractRelevantAttributes(span.attributes)
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Extract stack trace from span events (exception events)
|
|
99
|
-
*/
|
|
100
|
-
extractStackFromEvents(span) {
|
|
101
|
-
if (!span.events) return void 0;
|
|
102
|
-
const exceptionEvent = span.events.find((e) => e.name === "exception");
|
|
103
|
-
if (exceptionEvent?.attributes) return stringAttr(exceptionEvent.attributes, "exception.stacktrace", "exception.stack");
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Extract relevant attributes for error context
|
|
107
|
-
*/
|
|
108
|
-
extractRelevantAttributes(attributes) {
|
|
109
|
-
const relevant = {};
|
|
110
|
-
for (const key of [
|
|
111
|
-
"http.method",
|
|
112
|
-
"http.url",
|
|
113
|
-
"http.route",
|
|
114
|
-
"http.status_code",
|
|
115
|
-
"db.system",
|
|
116
|
-
"db.operation",
|
|
117
|
-
"rpc.method",
|
|
118
|
-
"rpc.service",
|
|
119
|
-
"code.function",
|
|
120
|
-
"code.filepath",
|
|
121
|
-
"user.id",
|
|
122
|
-
"operation.name"
|
|
123
|
-
]) if (key in attributes) relevant[key] = attributes[key];
|
|
124
|
-
return relevant;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Generate a fingerprint for error grouping
|
|
128
|
-
*
|
|
129
|
-
* Uses error type + first N stack frames (normalized)
|
|
130
|
-
*/
|
|
131
|
-
generateFingerprint(occurrence) {
|
|
132
|
-
if (occurrence.error.fingerprint) return occurrence.error.fingerprint;
|
|
133
|
-
const parts = [occurrence.error.type];
|
|
134
|
-
if (occurrence.error.stackTrace) {
|
|
135
|
-
const frames = this.extractStackFrames(occurrence.error.stackTrace, this.options.stackFramesForFingerprint);
|
|
136
|
-
parts.push(...frames);
|
|
137
|
-
} else parts.push(this.normalizeMessage(occurrence.error.message));
|
|
138
|
-
return this.simpleHash(parts.join("|"));
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Extract and normalize stack frames from a stack trace
|
|
142
|
-
*/
|
|
143
|
-
extractStackFrames(stackTrace, count) {
|
|
144
|
-
const lines = stackTrace.split("\n");
|
|
145
|
-
const frames = [];
|
|
146
|
-
for (const line of lines) {
|
|
147
|
-
if (frames.length >= count) break;
|
|
148
|
-
const trimmed = line.trim();
|
|
149
|
-
const nodeMatch = trimmed.match(/^at\s+(.+?)\s+\((.+?):(\d+):\d+\)$/);
|
|
150
|
-
if (nodeMatch) {
|
|
151
|
-
frames.push(`${nodeMatch[1]}@${this.normalizeFilePath(nodeMatch[2])}`);
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const anonMatch = trimmed.match(/^at\s+(.+?):(\d+):\d+$/);
|
|
155
|
-
if (anonMatch) {
|
|
156
|
-
frames.push(`anonymous@${this.normalizeFilePath(anonMatch[1])}`);
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
const browserMatch = trimmed.match(/^(.+?)@(.+?):(\d+):\d+$/);
|
|
160
|
-
if (browserMatch) {
|
|
161
|
-
frames.push(`${browserMatch[1]}@${this.normalizeFilePath(browserMatch[2])}`);
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return frames;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Normalize file path by removing absolute path prefixes and node_modules paths
|
|
169
|
-
*/
|
|
170
|
-
normalizeFilePath(filePath) {
|
|
171
|
-
const nodeModulesMatch = filePath.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/);
|
|
172
|
-
if (nodeModulesMatch) return `[npm]/${nodeModulesMatch[1]}`;
|
|
173
|
-
return filePath.replace(/^.*?\/src\//, "src/").replace(/^.*?\/dist\//, "dist/").replace(/^.*?\/lib\//, "lib/").replace(/^file:\/\//, "");
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Normalize error message by removing dynamic parts
|
|
177
|
-
*/
|
|
178
|
-
normalizeMessage(message) {
|
|
179
|
-
return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\d+/g, "[N]").replaceAll(/"[^"]*"/g, "\"[STR]\"").replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200);
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Normalize stack trace for display
|
|
183
|
-
*/
|
|
184
|
-
normalizeStackTrace(stackTrace) {
|
|
185
|
-
if (!stackTrace) return void 0;
|
|
186
|
-
return stackTrace.split("\n").slice(0, 10).join("\n");
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Simple hash function for fingerprinting
|
|
190
|
-
*/
|
|
191
|
-
simpleHash(str) {
|
|
192
|
-
let hash = 0;
|
|
193
|
-
for (let i = 0; i < str.length; i++) {
|
|
194
|
-
const char = str.charCodeAt(i);
|
|
195
|
-
hash = (hash << 5) - hash + char;
|
|
196
|
-
hash = hash & hash;
|
|
197
|
-
}
|
|
198
|
-
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Evict the oldest error group
|
|
202
|
-
*/
|
|
203
|
-
evictOldestGroup() {
|
|
204
|
-
let oldest = null;
|
|
205
|
-
for (const [fingerprint, group] of this.errorGroups) if (!oldest || group.lastSeen < oldest.lastSeen) oldest = {
|
|
206
|
-
fingerprint,
|
|
207
|
-
lastSeen: group.lastSeen
|
|
208
|
-
};
|
|
209
|
-
if (oldest) this.errorGroups.delete(oldest.fingerprint);
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Get all error groups, sorted by most recent
|
|
213
|
-
*/
|
|
214
|
-
getErrorGroups() {
|
|
215
|
-
return [...this.errorGroups.values()].sort((a, b) => b.lastSeen - a.lastSeen);
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Get error groups sorted by count (most frequent first)
|
|
219
|
-
*/
|
|
220
|
-
getErrorGroupsByFrequency() {
|
|
221
|
-
return [...this.errorGroups.values()].sort((a, b) => b.count - a.count);
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Get a specific error group by fingerprint
|
|
225
|
-
*/
|
|
226
|
-
getErrorGroup(fingerprint) {
|
|
227
|
-
return this.errorGroups.get(fingerprint);
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Get error groups for a specific service
|
|
231
|
-
*/
|
|
232
|
-
getErrorGroupsByService(service) {
|
|
233
|
-
return this.getErrorGroups().filter((g) => g.service === service);
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Get total error count across all groups
|
|
237
|
-
*/
|
|
238
|
-
getTotalErrorCount() {
|
|
239
|
-
let total = 0;
|
|
240
|
-
for (const group of this.errorGroups.values()) total += group.count;
|
|
241
|
-
return total;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Get error statistics
|
|
245
|
-
*/
|
|
246
|
-
getStats() {
|
|
247
|
-
const oneHourAgo = Date.now() - 3600 * 1e3;
|
|
248
|
-
let recentErrors = 0;
|
|
249
|
-
const typeCount = /* @__PURE__ */ new Map();
|
|
250
|
-
for (const group of this.errorGroups.values()) {
|
|
251
|
-
if (group.lastSeen > oneHourAgo) recentErrors += group.count;
|
|
252
|
-
typeCount.set(group.type, (typeCount.get(group.type) || 0) + group.count);
|
|
253
|
-
}
|
|
254
|
-
const topErrorTypes = [...typeCount.entries()].map(([type, count]) => ({
|
|
255
|
-
type,
|
|
256
|
-
count
|
|
257
|
-
})).sort((a, b) => b.count - a.count).slice(0, 5);
|
|
258
|
-
return {
|
|
259
|
-
totalGroups: this.errorGroups.size,
|
|
260
|
-
totalErrors: this.getTotalErrorCount(),
|
|
261
|
-
recentErrors,
|
|
262
|
-
topErrorTypes
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Clear all error groups
|
|
267
|
-
*/
|
|
268
|
-
clear() {
|
|
269
|
-
this.errorGroups.clear();
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Clear old error groups (not seen in given time window)
|
|
273
|
-
*/
|
|
274
|
-
clearOlderThan(maxAgeMs) {
|
|
275
|
-
const cutoff = Date.now() - maxAgeMs;
|
|
276
|
-
let cleared = 0;
|
|
277
|
-
for (const [fingerprint, group] of this.errorGroups) if (group.lastSeen < cutoff) {
|
|
278
|
-
this.errorGroups.delete(fingerprint);
|
|
279
|
-
cleared++;
|
|
280
|
-
}
|
|
281
|
-
return cleared;
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
//#endregion
|
|
286
|
-
//#region src/server/telemetry-limits.ts
|
|
287
|
-
const defaultLimit = 100;
|
|
288
|
-
function parseLimit(value) {
|
|
289
|
-
if (!value) return void 0;
|
|
290
|
-
const parsed = Number.parseInt(value, 10);
|
|
291
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
292
|
-
}
|
|
293
|
-
function resolveTelemetryLimits(args = {}) {
|
|
294
|
-
const env = args.env ?? process.env;
|
|
295
|
-
const fallback = args.maxHistory ?? defaultLimit;
|
|
296
|
-
return {
|
|
297
|
-
maxTraceCount: args.maxTraceCount ?? parseLimit(env.AUTOTEL_MAX_TRACE_COUNT) ?? fallback,
|
|
298
|
-
maxLogCount: args.maxLogCount ?? parseLimit(env.AUTOTEL_MAX_LOG_COUNT) ?? fallback,
|
|
299
|
-
maxMetricCount: args.maxMetricCount ?? parseLimit(env.AUTOTEL_MAX_METRIC_COUNT) ?? fallback
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
function appendWithLimit(items, item, limit) {
|
|
303
|
-
if (limit <= 0) return [];
|
|
304
|
-
const next = [...items, item];
|
|
305
|
-
return next.length > limit ? next.slice(next.length - limit) : next;
|
|
306
|
-
}
|
|
307
|
-
function appendManyWithLimit(items, incoming, limit) {
|
|
308
|
-
if (limit <= 0 || incoming.length === 0) return limit <= 0 ? [] : items;
|
|
309
|
-
const next = [...items, ...incoming];
|
|
310
|
-
return next.length > limit ? next.slice(next.length - limit) : next;
|
|
311
|
-
}
|
|
312
|
-
function applyTelemetryLimits(data, limits) {
|
|
313
|
-
return {
|
|
314
|
-
...data,
|
|
315
|
-
traces: data.traces.slice(-limits.maxTraceCount),
|
|
316
|
-
logs: data.logs.slice(-limits.maxLogCount),
|
|
317
|
-
metrics: data.metrics.slice(-limits.maxMetricCount)
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
//#endregion
|
|
322
|
-
//#region src/server/origin-guard.ts
|
|
323
|
-
const LOOPBACK_IPV6 = /* @__PURE__ */ new Set(["::1", "0:0:0:0:0:0:0:1"]);
|
|
324
|
-
/** True for `localhost`, any `127.x.x.x`, and IPv6 loopback. Case-insensitive. */
|
|
325
|
-
function isLoopbackHostname(hostname) {
|
|
326
|
-
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
327
|
-
return h === "localhost" || /^127\./.test(h) || LOOPBACK_IPV6.has(h);
|
|
328
|
-
}
|
|
329
|
-
/** Hostname from a `Host` header (`host`, `host:port`, `[::1]:port`). */
|
|
330
|
-
function hostnameFromHostHeader(host) {
|
|
331
|
-
const h = host.trim();
|
|
332
|
-
if (h.startsWith("[")) {
|
|
333
|
-
const end = h.indexOf("]");
|
|
334
|
-
return end > 0 ? h.slice(1, end) : h;
|
|
335
|
-
}
|
|
336
|
-
const colon = h.indexOf(":");
|
|
337
|
-
return colon === -1 ? h : h.slice(0, colon);
|
|
338
|
-
}
|
|
339
|
-
/** True when the `Host` header names a loopback host. */
|
|
340
|
-
function hostHeaderIsLoopback(host) {
|
|
341
|
-
return isLoopbackHostname(hostnameFromHostHeader(host));
|
|
342
|
-
}
|
|
343
|
-
/** True when an `Origin` header names a loopback origin. A malformed or opaque
|
|
344
|
-
* origin (e.g. the literal `null` from a sandboxed iframe) is treated as
|
|
345
|
-
* non-loopback. */
|
|
346
|
-
function originIsLoopback(origin) {
|
|
347
|
-
try {
|
|
348
|
-
return isLoopbackHostname(new URL(origin).hostname);
|
|
349
|
-
} catch {
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Decide whether a request to a sensitive (read/mutate) endpoint is allowed.
|
|
355
|
-
* - A present, non-loopback `Origin` is always rejected (cross-origin read).
|
|
356
|
-
* - When `loopbackOnly`, a present, non-loopback `Host` is rejected (DNS
|
|
357
|
-
* rebinding). Skipped when the receiver is bound to a non-loopback host.
|
|
358
|
-
*/
|
|
359
|
-
function allowSensitiveRequest(headers, loopbackOnly) {
|
|
360
|
-
const { origin, host } = headers;
|
|
361
|
-
if (origin && origin.length > 0 && !originIsLoopback(origin)) return false;
|
|
362
|
-
if (loopbackOnly && host && host.length > 0 && !hostHeaderIsLoopback(host)) return false;
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
//#endregion
|
|
367
|
-
//#region src/server/server.ts
|
|
368
|
-
var DevtoolsServer = class {
|
|
369
|
-
wss;
|
|
370
|
-
clients = /* @__PURE__ */ new Set();
|
|
371
|
-
httpServer;
|
|
372
|
-
traces = [];
|
|
373
|
-
logs = [];
|
|
374
|
-
metrics = [];
|
|
375
|
-
agentSessions = /* @__PURE__ */ new Map();
|
|
376
|
-
errorAggregator = new ErrorAggregator();
|
|
377
|
-
limits;
|
|
378
|
-
verbose;
|
|
379
|
-
_port;
|
|
380
|
-
onData;
|
|
381
|
-
constructor(options = {}) {
|
|
382
|
-
this.limits = resolveTelemetryLimits(options);
|
|
383
|
-
this.verbose = options.verbose ?? false;
|
|
384
|
-
this._port = options.port ?? 4318;
|
|
385
|
-
this.onData = options.onData;
|
|
386
|
-
this.httpServer = options.server ?? createServer();
|
|
387
|
-
const loopbackOnly = options.host == null || hostHeaderIsLoopback(options.host);
|
|
388
|
-
this.wss = new WebSocketServer({
|
|
389
|
-
server: this.httpServer,
|
|
390
|
-
path: options.path ?? "/ws",
|
|
391
|
-
verifyClient: ({ origin, req }) => allowSensitiveRequest({
|
|
392
|
-
origin,
|
|
393
|
-
host: req.headers.host
|
|
394
|
-
}, loopbackOnly)
|
|
395
|
-
});
|
|
396
|
-
this.wss.on("error", (err) => {
|
|
397
|
-
if (this.httpServer.listening) throw err;
|
|
398
|
-
});
|
|
399
|
-
this.wss.on("connection", (ws) => {
|
|
400
|
-
this.clients.add(ws);
|
|
401
|
-
this.log(`Client connected (${this.clients.size} total)`);
|
|
402
|
-
const data = this.getCurrentData();
|
|
403
|
-
if (data.traces.length > 0 || data.logs.length > 0 || data.errors.length > 0 || (data.agents?.length ?? 0) > 0) ws.send(JSON.stringify(data));
|
|
404
|
-
ws.on("close", () => {
|
|
405
|
-
this.clients.delete(ws);
|
|
406
|
-
this.log(`Client disconnected (${this.clients.size} total)`);
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
if (!options.server) this.httpServer.listen(this._port, () => {
|
|
410
|
-
const addr = this.httpServer.address();
|
|
411
|
-
if (addr && typeof addr === "object") this._port = addr.port;
|
|
412
|
-
this.log(`WebSocket server listening on port ${this._port}`);
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
get port() {
|
|
416
|
-
const addr = this.httpServer.address();
|
|
417
|
-
if (addr && typeof addr === "object") return addr.port;
|
|
418
|
-
return this._port;
|
|
419
|
-
}
|
|
420
|
-
get clientCount() {
|
|
421
|
-
return this.clients.size;
|
|
422
|
-
}
|
|
423
|
-
addTrace(trace) {
|
|
424
|
-
const existing = this.traces.find((t) => t.traceId === trace.traceId);
|
|
425
|
-
const merged = existing ?? trace;
|
|
426
|
-
if (existing) {
|
|
427
|
-
const existingSpanIds = new Set(existing.spans.map((s) => s.spanId));
|
|
428
|
-
for (const span of trace.spans) if (!existingSpanIds.has(span.spanId)) existing.spans.push(span);
|
|
429
|
-
existing.startTime = Math.min(existing.startTime, trace.startTime);
|
|
430
|
-
existing.endTime = Math.max(existing.endTime, trace.endTime);
|
|
431
|
-
existing.duration = existing.endTime - existing.startTime;
|
|
432
|
-
if (trace.status === "ERROR") existing.status = "ERROR";
|
|
433
|
-
existing.spans.sort((a, b) => a.startTime - b.startTime);
|
|
434
|
-
const { rootSpan, partial } = pickRoot(existing.spans);
|
|
435
|
-
existing.rootSpan = rootSpan;
|
|
436
|
-
if (partial) existing.partial = true;
|
|
437
|
-
else {
|
|
438
|
-
delete existing.partial;
|
|
439
|
-
const rootService = rootSpan.attributes?.["service.name"];
|
|
440
|
-
if (typeof rootService === "string" && rootService.length > 0) existing.service = rootService;
|
|
441
|
-
}
|
|
442
|
-
} else this.traces = appendWithLimit(this.traces, trace, this.limits.maxTraceCount);
|
|
443
|
-
this.errorAggregator.addErrorsFromTrace(trace);
|
|
444
|
-
this.broadcast({
|
|
445
|
-
traces: [merged],
|
|
446
|
-
metrics: [],
|
|
447
|
-
logs: [],
|
|
448
|
-
errors: this.errorAggregator.getErrorGroups()
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
addTraces(traces) {
|
|
452
|
-
for (const trace of traces) this.addTrace(trace);
|
|
453
|
-
}
|
|
454
|
-
addLog(log) {
|
|
455
|
-
this.logs = appendWithLimit(this.logs, log, this.limits.maxLogCount);
|
|
456
|
-
this.broadcast({
|
|
457
|
-
traces: [],
|
|
458
|
-
metrics: [],
|
|
459
|
-
logs: [log],
|
|
460
|
-
errors: this.errorAggregator.getErrorGroups()
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
addLogs(logs) {
|
|
464
|
-
this.logs = appendManyWithLimit(this.logs, logs, this.limits.maxLogCount);
|
|
465
|
-
this.broadcast({
|
|
466
|
-
traces: [],
|
|
467
|
-
metrics: [],
|
|
468
|
-
logs,
|
|
469
|
-
errors: this.errorAggregator.getErrorGroups()
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
addMetric(metric) {
|
|
473
|
-
this.metrics = appendWithLimit(this.metrics, metric, this.limits.maxMetricCount);
|
|
474
|
-
this.broadcast({
|
|
475
|
-
traces: [],
|
|
476
|
-
metrics: [metric],
|
|
477
|
-
logs: [],
|
|
478
|
-
errors: this.errorAggregator.getErrorGroups()
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
/** Fold decoded agent log events into sessions and broadcast the full set. */
|
|
482
|
-
ingestAgentEvents(records) {
|
|
483
|
-
if (records.length === 0) return;
|
|
484
|
-
ingestAgentEvents(this.agentSessions, records);
|
|
485
|
-
this.broadcastAgents();
|
|
486
|
-
}
|
|
487
|
-
/** Fold decoded agent metric records into sessions and broadcast the full set. */
|
|
488
|
-
ingestAgentMetrics(records) {
|
|
489
|
-
if (records.length === 0) return;
|
|
490
|
-
ingestAgentMetrics(this.agentSessions, records);
|
|
491
|
-
this.broadcastAgents();
|
|
492
|
-
}
|
|
493
|
-
broadcastAgents() {
|
|
494
|
-
this.broadcast({
|
|
495
|
-
traces: [],
|
|
496
|
-
metrics: [],
|
|
497
|
-
logs: [],
|
|
498
|
-
errors: this.errorAggregator.getErrorGroups(),
|
|
499
|
-
agents: [...this.agentSessions.values()]
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
getCurrentData() {
|
|
503
|
-
return {
|
|
504
|
-
traces: this.traces,
|
|
505
|
-
metrics: this.metrics,
|
|
506
|
-
logs: this.logs,
|
|
507
|
-
errors: this.errorAggregator.getErrorGroups(),
|
|
508
|
-
agents: [...this.agentSessions.values()]
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
clearData() {
|
|
512
|
-
this.traces = [];
|
|
513
|
-
this.logs = [];
|
|
514
|
-
this.metrics = [];
|
|
515
|
-
this.agentSessions.clear();
|
|
516
|
-
this.errorAggregator.clear();
|
|
517
|
-
}
|
|
518
|
-
broadcast(data) {
|
|
519
|
-
const msg = JSON.stringify(data);
|
|
520
|
-
for (const client of this.clients) if (client.readyState === WebSocket.OPEN) client.send(msg);
|
|
521
|
-
if (this.onData) try {
|
|
522
|
-
this.onData(data);
|
|
523
|
-
} catch {}
|
|
524
|
-
}
|
|
525
|
-
log(message) {
|
|
526
|
-
if (this.verbose) console.log(`[autotel-devtools] ${message}`);
|
|
527
|
-
}
|
|
528
|
-
async close() {
|
|
529
|
-
for (const client of this.clients) client.close();
|
|
530
|
-
this.clients.clear();
|
|
531
|
-
this.wss.close();
|
|
532
|
-
await new Promise((resolve) => this.httpServer.close(() => resolve()));
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
//#endregion
|
|
537
|
-
//#region src/server/otlp-types.ts
|
|
538
|
-
/**
|
|
539
|
-
* An exporter's payload, read as the envelope it claims to be.
|
|
540
|
-
*
|
|
541
|
-
* SAFETY: this is the one place the receiver trusts the wire. Every field of
|
|
542
|
-
* every envelope above is optional, so a payload that is not what it claims
|
|
543
|
-
* reads back as empty arrays and undefined fields rather than throwing - which
|
|
544
|
-
* is what the callers below rely on when they find no spans to add.
|
|
545
|
-
*/
|
|
546
|
-
function otlpEnvelope(payload) {
|
|
547
|
-
if (typeof payload !== "object" || payload === null) return void 0;
|
|
548
|
-
return payload;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
//#endregion
|
|
552
|
-
//#region src/server/otlp.ts
|
|
553
|
-
function resolveOtlpValue(v) {
|
|
554
|
-
if (!v) return void 0;
|
|
555
|
-
if (v.stringValue !== void 0) return v.stringValue;
|
|
556
|
-
if (v.boolValue !== void 0) return v.boolValue;
|
|
557
|
-
if (v.intValue !== void 0) return Number(v.intValue);
|
|
558
|
-
if (v.doubleValue !== void 0) return v.doubleValue;
|
|
559
|
-
if (v.bytesValue !== void 0) return v.bytesValue;
|
|
560
|
-
if (v.arrayValue?.values) return v.arrayValue.values.map(resolveOtlpValue);
|
|
561
|
-
if (v.kvlistValue?.values) return flattenAttributes(v.kvlistValue.values);
|
|
562
|
-
}
|
|
563
|
-
function flattenAttributes(attrs) {
|
|
564
|
-
const out = {};
|
|
565
|
-
if (!attrs) return out;
|
|
566
|
-
for (const { key, value } of attrs) out[key] = resolveOtlpValue(value);
|
|
567
|
-
return out;
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* A log record's body: the text it carried, or the structure it carried when
|
|
571
|
-
* the sender used an OTLP kvlist or array rather than a string.
|
|
572
|
-
*/
|
|
573
|
-
function logBody(body) {
|
|
574
|
-
const text = asString(body);
|
|
575
|
-
if (text !== void 0) return text;
|
|
576
|
-
if (body === void 0 || body === null) return "";
|
|
577
|
-
const structured = asObject(body);
|
|
578
|
-
if (!structured) return String(body);
|
|
579
|
-
return structured;
|
|
580
|
-
}
|
|
581
|
-
/**
|
|
582
|
-
* Attributes handed to the agent layer, whose `Attributes` is OTel's own -
|
|
583
|
-
* scalars and arrays of scalars, nothing nested.
|
|
584
|
-
*
|
|
585
|
-
* SAFETY: a coding agent's metrics and events carry scalar attributes only,
|
|
586
|
-
* so the two shapes agree in practice. A sender that nests one anyway is
|
|
587
|
-
* rendered by the Agents tab as whatever it is rather than being dropped.
|
|
588
|
-
*/
|
|
589
|
-
function agentAttributes(attributes) {
|
|
590
|
-
return attributes;
|
|
591
|
-
}
|
|
592
|
-
function nanoToMs(nano) {
|
|
593
|
-
if (!nano) return 0;
|
|
594
|
-
const ns = BigInt(nano);
|
|
595
|
-
const ms = ns / 1000000n;
|
|
596
|
-
const remNs = ns % 1000000n;
|
|
597
|
-
return Number(ms) + Number(remNs) / 1e6;
|
|
598
|
-
}
|
|
599
|
-
const SPAN_KIND_MAP = /* @__PURE__ */ new Map([
|
|
600
|
-
[0, "INTERNAL"],
|
|
601
|
-
[1, "INTERNAL"],
|
|
602
|
-
[2, "SERVER"],
|
|
603
|
-
[3, "CLIENT"],
|
|
604
|
-
[4, "PRODUCER"],
|
|
605
|
-
[5, "CONSUMER"],
|
|
606
|
-
["SPAN_KIND_INTERNAL", "INTERNAL"],
|
|
607
|
-
["SPAN_KIND_SERVER", "SERVER"],
|
|
608
|
-
["SPAN_KIND_CLIENT", "CLIENT"],
|
|
609
|
-
["SPAN_KIND_PRODUCER", "PRODUCER"],
|
|
610
|
-
["SPAN_KIND_CONSUMER", "CONSUMER"]
|
|
611
|
-
]);
|
|
612
|
-
function normalizeHexId(id) {
|
|
613
|
-
if (!id) return "";
|
|
614
|
-
if (/^[A-Za-z0-9+/=]+$/.test(id) && !/^[0-9a-f]+$/i.test(id) && (id.length === 12 || id.length === 24 || id.length === 28 || id.length === 44 || id.length === 48)) try {
|
|
615
|
-
return Buffer.from(id, "base64").toString("hex");
|
|
616
|
-
} catch {}
|
|
617
|
-
return id;
|
|
618
|
-
}
|
|
619
|
-
function parseOtlpTraces(payload) {
|
|
620
|
-
const resourceSpans = otlpEnvelope(payload)?.resourceSpans;
|
|
621
|
-
if (!resourceSpans || resourceSpans.length === 0) return [];
|
|
622
|
-
const traceMap = /* @__PURE__ */ new Map();
|
|
623
|
-
for (const rs of resourceSpans) {
|
|
624
|
-
const resourceAttrs = flattenAttributes(rs.resource?.attributes);
|
|
625
|
-
const service = String(resourceAttrs["service.name"] || "unknown");
|
|
626
|
-
for (const ss of rs.scopeSpans ?? []) {
|
|
627
|
-
const scope = ss.scope?.name ? {
|
|
628
|
-
name: ss.scope.name,
|
|
629
|
-
version: ss.scope.version || void 0
|
|
630
|
-
} : void 0;
|
|
631
|
-
for (const span of ss.spans || []) {
|
|
632
|
-
const traceId = normalizeHexId(span.traceId);
|
|
633
|
-
if (!traceId) continue;
|
|
634
|
-
const startMs = nanoToMs(span.startTimeUnixNano);
|
|
635
|
-
const endMs = nanoToMs(span.endTimeUnixNano);
|
|
636
|
-
const statusCode = span.status?.code;
|
|
637
|
-
let status = "UNSET";
|
|
638
|
-
if (statusCode === 1 || statusCode === "STATUS_CODE_OK") status = "OK";
|
|
639
|
-
if (statusCode === 2 || statusCode === "STATUS_CODE_ERROR") status = "ERROR";
|
|
640
|
-
const spanData = {
|
|
641
|
-
traceId,
|
|
642
|
-
spanId: normalizeHexId(span.spanId),
|
|
643
|
-
parentSpanId: normalizeHexId(span.parentSpanId) || void 0,
|
|
644
|
-
name: span.name || "unknown",
|
|
645
|
-
kind: SPAN_KIND_MAP.get(span.kind ?? 0) ?? "INTERNAL",
|
|
646
|
-
startTime: startMs,
|
|
647
|
-
endTime: endMs,
|
|
648
|
-
duration: endMs - startMs,
|
|
649
|
-
attributes: {
|
|
650
|
-
...resourceAttrs,
|
|
651
|
-
...flattenAttributes(span.attributes)
|
|
652
|
-
},
|
|
653
|
-
status: {
|
|
654
|
-
code: status,
|
|
655
|
-
message: span.status?.message
|
|
656
|
-
},
|
|
657
|
-
events: (span.events ?? []).map((e) => ({
|
|
658
|
-
name: e.name || "",
|
|
659
|
-
timestamp: nanoToMs(e.timeUnixNano),
|
|
660
|
-
attributes: flattenAttributes(e.attributes)
|
|
661
|
-
})),
|
|
662
|
-
links: (span.links ?? []).map((l) => ({
|
|
663
|
-
traceId: normalizeHexId(l.traceId),
|
|
664
|
-
spanId: normalizeHexId(l.spanId),
|
|
665
|
-
attributes: flattenAttributes(l.attributes)
|
|
666
|
-
})),
|
|
667
|
-
scope
|
|
668
|
-
};
|
|
669
|
-
const existing = traceMap.get(traceId);
|
|
670
|
-
if (existing) existing.spans.push(spanData);
|
|
671
|
-
else traceMap.set(traceId, {
|
|
672
|
-
spans: [spanData],
|
|
673
|
-
service
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
const traces = [];
|
|
679
|
-
for (const [traceId, { spans, service }] of traceMap) {
|
|
680
|
-
const sorted = spans.sort((a, b) => a.startTime - b.startTime);
|
|
681
|
-
const { rootSpan, partial } = pickRoot(sorted);
|
|
682
|
-
const startTime = Math.min(...sorted.map((s) => s.startTime));
|
|
683
|
-
const endTime = Math.max(...sorted.map((s) => s.endTime));
|
|
684
|
-
const hasError = sorted.some((s) => s.status.code === "ERROR");
|
|
685
|
-
const trace = {
|
|
686
|
-
traceId,
|
|
687
|
-
correlationId: traceId.slice(0, 16),
|
|
688
|
-
rootSpan,
|
|
689
|
-
spans: sorted,
|
|
690
|
-
startTime,
|
|
691
|
-
endTime,
|
|
692
|
-
duration: endTime - startTime,
|
|
693
|
-
status: hasError ? "ERROR" : "OK",
|
|
694
|
-
service
|
|
695
|
-
};
|
|
696
|
-
if (partial) trace.partial = true;
|
|
697
|
-
traces.push(trace);
|
|
698
|
-
}
|
|
699
|
-
return traces;
|
|
700
|
-
}
|
|
701
|
-
function parseOtlpLogs(payload) {
|
|
702
|
-
const resourceLogs = otlpEnvelope(payload)?.resourceLogs;
|
|
703
|
-
if (!resourceLogs) return [];
|
|
704
|
-
const logs = [];
|
|
705
|
-
for (const rl of resourceLogs) {
|
|
706
|
-
const resourceAttrs = flattenAttributes(rl.resource?.attributes);
|
|
707
|
-
for (const sl of rl.scopeLogs ?? []) for (const rec of sl.logRecords ?? []) {
|
|
708
|
-
const timestamp = nanoToMs(rec.timeUnixNano || rec.observedTimeUnixNano);
|
|
709
|
-
const traceId = normalizeHexId(rec.traceId) || void 0;
|
|
710
|
-
const spanId = normalizeHexId(rec.spanId) || void 0;
|
|
711
|
-
const body = rec.body ? resolveOtlpValue(rec.body) : "";
|
|
712
|
-
logs.push({
|
|
713
|
-
id: `${traceId || "no-trace"}:${spanId || "no-span"}:${timestamp}:${rec.severityNumber || 0}`,
|
|
714
|
-
traceId,
|
|
715
|
-
spanId,
|
|
716
|
-
resourceName: getResourceName(resourceAttrs),
|
|
717
|
-
severityText: rec.severityText,
|
|
718
|
-
severityNumber: rec.severityNumber,
|
|
719
|
-
body: logBody(body),
|
|
720
|
-
timestamp,
|
|
721
|
-
attributes: flattenAttributes(rec.attributes),
|
|
722
|
-
resource: resourceAttrs
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return logs;
|
|
727
|
-
}
|
|
728
|
-
function countOtlpMetrics(payload) {
|
|
729
|
-
const resourceMetrics = otlpEnvelope(payload)?.resourceMetrics;
|
|
730
|
-
if (!resourceMetrics) return 0;
|
|
731
|
-
let count = 0;
|
|
732
|
-
for (const rm of resourceMetrics) for (const sm of rm.scopeMetrics ?? []) count += (sm.metrics ?? []).length;
|
|
733
|
-
return count;
|
|
734
|
-
}
|
|
735
|
-
function extractDataPoints(metric) {
|
|
736
|
-
const points = [];
|
|
737
|
-
const numberPoints = metric.sum?.dataPoints ?? metric.gauge?.dataPoints;
|
|
738
|
-
if (Array.isArray(numberPoints)) for (const dp of numberPoints) {
|
|
739
|
-
const value = dp.asDouble !== void 0 ? Number(dp.asDouble) : dp.asInt !== void 0 ? Number(dp.asInt) : 0;
|
|
740
|
-
points.push({
|
|
741
|
-
value,
|
|
742
|
-
attributes: agentAttributes(flattenAttributes(dp.attributes)),
|
|
743
|
-
timestamp: nanoToMs(dp.timeUnixNano || dp.startTimeUnixNano)
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
const histPoints = metric.histogram?.dataPoints;
|
|
747
|
-
if (Array.isArray(histPoints)) for (const dp of histPoints) points.push({
|
|
748
|
-
value: dp.count !== void 0 ? Number(dp.count) : 0,
|
|
749
|
-
attributes: agentAttributes(flattenAttributes(dp.attributes)),
|
|
750
|
-
timestamp: nanoToMs(dp.timeUnixNano || dp.startTimeUnixNano)
|
|
751
|
-
});
|
|
752
|
-
return points;
|
|
753
|
-
}
|
|
754
|
-
/**
|
|
755
|
-
* Parse OTLP metrics into structured records with data points + attributes,
|
|
756
|
-
* for the agent layer (and richer metric views). Works for both OTLP/JSON and
|
|
757
|
-
* decoded OTLP/protobuf — they share the same camelCase shape.
|
|
758
|
-
*/
|
|
759
|
-
function readTemporality(metric) {
|
|
760
|
-
const raw = metric.sum?.aggregationTemporality ?? metric.histogram?.aggregationTemporality;
|
|
761
|
-
if (raw === 2 || raw === "AGGREGATION_TEMPORALITY_CUMULATIVE") return "cumulative";
|
|
762
|
-
if (raw === 1 || raw === "AGGREGATION_TEMPORALITY_DELTA") return "delta";
|
|
763
|
-
}
|
|
764
|
-
function parseOtlpMetrics(payload) {
|
|
765
|
-
const resourceMetrics = otlpEnvelope(payload)?.resourceMetrics;
|
|
766
|
-
if (!resourceMetrics) return [];
|
|
767
|
-
const records = [];
|
|
768
|
-
for (const rm of resourceMetrics) {
|
|
769
|
-
const resource = agentAttributes(flattenAttributes(rm.resource?.attributes));
|
|
770
|
-
for (const sm of rm.scopeMetrics ?? []) {
|
|
771
|
-
const scope = sm.scope?.name ? {
|
|
772
|
-
name: sm.scope.name,
|
|
773
|
-
version: sm.scope.version || void 0
|
|
774
|
-
} : void 0;
|
|
775
|
-
for (const metric of sm.metrics ?? []) records.push({
|
|
776
|
-
name: metric.name ?? "",
|
|
777
|
-
unit: metric.unit || void 0,
|
|
778
|
-
description: metric.description || void 0,
|
|
779
|
-
temporality: readTemporality(metric),
|
|
780
|
-
dataPoints: extractDataPoints(metric),
|
|
781
|
-
resource,
|
|
782
|
-
scope
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
return records;
|
|
787
|
-
}
|
|
788
|
-
/**
|
|
789
|
-
* Parse OTLP logs into `AgentRawEvent`s for the agent layer. Keeps the
|
|
790
|
-
* instrumentation scope and event name (Claude Code emits its events as logs,
|
|
791
|
-
* with the unprefixed name in the `event.name` attribute). Distinct from
|
|
792
|
-
* `parseOtlpLogs`, which feeds the generic Logs tab.
|
|
793
|
-
*/
|
|
794
|
-
function parseOtlpAgentEvents(payload) {
|
|
795
|
-
const resourceLogs = otlpEnvelope(payload)?.resourceLogs;
|
|
796
|
-
if (!resourceLogs) return [];
|
|
797
|
-
const events = [];
|
|
798
|
-
for (const rl of resourceLogs) {
|
|
799
|
-
const resource = agentAttributes(flattenAttributes(rl.resource?.attributes));
|
|
800
|
-
for (const sl of rl.scopeLogs ?? []) {
|
|
801
|
-
const scope = sl.scope?.name ? {
|
|
802
|
-
name: sl.scope.name,
|
|
803
|
-
version: sl.scope.version || void 0
|
|
804
|
-
} : void 0;
|
|
805
|
-
for (const rec of sl.logRecords ?? []) {
|
|
806
|
-
const attributes = agentAttributes(flattenAttributes(rec.attributes));
|
|
807
|
-
const eventName = rec.eventName || String(attributes["event.name"] ?? "");
|
|
808
|
-
events.push({
|
|
809
|
-
eventName,
|
|
810
|
-
timestamp: nanoToMs(rec.timeUnixNano || rec.observedTimeUnixNano),
|
|
811
|
-
body: rec.body ? resolveOtlpValue(rec.body) : void 0,
|
|
812
|
-
attributes,
|
|
813
|
-
resource,
|
|
814
|
-
scope
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
return events;
|
|
820
|
-
}
|
|
821
|
-
async function readJsonBody(req) {
|
|
822
|
-
return new Promise((resolve, reject) => {
|
|
823
|
-
const chunks = [];
|
|
824
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
825
|
-
req.on("end", () => {
|
|
826
|
-
try {
|
|
827
|
-
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
828
|
-
} catch {
|
|
829
|
-
reject(/* @__PURE__ */ new Error("Invalid JSON"));
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
req.on("error", reject);
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
async function readRawBody(req) {
|
|
836
|
-
return new Promise((resolve, reject) => {
|
|
837
|
-
const chunks = [];
|
|
838
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
839
|
-
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
840
|
-
req.on("error", reject);
|
|
841
|
-
});
|
|
842
|
-
}
|
|
843
|
-
/**
|
|
844
|
-
* True for OTLP/protobuf bodies. The OpenTelemetry Python/Java/Go SDKs default to
|
|
845
|
-
* `http/protobuf` over OTLP HTTP, sending `application/x-protobuf`; some clients use
|
|
846
|
-
* `application/protobuf`. Anything else (JSON, unset) is treated as OTLP/JSON.
|
|
847
|
-
*/
|
|
848
|
-
function isProtobufContentType(contentType) {
|
|
849
|
-
if (!contentType) return false;
|
|
850
|
-
const value = contentType.toLowerCase();
|
|
851
|
-
return value.includes("application/x-protobuf") || value.includes("application/protobuf");
|
|
852
|
-
}
|
|
853
|
-
function sendJson(res, status, data) {
|
|
854
|
-
const body = JSON.stringify(data);
|
|
855
|
-
res.writeHead(status, {
|
|
856
|
-
"Content-Type": "application/json",
|
|
857
|
-
"Content-Length": Buffer.byteLength(body)
|
|
858
|
-
});
|
|
859
|
-
res.end(body);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
//#endregion
|
|
863
|
-
//#region src/server/otlp-proto.ts
|
|
864
|
-
const COMMON_PROTO = `
|
|
865
|
-
syntax = "proto3";
|
|
866
|
-
package opentelemetry.proto.common.v1;
|
|
867
|
-
|
|
868
|
-
message AnyValue {
|
|
869
|
-
oneof value {
|
|
870
|
-
string string_value = 1;
|
|
871
|
-
bool bool_value = 2;
|
|
872
|
-
int64 int_value = 3;
|
|
873
|
-
double double_value = 4;
|
|
874
|
-
ArrayValue array_value = 5;
|
|
875
|
-
KeyValueList kvlist_value = 6;
|
|
876
|
-
bytes bytes_value = 7;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
message ArrayValue { repeated AnyValue values = 1; }
|
|
880
|
-
message KeyValueList { repeated KeyValue values = 1; }
|
|
881
|
-
message KeyValue {
|
|
882
|
-
string key = 1;
|
|
883
|
-
AnyValue value = 2;
|
|
884
|
-
}
|
|
885
|
-
message InstrumentationScope {
|
|
886
|
-
string name = 1;
|
|
887
|
-
string version = 2;
|
|
888
|
-
repeated KeyValue attributes = 3;
|
|
889
|
-
uint32 dropped_attributes_count = 4;
|
|
890
|
-
}
|
|
891
|
-
`;
|
|
892
|
-
const RESOURCE_PROTO = `
|
|
893
|
-
syntax = "proto3";
|
|
894
|
-
package opentelemetry.proto.resource.v1;
|
|
895
|
-
|
|
896
|
-
message Resource {
|
|
897
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 1;
|
|
898
|
-
uint32 dropped_attributes_count = 2;
|
|
899
|
-
}
|
|
900
|
-
`;
|
|
901
|
-
const TRACE_PROTO = `
|
|
902
|
-
syntax = "proto3";
|
|
903
|
-
package opentelemetry.proto.trace.v1;
|
|
904
|
-
|
|
905
|
-
message ResourceSpans {
|
|
906
|
-
opentelemetry.proto.resource.v1.Resource resource = 1;
|
|
907
|
-
repeated ScopeSpans scope_spans = 2;
|
|
908
|
-
string schema_url = 3;
|
|
909
|
-
}
|
|
910
|
-
message ScopeSpans {
|
|
911
|
-
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
|
912
|
-
repeated Span spans = 2;
|
|
913
|
-
string schema_url = 3;
|
|
914
|
-
}
|
|
915
|
-
message Span {
|
|
916
|
-
bytes trace_id = 1;
|
|
917
|
-
bytes span_id = 2;
|
|
918
|
-
string trace_state = 3;
|
|
919
|
-
bytes parent_span_id = 4;
|
|
920
|
-
fixed32 flags = 16;
|
|
921
|
-
string name = 5;
|
|
922
|
-
SpanKind kind = 6;
|
|
923
|
-
fixed64 start_time_unix_nano = 7;
|
|
924
|
-
fixed64 end_time_unix_nano = 8;
|
|
925
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 9;
|
|
926
|
-
uint32 dropped_attributes_count = 10;
|
|
927
|
-
repeated Event events = 11;
|
|
928
|
-
uint32 dropped_events_count = 12;
|
|
929
|
-
repeated Link links = 13;
|
|
930
|
-
uint32 dropped_links_count = 14;
|
|
931
|
-
Status status = 15;
|
|
932
|
-
|
|
933
|
-
enum SpanKind {
|
|
934
|
-
SPAN_KIND_UNSPECIFIED = 0;
|
|
935
|
-
SPAN_KIND_INTERNAL = 1;
|
|
936
|
-
SPAN_KIND_SERVER = 2;
|
|
937
|
-
SPAN_KIND_CLIENT = 3;
|
|
938
|
-
SPAN_KIND_PRODUCER = 4;
|
|
939
|
-
SPAN_KIND_CONSUMER = 5;
|
|
940
|
-
}
|
|
941
|
-
message Event {
|
|
942
|
-
fixed64 time_unix_nano = 1;
|
|
943
|
-
string name = 2;
|
|
944
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 3;
|
|
945
|
-
uint32 dropped_attributes_count = 4;
|
|
946
|
-
}
|
|
947
|
-
message Link {
|
|
948
|
-
bytes trace_id = 1;
|
|
949
|
-
bytes span_id = 2;
|
|
950
|
-
string trace_state = 3;
|
|
951
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 4;
|
|
952
|
-
uint32 dropped_attributes_count = 5;
|
|
953
|
-
fixed32 flags = 6;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
message Status {
|
|
957
|
-
reserved 1;
|
|
958
|
-
string message = 2;
|
|
959
|
-
StatusCode code = 3;
|
|
960
|
-
|
|
961
|
-
enum StatusCode {
|
|
962
|
-
STATUS_CODE_UNSET = 0;
|
|
963
|
-
STATUS_CODE_OK = 1;
|
|
964
|
-
STATUS_CODE_ERROR = 2;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
message ExportTraceServiceRequest {
|
|
968
|
-
repeated ResourceSpans resource_spans = 1;
|
|
969
|
-
}
|
|
970
|
-
`;
|
|
971
|
-
const LOGS_PROTO = `
|
|
972
|
-
syntax = "proto3";
|
|
973
|
-
package opentelemetry.proto.logs.v1;
|
|
974
|
-
|
|
975
|
-
enum SeverityNumber {
|
|
976
|
-
SEVERITY_NUMBER_UNSPECIFIED = 0;
|
|
977
|
-
SEVERITY_NUMBER_TRACE = 1;
|
|
978
|
-
SEVERITY_NUMBER_TRACE2 = 2;
|
|
979
|
-
SEVERITY_NUMBER_TRACE3 = 3;
|
|
980
|
-
SEVERITY_NUMBER_TRACE4 = 4;
|
|
981
|
-
SEVERITY_NUMBER_DEBUG = 5;
|
|
982
|
-
SEVERITY_NUMBER_DEBUG2 = 6;
|
|
983
|
-
SEVERITY_NUMBER_DEBUG3 = 7;
|
|
984
|
-
SEVERITY_NUMBER_DEBUG4 = 8;
|
|
985
|
-
SEVERITY_NUMBER_INFO = 9;
|
|
986
|
-
SEVERITY_NUMBER_INFO2 = 10;
|
|
987
|
-
SEVERITY_NUMBER_INFO3 = 11;
|
|
988
|
-
SEVERITY_NUMBER_INFO4 = 12;
|
|
989
|
-
SEVERITY_NUMBER_WARN = 13;
|
|
990
|
-
SEVERITY_NUMBER_WARN2 = 14;
|
|
991
|
-
SEVERITY_NUMBER_WARN3 = 15;
|
|
992
|
-
SEVERITY_NUMBER_WARN4 = 16;
|
|
993
|
-
SEVERITY_NUMBER_ERROR = 17;
|
|
994
|
-
SEVERITY_NUMBER_ERROR2 = 18;
|
|
995
|
-
SEVERITY_NUMBER_ERROR3 = 19;
|
|
996
|
-
SEVERITY_NUMBER_ERROR4 = 20;
|
|
997
|
-
SEVERITY_NUMBER_FATAL = 21;
|
|
998
|
-
SEVERITY_NUMBER_FATAL2 = 22;
|
|
999
|
-
SEVERITY_NUMBER_FATAL3 = 23;
|
|
1000
|
-
SEVERITY_NUMBER_FATAL4 = 24;
|
|
1001
|
-
}
|
|
1002
|
-
message ResourceLogs {
|
|
1003
|
-
opentelemetry.proto.resource.v1.Resource resource = 1;
|
|
1004
|
-
repeated ScopeLogs scope_logs = 2;
|
|
1005
|
-
string schema_url = 3;
|
|
1006
|
-
}
|
|
1007
|
-
message ScopeLogs {
|
|
1008
|
-
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
|
1009
|
-
repeated LogRecord log_records = 2;
|
|
1010
|
-
string schema_url = 3;
|
|
1011
|
-
}
|
|
1012
|
-
message LogRecord {
|
|
1013
|
-
reserved 4;
|
|
1014
|
-
fixed64 time_unix_nano = 1;
|
|
1015
|
-
fixed64 observed_time_unix_nano = 11;
|
|
1016
|
-
SeverityNumber severity_number = 2;
|
|
1017
|
-
string severity_text = 3;
|
|
1018
|
-
opentelemetry.proto.common.v1.AnyValue body = 5;
|
|
1019
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 6;
|
|
1020
|
-
uint32 dropped_attributes_count = 7;
|
|
1021
|
-
fixed32 flags = 8;
|
|
1022
|
-
bytes trace_id = 9;
|
|
1023
|
-
bytes span_id = 10;
|
|
1024
|
-
}
|
|
1025
|
-
message ExportLogsServiceRequest {
|
|
1026
|
-
repeated ResourceLogs resource_logs = 1;
|
|
1027
|
-
}
|
|
1028
|
-
`;
|
|
1029
|
-
const METRICS_PROTO = `
|
|
1030
|
-
syntax = "proto3";
|
|
1031
|
-
package opentelemetry.proto.metrics.v1;
|
|
1032
|
-
|
|
1033
|
-
enum AggregationTemporality {
|
|
1034
|
-
AGGREGATION_TEMPORALITY_UNSPECIFIED = 0;
|
|
1035
|
-
AGGREGATION_TEMPORALITY_DELTA = 1;
|
|
1036
|
-
AGGREGATION_TEMPORALITY_CUMULATIVE = 2;
|
|
1037
|
-
}
|
|
1038
|
-
message ResourceMetrics {
|
|
1039
|
-
opentelemetry.proto.resource.v1.Resource resource = 1;
|
|
1040
|
-
repeated ScopeMetrics scope_metrics = 2;
|
|
1041
|
-
string schema_url = 3;
|
|
1042
|
-
}
|
|
1043
|
-
message ScopeMetrics {
|
|
1044
|
-
opentelemetry.proto.common.v1.InstrumentationScope scope = 1;
|
|
1045
|
-
repeated Metric metrics = 2;
|
|
1046
|
-
string schema_url = 3;
|
|
1047
|
-
}
|
|
1048
|
-
message Metric {
|
|
1049
|
-
string name = 1;
|
|
1050
|
-
string description = 2;
|
|
1051
|
-
string unit = 3;
|
|
1052
|
-
oneof data {
|
|
1053
|
-
Gauge gauge = 5;
|
|
1054
|
-
Sum sum = 7;
|
|
1055
|
-
Histogram histogram = 9;
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
message Gauge {
|
|
1059
|
-
repeated NumberDataPoint data_points = 1;
|
|
1060
|
-
}
|
|
1061
|
-
message Sum {
|
|
1062
|
-
repeated NumberDataPoint data_points = 1;
|
|
1063
|
-
AggregationTemporality aggregation_temporality = 2;
|
|
1064
|
-
bool is_monotonic = 3;
|
|
1065
|
-
}
|
|
1066
|
-
message Histogram {
|
|
1067
|
-
repeated HistogramDataPoint data_points = 1;
|
|
1068
|
-
AggregationTemporality aggregation_temporality = 2;
|
|
1069
|
-
}
|
|
1070
|
-
message NumberDataPoint {
|
|
1071
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 7;
|
|
1072
|
-
fixed64 start_time_unix_nano = 2;
|
|
1073
|
-
fixed64 time_unix_nano = 3;
|
|
1074
|
-
oneof value {
|
|
1075
|
-
double as_double = 4;
|
|
1076
|
-
sfixed64 as_int = 6;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
message HistogramDataPoint {
|
|
1080
|
-
repeated opentelemetry.proto.common.v1.KeyValue attributes = 9;
|
|
1081
|
-
fixed64 start_time_unix_nano = 2;
|
|
1082
|
-
fixed64 time_unix_nano = 3;
|
|
1083
|
-
fixed64 count = 4;
|
|
1084
|
-
double sum = 5;
|
|
1085
|
-
repeated fixed64 bucket_counts = 6;
|
|
1086
|
-
repeated double explicit_bounds = 7;
|
|
1087
|
-
}
|
|
1088
|
-
message ExportMetricsServiceRequest {
|
|
1089
|
-
repeated ResourceMetrics resource_metrics = 1;
|
|
1090
|
-
}
|
|
1091
|
-
`;
|
|
1092
|
-
const TO_OBJECT_OPTIONS = {
|
|
1093
|
-
longs: String,
|
|
1094
|
-
bytes: String,
|
|
1095
|
-
defaults: false
|
|
1096
|
-
};
|
|
1097
|
-
let cachedRoot = null;
|
|
1098
|
-
function getRoot() {
|
|
1099
|
-
if (cachedRoot) return cachedRoot;
|
|
1100
|
-
const root = new protobuf.Root();
|
|
1101
|
-
for (const source of [
|
|
1102
|
-
COMMON_PROTO,
|
|
1103
|
-
RESOURCE_PROTO,
|
|
1104
|
-
TRACE_PROTO,
|
|
1105
|
-
LOGS_PROTO,
|
|
1106
|
-
METRICS_PROTO
|
|
1107
|
-
]) protobuf.parse(source, root, { keepCase: false });
|
|
1108
|
-
root.resolveAll();
|
|
1109
|
-
cachedRoot = root;
|
|
1110
|
-
return root;
|
|
1111
|
-
}
|
|
1112
|
-
function decodeRequest(typeName, body) {
|
|
1113
|
-
const messageType = getRoot().lookupType(typeName);
|
|
1114
|
-
const message = messageType.decode(body);
|
|
1115
|
-
return messageType.toObject(message, TO_OBJECT_OPTIONS);
|
|
1116
|
-
}
|
|
1117
|
-
/** Decode an OTLP/protobuf `ExportTraceServiceRequest` into the OTLP/JSON object shape. */
|
|
1118
|
-
function decodeOtlpTraceRequest(body) {
|
|
1119
|
-
return decodeRequest("opentelemetry.proto.trace.v1.ExportTraceServiceRequest", body);
|
|
1120
|
-
}
|
|
1121
|
-
/** Decode an OTLP/protobuf `ExportLogsServiceRequest` into the OTLP/JSON object shape. */
|
|
1122
|
-
function decodeOtlpLogsRequest(body) {
|
|
1123
|
-
return decodeRequest("opentelemetry.proto.logs.v1.ExportLogsServiceRequest", body);
|
|
1124
|
-
}
|
|
1125
|
-
/** Decode an OTLP/protobuf `ExportMetricsServiceRequest` into the OTLP/JSON object shape. */
|
|
1126
|
-
function decodeOtlpMetricsRequest(body) {
|
|
1127
|
-
return decodeRequest("opentelemetry.proto.metrics.v1.ExportMetricsServiceRequest", body);
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
//#endregion
|
|
1131
|
-
//#region src/server/identity.ts
|
|
1132
|
-
/** Value of the `x-autotel-devtools` response header and the /healthz `service` field. */
|
|
1133
|
-
const DEVTOOLS_IDENTITY = "autotel-devtools";
|
|
1134
|
-
/**
|
|
1135
|
-
* Probe `host:port` over HTTP and classify what is listening. Used when our
|
|
1136
|
-
* requested port is busy: it lets us tell "a stale autotel-devtools is still
|
|
1137
|
-
* up" (benign) apart from "a foreign collector owns this port" — the latter is
|
|
1138
|
-
* the silent footgun where apps keep exporting OTLP to the busy port and reach
|
|
1139
|
-
* the wrong process, so the devtools UI stays empty and the app sees errors.
|
|
1140
|
-
*/
|
|
1141
|
-
async function probePortHolder(host, port, timeoutMs = 500) {
|
|
1142
|
-
const authority = host.includes(":") ? `[${host}]` : host;
|
|
1143
|
-
const controller = new AbortController();
|
|
1144
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1145
|
-
try {
|
|
1146
|
-
const res = await fetch(`http://${authority}:${port}/healthz`, { signal: controller.signal });
|
|
1147
|
-
if (res.headers.get("x-autotel-devtools")) return "autotel-devtools";
|
|
1148
|
-
try {
|
|
1149
|
-
const body = await res.json();
|
|
1150
|
-
if (body && body.service === "autotel-devtools") return "autotel-devtools";
|
|
1151
|
-
} catch {}
|
|
1152
|
-
return "foreign";
|
|
1153
|
-
} catch {
|
|
1154
|
-
return "none";
|
|
1155
|
-
} finally {
|
|
1156
|
-
clearTimeout(timer);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
//#endregion
|
|
1161
|
-
//#region src/server/source-file.ts
|
|
1162
|
-
/** Refuse to slurp something huge just because a span named it. */
|
|
1163
|
-
const MAX_BYTES = 2e6;
|
|
1164
|
-
const DISABLED = /* @__PURE__ */ new Set([
|
|
1165
|
-
"false",
|
|
1166
|
-
"0",
|
|
1167
|
-
"off",
|
|
1168
|
-
"no",
|
|
1169
|
-
""
|
|
1170
|
-
]);
|
|
1171
|
-
/**
|
|
1172
|
-
* Decide what `GET /source` may read, from `AUTOTEL_DEVTOOLS_SOURCE_ROOT`.
|
|
1173
|
-
*
|
|
1174
|
-
* Defaults **on**, at the working directory: devtools is a local tool whose
|
|
1175
|
-
* whole point is showing you your own code, and requiring a flag for that would
|
|
1176
|
-
* mean nobody ever sees the feature. The blast radius stays small because two
|
|
1177
|
-
* other things still hold — the receiver is bound to loopback, and nothing
|
|
1178
|
-
* outside this directory is reachable. Set the variable to `false` to turn it
|
|
1179
|
-
* off outright.
|
|
1180
|
-
*
|
|
1181
|
-
* A non-loopback bind (`--host 0.0.0.0`) removes the first of those, and the
|
|
1182
|
-
* Origin guard does not replace it: a request with no `Origin` at all — any
|
|
1183
|
-
* `curl` on the network — passes. The root holds whatever else lives in the
|
|
1184
|
-
* project, `.env` included, so the default flips to **off** there. An explicit
|
|
1185
|
-
* root is still honoured: exposing it on purpose is the caller's call.
|
|
1186
|
-
*/
|
|
1187
|
-
function resolveSourceRoot(configured, cwd, loopbackOnly = true) {
|
|
1188
|
-
if (configured === void 0) return loopbackOnly ? cwd : void 0;
|
|
1189
|
-
if (DISABLED.has(configured.trim().toLowerCase())) return void 0;
|
|
1190
|
-
return configured;
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* Resolve `requested` against `root`, or return `null` if it escapes.
|
|
1194
|
-
*
|
|
1195
|
-
* Containment is judged on **real** paths so a symlink inside the root that
|
|
1196
|
-
* points outside it is rejected — lexical `..` stripping alone cannot see that.
|
|
1197
|
-
* The value returned is the *lexical* resolution, because the real one differs
|
|
1198
|
-
* from the caller's path whenever an ancestor is a symlink (on macOS both
|
|
1199
|
-
* `/tmp` and `/var` are), and a caller comparing paths should not have to know.
|
|
1200
|
-
*/
|
|
1201
|
-
function resolveWithinRoot(root, requested) {
|
|
1202
|
-
const lexicalRoot = path.resolve(root);
|
|
1203
|
-
const realRoot = safeRealpath(lexicalRoot);
|
|
1204
|
-
if (realRoot === null) return null;
|
|
1205
|
-
const lexicalTarget = path.resolve(lexicalRoot, requested);
|
|
1206
|
-
const realTarget = safeRealpath(lexicalTarget);
|
|
1207
|
-
if (realTarget === null) return null;
|
|
1208
|
-
if (!isInside(realRoot, realTarget)) return null;
|
|
1209
|
-
return lexicalTarget;
|
|
1210
|
-
}
|
|
1211
|
-
/** True when `target` is `root` itself or sits beneath it. */
|
|
1212
|
-
function isInside(root, target) {
|
|
1213
|
-
if (target === root) return true;
|
|
1214
|
-
return target.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
|
|
1215
|
-
}
|
|
1216
|
-
function safeRealpath(p) {
|
|
1217
|
-
try {
|
|
1218
|
-
return realpathSync(p);
|
|
1219
|
-
} catch {
|
|
1220
|
-
return null;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Read `context` lines either side of `line` from a file inside `root`.
|
|
1225
|
-
* Returns `null` when the path escapes the root, is not a readable file, or is
|
|
1226
|
-
* too large — the caller cannot distinguish those, which is the point.
|
|
1227
|
-
*/
|
|
1228
|
-
function readSourceWindow(root, requested, line, context) {
|
|
1229
|
-
const resolved = resolveWithinRoot(root, requested);
|
|
1230
|
-
if (resolved === null) return null;
|
|
1231
|
-
let text;
|
|
1232
|
-
try {
|
|
1233
|
-
const stat = statSync(resolved);
|
|
1234
|
-
if (!stat.isFile() || stat.size > MAX_BYTES) return null;
|
|
1235
|
-
text = readFileSync(resolved, "utf8");
|
|
1236
|
-
} catch {
|
|
1237
|
-
return null;
|
|
1238
|
-
}
|
|
1239
|
-
const all = text.split("\n");
|
|
1240
|
-
if (all.at(-1) === "") all.pop();
|
|
1241
|
-
const startLine = Math.max(1, line - context);
|
|
1242
|
-
const endLine = Math.min(all.length, line + context);
|
|
1243
|
-
if (startLine > all.length) return null;
|
|
1244
|
-
return {
|
|
1245
|
-
file: path.relative(path.resolve(root), resolved),
|
|
1246
|
-
line,
|
|
1247
|
-
startLine,
|
|
1248
|
-
lines: all.slice(startLine - 1, endLine)
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
//#endregion
|
|
1253
|
-
//#region src/server/http.ts
|
|
1254
|
-
function sendOtlpError(res, req, e) {
|
|
1255
|
-
sendJson(res, 400, {
|
|
1256
|
-
error: "Invalid OTLP payload",
|
|
1257
|
-
message: e instanceof Error ? e.message : String(e),
|
|
1258
|
-
contentType: req.headers["content-type"] ?? null
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
const PROTOBUF_DECODERS = {
|
|
1262
|
-
traces: decodeOtlpTraceRequest,
|
|
1263
|
-
logs: decodeOtlpLogsRequest,
|
|
1264
|
-
metrics: decodeOtlpMetricsRequest
|
|
1265
|
-
};
|
|
1266
|
-
async function readOtlpPayload(req, signal) {
|
|
1267
|
-
if (isProtobufContentType(req.headers["content-type"])) return PROTOBUF_DECODERS[signal](await readRawBody(req));
|
|
1268
|
-
return readJsonBody(req);
|
|
1269
|
-
}
|
|
1270
|
-
function findPackageRoot() {
|
|
1271
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
1272
|
-
for (let i = 0; i < 5; i++) {
|
|
1273
|
-
if (existsSync(resolve(dir, "package.json"))) return dir;
|
|
1274
|
-
dir = dirname(dir);
|
|
1275
|
-
}
|
|
1276
|
-
return dir;
|
|
1277
|
-
}
|
|
1278
|
-
const DEVTOOLS_FAVICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><rect width=\"64\" height=\"64\" rx=\"14\" fill=\"#0f172a\"/><text x=\"32\" y=\"41\" text-anchor=\"middle\" font-size=\"32\">🛰️</text></svg>";
|
|
1279
|
-
/**
|
|
1280
|
-
* The title is user-supplied (`--title` / `AUTOTEL_DEVTOOLS_TITLE`) and lands
|
|
1281
|
-
* inside `<title>`, where an unescaped `<` would close the element early.
|
|
1282
|
-
*/
|
|
1283
|
-
function escapeHtml(value) {
|
|
1284
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1285
|
-
}
|
|
1286
|
-
function renderFullpageHtml(title = "autotel-devtools") {
|
|
1287
|
-
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${escapeHtml(title)}</title><link rel="icon" href="/favicon.svg" type="image/svg+xml"><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>`;
|
|
1288
|
-
}
|
|
1289
|
-
let cachedVersion = null;
|
|
1290
|
-
function getVersion() {
|
|
1291
|
-
if (cachedVersion !== null) return cachedVersion;
|
|
1292
|
-
let version = "unknown";
|
|
1293
|
-
try {
|
|
1294
|
-
const pkg = JSON.parse(readFileSync(resolve(findPackageRoot(), "package.json"), "utf8"));
|
|
1295
|
-
if (typeof pkg.version === "string") version = pkg.version;
|
|
1296
|
-
} catch {}
|
|
1297
|
-
cachedVersion = version;
|
|
1298
|
-
return version;
|
|
1299
|
-
}
|
|
1300
|
-
let cachedWidgetJs = null;
|
|
1301
|
-
function getWidgetJs() {
|
|
1302
|
-
if (!cachedWidgetJs) {
|
|
1303
|
-
const pkgRoot = findPackageRoot();
|
|
1304
|
-
const candidates = [resolve(pkgRoot, "dist", "widget.global.js"), resolve(pkgRoot, "widget.global.js")];
|
|
1305
|
-
for (const candidate of candidates) try {
|
|
1306
|
-
cachedWidgetJs = readFileSync(candidate, "utf8");
|
|
1307
|
-
break;
|
|
1308
|
-
} catch {}
|
|
1309
|
-
if (!cachedWidgetJs) cachedWidgetJs = "// widget bundle not found - run pnpm build first";
|
|
1310
|
-
}
|
|
1311
|
-
return cachedWidgetJs;
|
|
1312
|
-
}
|
|
1313
|
-
function attachDevtoolsRoutes(httpServer, devtools, options = {}) {
|
|
1314
|
-
const loopbackOnly = options.loopbackOnly ?? true;
|
|
1315
|
-
const sourceRoot = options.sourceRoot;
|
|
1316
|
-
const fullpageHtml = renderFullpageHtml(options.title);
|
|
1317
|
-
httpServer.on("request", async (req, res) => {
|
|
1318
|
-
if (req.headers.upgrade?.toLowerCase() === "websocket") return;
|
|
1319
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1320
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
1321
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1322
|
-
res.setHeader("x-autotel-devtools", getVersion());
|
|
1323
|
-
res.setHeader("Access-Control-Expose-Headers", "x-autotel-devtools");
|
|
1324
|
-
if (req.method === "OPTIONS") {
|
|
1325
|
-
res.writeHead(204);
|
|
1326
|
-
res.end();
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
const url = req.url || "/";
|
|
1330
|
-
if (req.method === "GET" && url === "/") {
|
|
1331
|
-
res.writeHead(200, {
|
|
1332
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
1333
|
-
"Content-Length": Buffer.byteLength(fullpageHtml)
|
|
1334
|
-
});
|
|
1335
|
-
res.end(fullpageHtml);
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
if (req.method === "GET" && url.startsWith("/widget.js")) {
|
|
1339
|
-
const js = getWidgetJs();
|
|
1340
|
-
res.writeHead(200, {
|
|
1341
|
-
"Content-Type": "application/javascript; charset=utf-8",
|
|
1342
|
-
"Content-Length": Buffer.byteLength(js)
|
|
1343
|
-
});
|
|
1344
|
-
res.end(js);
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
if (req.method === "GET" && (url === "/favicon.svg" || url === "/favicon.ico")) {
|
|
1348
|
-
res.writeHead(200, {
|
|
1349
|
-
"Content-Type": "image/svg+xml; charset=utf-8",
|
|
1350
|
-
"Cache-Control": "public, max-age=86400",
|
|
1351
|
-
"Content-Length": Buffer.byteLength(DEVTOOLS_FAVICON_SVG)
|
|
1352
|
-
});
|
|
1353
|
-
res.end(DEVTOOLS_FAVICON_SVG);
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
if (req.method === "GET" && url.split("?")[0] === "/source") {
|
|
1357
|
-
if (!sourceRoot) {
|
|
1358
|
-
sendJson(res, 404, { error: "Not found" });
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1362
|
-
sendJson(res, 403, { error: "Forbidden" });
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
const query = new URL(url, "http://localhost").searchParams;
|
|
1366
|
-
const file = query.get("file");
|
|
1367
|
-
const line = Number(query.get("line"));
|
|
1368
|
-
const context = Math.min(Math.max(Number(query.get("context") ?? 5) || 0, 0), 50);
|
|
1369
|
-
if (!file || !Number.isInteger(line) || line < 1) {
|
|
1370
|
-
sendJson(res, 400, { error: "file and a positive integer line are required" });
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
const window = readSourceWindow(sourceRoot, file, line, context);
|
|
1374
|
-
if (window === null) {
|
|
1375
|
-
sendJson(res, 404, { error: "Not found" });
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
sendJson(res, 200, { ...window });
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
if (req.method === "GET" && url === "/healthz") {
|
|
1382
|
-
sendJson(res, 200, {
|
|
1383
|
-
ok: true,
|
|
1384
|
-
service: DEVTOOLS_IDENTITY,
|
|
1385
|
-
version: getVersion(),
|
|
1386
|
-
clients: devtools.clientCount
|
|
1387
|
-
});
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
if (req.method === "GET" && url === "/v1/traces") {
|
|
1391
|
-
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1392
|
-
sendJson(res, 403, { error: "Forbidden" });
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
const data = devtools.getCurrentData();
|
|
1396
|
-
sendJson(res, 200, {
|
|
1397
|
-
traces: data.traces,
|
|
1398
|
-
count: data.traces.length
|
|
1399
|
-
});
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
if (req.method === "DELETE" && url === "/v1/traces") {
|
|
1403
|
-
if (!allowSensitiveRequest(req.headers, loopbackOnly)) {
|
|
1404
|
-
sendJson(res, 403, { error: "Forbidden" });
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1407
|
-
devtools.clearData();
|
|
1408
|
-
sendJson(res, 200, { cleared: true });
|
|
1409
|
-
return;
|
|
1410
|
-
}
|
|
1411
|
-
if (req.method === "POST" && url === "/v1/traces") {
|
|
1412
|
-
try {
|
|
1413
|
-
const traces = parseOtlpTraces(await readOtlpPayload(req, "traces"));
|
|
1414
|
-
devtools.addTraces(traces);
|
|
1415
|
-
sendJson(res, 200, { acceptedTraces: traces.length });
|
|
1416
|
-
} catch (e) {
|
|
1417
|
-
sendOtlpError(res, req, e);
|
|
1418
|
-
}
|
|
1419
|
-
return;
|
|
1420
|
-
}
|
|
1421
|
-
if (req.method === "POST" && url === "/v1/logs") {
|
|
1422
|
-
try {
|
|
1423
|
-
const payload = await readOtlpPayload(req, "logs");
|
|
1424
|
-
const logs = parseOtlpLogs(payload);
|
|
1425
|
-
devtools.addLogs(logs);
|
|
1426
|
-
devtools.ingestAgentEvents(parseOtlpAgentEvents(payload));
|
|
1427
|
-
sendJson(res, 200, { acceptedLogs: logs.length });
|
|
1428
|
-
} catch (e) {
|
|
1429
|
-
sendOtlpError(res, req, e);
|
|
1430
|
-
}
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
if (req.method === "POST" && url === "/v1/metrics") {
|
|
1434
|
-
try {
|
|
1435
|
-
const payload = await readOtlpPayload(req, "metrics");
|
|
1436
|
-
devtools.ingestAgentMetrics(parseOtlpMetrics(payload));
|
|
1437
|
-
sendJson(res, 200, { acceptedMetrics: countOtlpMetrics(payload) });
|
|
1438
|
-
} catch (e) {
|
|
1439
|
-
sendOtlpError(res, req, e);
|
|
1440
|
-
}
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
sendJson(res, 404, { error: "Not found" });
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
function createDevtoolsHttpServer(devtools, _options = {}) {
|
|
1447
|
-
const server = createServer();
|
|
1448
|
-
attachDevtoolsRoutes(server, devtools);
|
|
1449
|
-
return server;
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
//#endregion
|
|
1453
|
-
export { appendManyWithLimit as _, probePortHolder as a, resolveTelemetryLimits as b, decodeOtlpTraceRequest as c, parseOtlpTraces as d, DevtoolsServer as f, originIsLoopback as g, isLoopbackHostname as h, DEVTOOLS_IDENTITY as i, isProtobufContentType as l, hostHeaderIsLoopback as m, createDevtoolsHttpServer as n, decodeOtlpLogsRequest as o, allowSensitiveRequest as p, resolveSourceRoot as r, decodeOtlpMetricsRequest as s, attachDevtoolsRoutes as t, parseOtlpLogs as u, appendWithLimit as v, ErrorAggregator as x, applyTelemetryLimits as y };
|