@autter/otlp-ingester 1.6.1 → 1.8.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 +31 -5
- package/dist/auth.d.ts +16 -1
- package/dist/auth.js +34 -10
- package/dist/clickhouse.d.ts +3 -0
- package/dist/clickhouse.js +38 -1
- package/dist/compat-manifest.json +37 -0
- package/dist/config.d.ts +23 -2
- package/dist/config.js +33 -8
- package/dist/context.d.ts +5 -0
- package/dist/context.js +49 -10
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -2
- package/dist/ingest-drops.d.ts +308 -0
- package/dist/ingest-drops.js +448 -0
- package/dist/log-promotion.d.ts +4 -3
- package/dist/log-promotion.js +5 -5
- package/dist/logs.d.ts +16 -1
- package/dist/logs.js +17 -3
- package/dist/memory.d.ts +9 -1
- package/dist/memory.js +28 -7
- package/dist/migrations.d.ts +8 -0
- package/dist/migrations.js +29 -1
- package/dist/normalize-browser.d.ts +53 -25
- package/dist/normalize-browser.js +56 -10
- package/dist/normalize-otlp.d.ts +6 -0
- package/dist/normalize-otlp.js +275 -108
- package/dist/otlp-proto.d.ts +14 -0
- package/dist/otlp-proto.js +25 -0
- package/dist/profiles.d.ts +4 -0
- package/dist/profiles.js +6 -2
- package/dist/request-spans.d.ts +45 -0
- package/dist/request-spans.js +91 -0
- package/dist/sdk-versions.d.ts +39 -2
- package/dist/sdk-versions.js +49 -8
- package/dist/server.d.ts +10 -0
- package/dist/server.js +345 -105
- package/dist/sink.d.ts +14 -0
- package/dist/sink.js +15 -0
- package/dist/source-maps-lookup.d.ts +6 -3
- package/dist/source-maps-lookup.js +35 -13
- package/dist/source-maps.d.ts +6 -2
- package/dist/source-maps.js +12 -4
- package/dist/stack-frames.d.ts +5 -2
- package/dist/stack-frames.js +55 -9
- package/dist/symbolicate.d.ts +13 -0
- package/dist/symbolicate.js +22 -0
- package/dist/trace-dedupe.d.ts +98 -0
- package/dist/trace-dedupe.js +385 -0
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
package/dist/normalize-otlp.js
CHANGED
|
@@ -5,6 +5,8 @@ import { scrubText } from "./redact.js";
|
|
|
5
5
|
import { normalizeRoute } from "./fingerprint.js";
|
|
6
6
|
import { extractLlmCall } from "./llm.js";
|
|
7
7
|
import { BREADCRUMBS_ATTRIBUTE, breadcrumbsField } from "./breadcrumbs.js";
|
|
8
|
+
import { dedupeTraceSignals, hasAutterKey, hasNextContext } from "./trace-dedupe.js";
|
|
9
|
+
import { requestMembership } from "./request-spans.js";
|
|
8
10
|
import { asSeverity, } from "./types.js";
|
|
9
11
|
const EMAIL_VALUE_RE = /[A-Z0-9._%+-]{1,64}@[A-Z0-9.-]{1,253}\.[A-Z]{2,63}/gi;
|
|
10
12
|
function attrMap(attributes) {
|
|
@@ -56,13 +58,18 @@ function spanKind(kind) {
|
|
|
56
58
|
}
|
|
57
59
|
function resourceInfo(resource) {
|
|
58
60
|
const attrs = attrMap(resource?.attributes);
|
|
61
|
+
const service = attrs.get("service.name") ?? "unknown";
|
|
62
|
+
const environment = attrs.get("deployment.environment.name") ??
|
|
63
|
+
attrs.get("deployment.environment") ??
|
|
64
|
+
"production";
|
|
65
|
+
const release = attrs.get("service.version") ?? null;
|
|
59
66
|
return {
|
|
60
|
-
service
|
|
61
|
-
environment
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
release: attrs.get("service.version") ?? null,
|
|
67
|
+
service,
|
|
68
|
+
environment,
|
|
69
|
+
release,
|
|
70
|
+
metricsCountRoutedRequests: attrs.get("autter.metrics_wired") === "true" && attrs.get("autter.metrics_routes") === "true",
|
|
65
71
|
metricsWired: attrs.get("autter.metrics_wired") === "true",
|
|
72
|
+
process: [service, environment, release ?? "", attrs.get("service.instance.id") ?? ""].join("\u0000"),
|
|
66
73
|
};
|
|
67
74
|
}
|
|
68
75
|
function routeOf(attrs) {
|
|
@@ -72,6 +79,13 @@ function routeOf(attrs) {
|
|
|
72
79
|
null;
|
|
73
80
|
return route ? (route.split("?")[0] ?? null) : null;
|
|
74
81
|
}
|
|
82
|
+
/** How good a request name `routeOf` found: 2 for a route template
|
|
83
|
+
* (`http.route`), 1 for a raw path (`url.path` / `http.target`), 0 for none. */
|
|
84
|
+
function routeQualityOf(attrs, route) {
|
|
85
|
+
if (!route)
|
|
86
|
+
return 0;
|
|
87
|
+
return attrs.has("http.route") ? 2 : 1;
|
|
88
|
+
}
|
|
75
89
|
function statusCodeOf(attrs) {
|
|
76
90
|
const raw = attrs.get("http.response.status_code") ?? attrs.get("http.status_code");
|
|
77
91
|
if (!raw)
|
|
@@ -100,9 +114,13 @@ function severityOf(eventAttrs, spanAttrs) {
|
|
|
100
114
|
}
|
|
101
115
|
const MAX_SPANS_PER_REQUEST = 5000;
|
|
102
116
|
export function normalizeTraces(request) {
|
|
103
|
-
const
|
|
104
|
-
|
|
117
|
+
const signals = [];
|
|
118
|
+
// Per signal: the span it was recorded on (its index in `spans`), and the
|
|
119
|
+
// route its own event rollup keys on (before HTTP inheritance).
|
|
120
|
+
const signalRollups = [];
|
|
105
121
|
const spans = [];
|
|
122
|
+
// Per span, what the request rollups need beyond the span row.
|
|
123
|
+
const spanFacts = [];
|
|
106
124
|
const llmCalls = [];
|
|
107
125
|
const rollups = new Map();
|
|
108
126
|
let spanCount = 0;
|
|
@@ -143,39 +161,61 @@ export function normalizeTraces(request) {
|
|
|
143
161
|
},
|
|
144
162
|
startedAt,
|
|
145
163
|
});
|
|
146
|
-
|
|
164
|
+
spanFacts.push({
|
|
165
|
+
process: resource.process,
|
|
166
|
+
routeQuality: routeQualityOf(attrs, route),
|
|
167
|
+
// Who counts this span's request, as its outermost server
|
|
168
|
+
// span: see countedBy.
|
|
169
|
+
countedBy: countedBy(resource, attrs),
|
|
170
|
+
templateRoute: attrs.has("http.route") && route ? normalizeRoute(route) : "",
|
|
171
|
+
});
|
|
172
|
+
// Error signals: one per exception event; if the span is
|
|
147
173
|
// errored without exception events, one from the span status.
|
|
148
|
-
|
|
174
|
+
// They become occurrences after the batch-wide dedupe below.
|
|
175
|
+
const traceId = span.traceId ?? "";
|
|
176
|
+
const spanId = span.spanId ?? "";
|
|
177
|
+
const rollup = { spanIndex: spans.length - 1, route: route ?? "" };
|
|
178
|
+
const spanAutter = attrs.has("autter.severity");
|
|
149
179
|
const exceptionEvents = (span.events ?? []).filter((event) => event.name === "exception");
|
|
150
180
|
for (const event of exceptionEvents) {
|
|
151
181
|
const eventAttrs = attrMap(event.attributes);
|
|
152
182
|
const handled = eventAttrs.get("autter.handled") === "true";
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
const eventAutter = hasAutterKey(eventAttrs);
|
|
184
|
+
signalRollups.push(rollup);
|
|
185
|
+
signals.push({
|
|
186
|
+
traceId,
|
|
187
|
+
spanId,
|
|
188
|
+
signal: "exception",
|
|
189
|
+
autterCapture: eventAutter || spanAutter,
|
|
190
|
+
eventAutter,
|
|
191
|
+
nextContext: hasNextContext(eventAttrs, attrs),
|
|
192
|
+
handled,
|
|
193
|
+
occurrence: {
|
|
194
|
+
source: "server",
|
|
195
|
+
severity: severityOf(eventAttrs, attrs),
|
|
196
|
+
service: resource.service,
|
|
197
|
+
environment: resource.environment,
|
|
198
|
+
release: resource.release,
|
|
199
|
+
errorType: eventAttrs.get("exception.type") ?? "Error",
|
|
200
|
+
message: eventAttrs.get("exception.message") ??
|
|
201
|
+
span.status?.message ??
|
|
202
|
+
span.name ??
|
|
203
|
+
"Unknown error",
|
|
204
|
+
stack: eventAttrs.get("exception.stacktrace") ?? null,
|
|
205
|
+
route,
|
|
206
|
+
method: methodOf(attrs),
|
|
207
|
+
statusCode,
|
|
208
|
+
traceId: span.traceId ?? null,
|
|
209
|
+
sessionId: null,
|
|
210
|
+
attributes: sanitizeRuntimeContext({ ...decodeOtlpAttributes(span.attributes), ...decodeOtlpAttributes(event.attributes),
|
|
211
|
+
...(handled ? { "autter.handled": true, "autter.sampled": eventAttrs.get("autter.sampled") === "true" } : {}) }),
|
|
212
|
+
occurredAt: nanosToDate(event.timeUnixNano ?? span.startTimeUnixNano),
|
|
213
|
+
// Event attributes first: one span can carry several
|
|
214
|
+
// exceptions, each with its own code.
|
|
215
|
+
...liftErrorFields(eventAttrs, attrs),
|
|
216
|
+
...breadcrumbsField(eventAttrs.get(BREADCRUMBS_ATTRIBUTE), attrs.get(BREADCRUMBS_ATTRIBUTE)),
|
|
217
|
+
...fingerprintOverrideOf(eventAttrs.get("autter.fingerprint") ?? attrs.get("autter.fingerprint")),
|
|
218
|
+
},
|
|
179
219
|
});
|
|
180
220
|
}
|
|
181
221
|
const failedOutcomes = (span.events ?? []).filter((event) => {
|
|
@@ -186,40 +226,58 @@ export function normalizeTraces(request) {
|
|
|
186
226
|
for (const event of failedOutcomes) {
|
|
187
227
|
const outcome = attrMap(event.attributes);
|
|
188
228
|
const name = (outcome.get("autter.outcome.name") ?? "operation").replace(EMAIL_VALUE_RE, "[redacted]").slice(0, 200);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
229
|
+
signalRollups.push(rollup);
|
|
230
|
+
signals.push({
|
|
231
|
+
traceId,
|
|
232
|
+
spanId,
|
|
233
|
+
signal: "outcome",
|
|
234
|
+
autterCapture: true,
|
|
235
|
+
eventAutter: true,
|
|
236
|
+
nextContext: hasNextContext(outcome, attrs),
|
|
237
|
+
handled: false,
|
|
238
|
+
occurrence: {
|
|
239
|
+
source: "server", severity: "error", service: resource.service,
|
|
240
|
+
environment: resource.environment, release: resource.release,
|
|
241
|
+
errorType: "OutcomeFailure",
|
|
242
|
+
message: `${name}: ${(outcome.get("autter.outcome.message") ?? "failed").replace(EMAIL_VALUE_RE, "[redacted]").slice(0, 1000)}`,
|
|
243
|
+
stack: String(decodeOtlpAttributes(event.attributes)["autter.outcome.stack"] ?? "") || null, route, method: methodOf(attrs), statusCode,
|
|
244
|
+
traceId: span.traceId ?? null, sessionId: null,
|
|
245
|
+
attributes: sanitizeRuntimeContext({ ...decodeOtlpAttributes(span.attributes), ...decodeOtlpAttributes(event.attributes) }),
|
|
246
|
+
occurredAt: nanosToDate(event.timeUnixNano ?? span.startTimeUnixNano),
|
|
247
|
+
...liftErrorFields(outcome, attrs),
|
|
248
|
+
},
|
|
200
249
|
});
|
|
201
250
|
}
|
|
202
251
|
if (exceptionEvents.length === 0 && failedOutcomes.length === 0 && isError) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
252
|
+
signalRollups.push(rollup);
|
|
253
|
+
signals.push({
|
|
254
|
+
traceId,
|
|
255
|
+
spanId,
|
|
256
|
+
signal: "status",
|
|
257
|
+
autterCapture: spanAutter,
|
|
258
|
+
eventAutter: false,
|
|
259
|
+
nextContext: hasNextContext(attrs),
|
|
260
|
+
handled: false,
|
|
261
|
+
occurrence: {
|
|
262
|
+
source: "server",
|
|
263
|
+
severity: severityOf(new Map(), attrs),
|
|
264
|
+
service: resource.service,
|
|
265
|
+
environment: resource.environment,
|
|
266
|
+
release: resource.release,
|
|
267
|
+
errorType: statusCode !== null && statusCode >= 500 ? "HttpServerError" : "SpanError",
|
|
268
|
+
message: span.status?.message || (statusCode !== null && statusCode >= 500
|
|
269
|
+
? `${methodOf(attrs) ?? "HTTP"} ${route ?? span.name ?? "request"} returned ${statusCode}`
|
|
270
|
+
: `${span.name ?? "span"} failed`),
|
|
271
|
+
stack: null,
|
|
272
|
+
route,
|
|
273
|
+
method: methodOf(attrs),
|
|
274
|
+
statusCode,
|
|
275
|
+
traceId: span.traceId ?? null,
|
|
276
|
+
sessionId: null,
|
|
277
|
+
attributes: decodeOtlpAttributes(span.attributes),
|
|
278
|
+
occurredAt: startedAt,
|
|
279
|
+
...liftErrorFields(attrs),
|
|
280
|
+
},
|
|
223
281
|
});
|
|
224
282
|
}
|
|
225
283
|
// LLM provider calls (GenAI semconv, Vercel AI SDK telemetry,
|
|
@@ -243,58 +301,159 @@ export function normalizeTraces(request) {
|
|
|
243
301
|
});
|
|
244
302
|
if (llmCall)
|
|
245
303
|
llmCalls.push(llmCall);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// One occurrence per thrown error: recordings of one error on several
|
|
308
|
+
// spans of a trace merge, and status-only occurrences an error on their
|
|
309
|
+
// span chain explains are dropped (trace-dedupe.ts).
|
|
310
|
+
const deduped = dedupeTraceSignals(signals, spans);
|
|
311
|
+
const occurrences = deduped.occurrences;
|
|
312
|
+
// Usage rollups (`runtime_metrics_1m`): `request_count` counts requests
|
|
313
|
+
// and only requests, `error_count` counts failed requests and errors
|
|
314
|
+
// outside any request. A request is its outermost server span
|
|
315
|
+
// (request-spans.ts); its server spans count it unless the sender's metric
|
|
316
|
+
// feed does (countedBySpans), and nothing else adds requests.
|
|
317
|
+
const membership = requestMembership(spans.map((span, i) => ({
|
|
318
|
+
traceId: span.traceId,
|
|
319
|
+
spanId: span.spanId,
|
|
320
|
+
parentSpanId: span.parentSpanId,
|
|
321
|
+
kind: span.kind,
|
|
322
|
+
process: spanFacts[i].process,
|
|
323
|
+
})));
|
|
324
|
+
const requests = spanRequests(spans, spanFacts, membership);
|
|
325
|
+
const requestOfSignal = (m) => {
|
|
326
|
+
const root = membership.requestOf[signalRollups[m].spanIndex];
|
|
327
|
+
return root >= 0 ? requests.get(root) : undefined;
|
|
328
|
+
};
|
|
329
|
+
// Each error or fatal occurrence (after the dedupe: one per thrown error)
|
|
330
|
+
// counts once. One recorded in a request (on its server spans or any span
|
|
331
|
+
// below them in the batch) makes that request a failed one; any other
|
|
332
|
+
// (captureException's always-on spans exported apart from their request,
|
|
333
|
+
// workers, consumers, jobs) is one error outside a request. Warnings and
|
|
334
|
+
// info count nothing; neither does a dropped status-only occurrence.
|
|
335
|
+
const errorsOutsideRequests = [];
|
|
336
|
+
deduped.members.forEach((members, i) => {
|
|
337
|
+
const occ = occurrences[i];
|
|
338
|
+
if (occ.severity !== "fatal" && occ.severity !== "error")
|
|
339
|
+
return;
|
|
340
|
+
const request = members.map(requestOfSignal).find((r) => r !== undefined);
|
|
341
|
+
if (request) {
|
|
342
|
+
request.hasErrorOccurrence = true;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
errorsOutsideRequests.push({
|
|
346
|
+
service: occ.service,
|
|
347
|
+
environment: occ.environment,
|
|
348
|
+
release: occ.release,
|
|
349
|
+
route: signalRollups[deduped.sourceIndexes[i]].route,
|
|
350
|
+
bucketAt: minuteBucket(occ.occurredAt),
|
|
351
|
+
requestCount: 0,
|
|
352
|
+
errorCount: 1,
|
|
353
|
+
durationSumMs: 0,
|
|
354
|
+
sessionCount: 0,
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
for (const [root, request] of requests) {
|
|
358
|
+
const span = spans[root];
|
|
359
|
+
const failed = request.isError || request.hasErrorOccurrence;
|
|
360
|
+
const point = {
|
|
361
|
+
service: span.service,
|
|
362
|
+
environment: span.environment,
|
|
363
|
+
release: span.release,
|
|
364
|
+
bucketAt: minuteBucket(span.startedAt),
|
|
365
|
+
sessionCount: 0,
|
|
366
|
+
};
|
|
367
|
+
switch (spanFacts[root].countedBy) {
|
|
368
|
+
case "spans":
|
|
369
|
+
addToRollup(rollups, {
|
|
370
|
+
...point,
|
|
371
|
+
route: request.route,
|
|
372
|
+
requestCount: 1,
|
|
373
|
+
errorCount: failed ? 1 : 0,
|
|
374
|
+
// The request's latency is its outermost span's.
|
|
375
|
+
durationSumMs: span.durationMs,
|
|
376
|
+
});
|
|
377
|
+
break;
|
|
378
|
+
case "metrics": {
|
|
379
|
+
// The metric feed counted it, and already made a 5xx response
|
|
380
|
+
// a failed request. A request it saw succeed that recorded an
|
|
381
|
+
// error adds that one error, on the route its point carried.
|
|
382
|
+
const metricCountedFailure = span.statusCode !== null && span.statusCode >= 500;
|
|
383
|
+
if (request.hasErrorOccurrence && !metricCountedFailure) {
|
|
384
|
+
addToRollup(rollups, { ...point, route: spanFacts[root].templateRoute, requestCount: 0, errorCount: 1, durationSumMs: 0 });
|
|
285
385
|
}
|
|
386
|
+
break;
|
|
286
387
|
}
|
|
388
|
+
case "none":
|
|
389
|
+
// Exported because it failed, with no metric feed counting
|
|
390
|
+
// requests: not a sample of the traffic, so no request; its
|
|
391
|
+
// failure counts once.
|
|
392
|
+
if (failed) {
|
|
393
|
+
addToRollup(rollups, { ...point, route: request.route, requestCount: 0, errorCount: 1, durationSumMs: 0 });
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
287
396
|
}
|
|
288
397
|
}
|
|
289
|
-
|
|
398
|
+
for (const error of errorsOutsideRequests)
|
|
399
|
+
addToRollup(rollups, error);
|
|
400
|
+
inheritHttpFromParent(occurrences, deduped.sourceIndexes.map((index) => signals[index].spanId), spans);
|
|
290
401
|
return {
|
|
291
402
|
occurrences,
|
|
403
|
+
occurrenceBatchIndexes: deduped.sourceIndexes,
|
|
292
404
|
spans,
|
|
293
405
|
metricPoints: [...rollups.values()],
|
|
294
406
|
llmCalls,
|
|
295
407
|
spanCount,
|
|
296
408
|
};
|
|
297
409
|
}
|
|
410
|
+
function countedBy(resource, attrs) {
|
|
411
|
+
if (resource.metricsCountRoutedRequests && attrs.get("autter.metric_has_route") === "true")
|
|
412
|
+
return "metrics";
|
|
413
|
+
if (attrs.get("autter.tail_retained") === "true")
|
|
414
|
+
return resource.metricsWired ? "metrics" : "none";
|
|
415
|
+
return "spans";
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Every request in the batch, keyed by the batch index of its outermost
|
|
419
|
+
* server span (request-spans.ts). Server spans nested in it add no request:
|
|
420
|
+
* they lend their route when it is better (a template such as Next's
|
|
421
|
+
* `/blog/[slug]` over the HTTP span's raw path; on a tie the shallower
|
|
422
|
+
* span's), and their error. Latency and minute bucket stay the outermost
|
|
423
|
+
* span's.
|
|
424
|
+
*/
|
|
425
|
+
function spanRequests(spans, facts, membership) {
|
|
426
|
+
const requests = new Map();
|
|
427
|
+
spans.forEach((span, i) => {
|
|
428
|
+
if (membership.requestOf[i] !== i)
|
|
429
|
+
return;
|
|
430
|
+
requests.set(i, {
|
|
431
|
+
route: span.route ?? "",
|
|
432
|
+
routeQuality: facts[i].routeQuality,
|
|
433
|
+
routeDepth: membership.depth[i],
|
|
434
|
+
isError: span.status === "error",
|
|
435
|
+
hasErrorOccurrence: false,
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
spans.forEach((span, i) => {
|
|
439
|
+
const root = membership.requestOf[i];
|
|
440
|
+
if (root === i || span.kind !== "server")
|
|
441
|
+
return;
|
|
442
|
+
const request = requests.get(root);
|
|
443
|
+
if (!request)
|
|
444
|
+
return;
|
|
445
|
+
const quality = facts[i].routeQuality;
|
|
446
|
+
const depth = membership.depth[i];
|
|
447
|
+
if (quality > request.routeQuality || (quality === request.routeQuality && depth < request.routeDepth)) {
|
|
448
|
+
request.route = span.route ?? "";
|
|
449
|
+
request.routeQuality = quality;
|
|
450
|
+
request.routeDepth = depth;
|
|
451
|
+
}
|
|
452
|
+
if (span.status === "error")
|
|
453
|
+
request.isError = true;
|
|
454
|
+
});
|
|
455
|
+
return requests;
|
|
456
|
+
}
|
|
298
457
|
/**
|
|
299
458
|
* Exception spans are often children named "Error" with no HTTP attributes.
|
|
300
459
|
* Copy method, route, status, and duration from the server span in the same
|
|
@@ -391,8 +550,10 @@ const HTTP_DURATION_INSTRUMENTS = {
|
|
|
391
550
|
* Cumulative histograms report lifetime totals on every export; adding them
|
|
392
551
|
* into a SummingMergeTree would re-count all past requests each interval.
|
|
393
552
|
* Only deltas are summable — cumulative senders are skipped and covered by
|
|
394
|
-
* the span-fed rollup fallback instead
|
|
395
|
-
* `
|
|
553
|
+
* the span-fed rollup fallback instead (a sender must not mark itself
|
|
554
|
+
* `autter.metrics_wired` with cumulative histograms). (Enum arrives as a
|
|
555
|
+
* number or an `AGGREGATION_TEMPORALITY_*` string depending on the
|
|
556
|
+
* serialiser.)
|
|
396
557
|
*/
|
|
397
558
|
function isCumulativeTemporality(t) {
|
|
398
559
|
return t === 2 || t === "AGGREGATION_TEMPORALITY_CUMULATIVE";
|
|
@@ -401,6 +562,10 @@ export function normalizeMetrics(request) {
|
|
|
401
562
|
const rollups = new Map();
|
|
402
563
|
for (const resourceMetric of request.resourceMetrics ?? []) {
|
|
403
564
|
const resource = resourceInfo(resourceMetric.resource);
|
|
565
|
+
// An SDK migrating semantic conventions can emit both instruments for
|
|
566
|
+
// every request (OTel's `OTEL_SEMCONV_STABILITY_OPT_IN=http/dup`).
|
|
567
|
+
// Count the stable one, as the latency histograms do (latency.ts).
|
|
568
|
+
const hasStableDuration = resourceMetric.scopeMetrics?.some((scope) => scope.metrics?.some((metric) => metric.name === "http.server.request.duration"));
|
|
404
569
|
for (const scopeMetric of resourceMetric.scopeMetrics ?? []) {
|
|
405
570
|
for (const metric of scopeMetric.metrics ?? []) {
|
|
406
571
|
const multiplier = metric.name
|
|
@@ -408,6 +573,8 @@ export function normalizeMetrics(request) {
|
|
|
408
573
|
: undefined;
|
|
409
574
|
if (multiplier === undefined)
|
|
410
575
|
continue;
|
|
576
|
+
if (hasStableDuration && metric.name === "http.server.duration")
|
|
577
|
+
continue;
|
|
411
578
|
if (isCumulativeTemporality(metric.histogram?.aggregationTemporality)) {
|
|
412
579
|
continue;
|
|
413
580
|
}
|
package/dist/otlp-proto.d.ts
CHANGED
|
@@ -3,3 +3,17 @@ import type { OtlpLogsRequest } from "./logs.js";
|
|
|
3
3
|
export declare function decodeTraceRequest(body: Buffer): OtlpTraceRequest;
|
|
4
4
|
export declare function decodeMetricsRequest(body: Buffer): OtlpMetricsRequest;
|
|
5
5
|
export declare function decodeLogsRequest(body: Buffer): OtlpLogsRequest;
|
|
6
|
+
/**
|
|
7
|
+
* The protobuf body of an OTLP export response: empty when everything was
|
|
8
|
+
* accepted, else `partial_success` with the rejected count and why
|
|
9
|
+
* (identical bytes for traces, logs and metrics).
|
|
10
|
+
*/
|
|
11
|
+
export declare function encodeExportResponse(partial: {
|
|
12
|
+
rejected: number;
|
|
13
|
+
errorMessage: string;
|
|
14
|
+
} | null): Uint8Array;
|
|
15
|
+
/** Decode an OTLP export response body (tests, and anything reading our answers). */
|
|
16
|
+
export declare function decodeExportResponse(body: Uint8Array): {
|
|
17
|
+
rejected: number;
|
|
18
|
+
errorMessage: string;
|
|
19
|
+
} | null;
|
package/dist/otlp-proto.js
CHANGED
|
@@ -93,11 +93,19 @@ message HistogramDataPoint {
|
|
|
93
93
|
repeated double explicit_bounds = 7;
|
|
94
94
|
repeated KeyValue attributes = 9;
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
// ExportTraceServiceResponse, ExportLogsServiceResponse and
|
|
98
|
+
// ExportMetricsServiceResponse share this wire shape: partial_success = 1,
|
|
99
|
+
// and in it the rejected count (rejected_spans / rejected_log_records /
|
|
100
|
+
// rejected_data_points) = 1 and error_message = 2.
|
|
101
|
+
message ExportServiceResponse { ExportPartialSuccess partial_success = 1; }
|
|
102
|
+
message ExportPartialSuccess { int64 rejected = 1; string error_message = 2; }
|
|
96
103
|
`;
|
|
97
104
|
const root = protobuf.parse(PROTO).root;
|
|
98
105
|
const TraceRequest = root.lookupType("otlp.ExportTraceServiceRequest");
|
|
99
106
|
const MetricsRequest = root.lookupType("otlp.ExportMetricsServiceRequest");
|
|
100
107
|
const LogsRequest = root.lookupType("otlp.ExportLogsServiceRequest");
|
|
108
|
+
const ServiceResponse = root.lookupType("otlp.ExportServiceResponse");
|
|
101
109
|
const TO_OBJECT_OPTIONS = {
|
|
102
110
|
longs: String, // 64-bit ints → strings (matches OTLP/JSON)
|
|
103
111
|
defaults: false,
|
|
@@ -131,3 +139,20 @@ export function decodeMetricsRequest(body) {
|
|
|
131
139
|
export function decodeLogsRequest(body) {
|
|
132
140
|
return hexifyIds(LogsRequest.toObject(LogsRequest.decode(body), TO_OBJECT_OPTIONS));
|
|
133
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The protobuf body of an OTLP export response: empty when everything was
|
|
144
|
+
* accepted, else `partial_success` with the rejected count and why
|
|
145
|
+
* (identical bytes for traces, logs and metrics).
|
|
146
|
+
*/
|
|
147
|
+
export function encodeExportResponse(partial) {
|
|
148
|
+
if (!partial)
|
|
149
|
+
return new Uint8Array(0);
|
|
150
|
+
return ServiceResponse.encode(ServiceResponse.create({ partialSuccess: { rejected: partial.rejected, errorMessage: partial.errorMessage } })).finish();
|
|
151
|
+
}
|
|
152
|
+
/** Decode an OTLP export response body (tests, and anything reading our answers). */
|
|
153
|
+
export function decodeExportResponse(body) {
|
|
154
|
+
const message = ServiceResponse.toObject(ServiceResponse.decode(body), { longs: Number, defaults: false });
|
|
155
|
+
if (!message.partialSuccess)
|
|
156
|
+
return null;
|
|
157
|
+
return { rejected: message.partialSuccess.rejected ?? 0, errorMessage: message.partialSuccess.errorMessage ?? "" };
|
|
158
|
+
}
|
package/dist/profiles.d.ts
CHANGED
|
@@ -13,10 +13,14 @@ export interface ProfileSample {
|
|
|
13
13
|
value: number;
|
|
14
14
|
}
|
|
15
15
|
export declare function profileTableDDL(db: string): string;
|
|
16
|
+
/** Samples kept per profile; the rest are dropped and counted (profile_samples_over_limit). */
|
|
17
|
+
export declare const MAX_PROFILE_SAMPLES = 1000;
|
|
16
18
|
export declare function decodeProfile(body: Buffer, meta: {
|
|
17
19
|
service: string;
|
|
18
20
|
environment: string;
|
|
19
21
|
release: string;
|
|
20
22
|
traceId: string;
|
|
21
23
|
instanceId: string;
|
|
24
|
+
}, stats?: {
|
|
25
|
+
overLimit: number;
|
|
22
26
|
}): ProfileSample[];
|
package/dist/profiles.js
CHANGED
|
@@ -28,7 +28,9 @@ export function profileTableDDL(db) {
|
|
|
28
28
|
ORDER BY (org_id, repository_id, service, environment, release, observed_at, profile_id)
|
|
29
29
|
TTL toDateTime(observed_at) + INTERVAL 7 DAY`;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
/** Samples kept per profile; the rest are dropped and counted (profile_samples_over_limit). */
|
|
32
|
+
export const MAX_PROFILE_SAMPLES = 1000;
|
|
33
|
+
export function decodeProfile(body, meta, stats) {
|
|
32
34
|
if (body.length === 0 || body.length > 1024 * 1024)
|
|
33
35
|
throw new Error("invalid profile size");
|
|
34
36
|
const payload = body[0] === 0x1f && body[1] === 0x8b
|
|
@@ -43,7 +45,9 @@ export function decodeProfile(body, meta) {
|
|
|
43
45
|
const observedAt = observedMs > 0 && observedMs <= Date.now() + 300_000 ? new Date(observedMs) : new Date();
|
|
44
46
|
const profileId = createHash("sha256").update(body).digest("hex").slice(0, 32);
|
|
45
47
|
const samples = [];
|
|
46
|
-
|
|
48
|
+
if (stats)
|
|
49
|
+
stats.overLimit += Math.max(0, (profile.sample ?? []).length - MAX_PROFILE_SAMPLES);
|
|
50
|
+
for (const [sampleIndex, sample] of (profile.sample ?? []).slice(0, MAX_PROFILE_SAMPLES).entries()) {
|
|
47
51
|
const stack = (sample.locationId ?? []).slice(0, 64).flatMap((id) => {
|
|
48
52
|
const locLine = locations.get(id)?.line?.[0];
|
|
49
53
|
const fn = locLine?.functionId ? functions.get(locLine.functionId) : undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which request each span of an export batch belongs to, so the span-fed
|
|
3
|
+
* usage rollups (`runtime_metrics_1m`) count one request once.
|
|
4
|
+
*
|
|
5
|
+
* A framework that opens its own SERVER span under the HTTP
|
|
6
|
+
* instrumentation's gives one request two server spans: Next.js's
|
|
7
|
+
* `BaseServer.handleRequest` span is a child of the HTTP server span, and
|
|
8
|
+
* some NestJS and Fastify setups and gRPC gateways serving RPCs in-process do
|
|
9
|
+
* the same. Each server span used to add a request, its latency and its error
|
|
10
|
+
* to the rollups, so every traced request of such an app counted twice.
|
|
11
|
+
*
|
|
12
|
+
* The request is the OUTERMOST server span. Walking up from a span through
|
|
13
|
+
* the parent links present in the batch, its request is the highest SERVER
|
|
14
|
+
* span reached. The walk stops:
|
|
15
|
+
* - at a parent that is not in the batch: the span's own chain ends there,
|
|
16
|
+
* so a server span whose parent is missing is a request of its own, as
|
|
17
|
+
* before. When an exporter's timer splits one request's spans across two
|
|
18
|
+
* batches, the nested server span therefore counts in its batch and the
|
|
19
|
+
* outer one in the other; a batch cannot tell a parent exported elsewhere
|
|
20
|
+
* from a remote caller;
|
|
21
|
+
* - at a parent in another process (service, environment, release and
|
|
22
|
+
* `service.instance.id`): one process serves one request;
|
|
23
|
+
* - where a SERVER or CONSUMER span's parent is a CLIENT or PRODUCER span:
|
|
24
|
+
* that edge is a call over the wire, a request of its own even when a
|
|
25
|
+
* service calls itself.
|
|
26
|
+
*/
|
|
27
|
+
export interface RequestSpanLink {
|
|
28
|
+
traceId: string;
|
|
29
|
+
spanId: string;
|
|
30
|
+
parentSpanId: string | null;
|
|
31
|
+
/** Lower-case OTel span kind: "server", "client", "internal", … */
|
|
32
|
+
kind: string;
|
|
33
|
+
/** The process the span was recorded in; spans of different processes
|
|
34
|
+
* are never one request. */
|
|
35
|
+
process: string;
|
|
36
|
+
}
|
|
37
|
+
export interface RequestMembership {
|
|
38
|
+
/** Per span: the batch index of its request's (outermost) server span,
|
|
39
|
+
* or -1 when no server span is on its chain. */
|
|
40
|
+
requestOf: number[];
|
|
41
|
+
/** Per span: how many parent links the walk followed above it (0 at the
|
|
42
|
+
* top of its chain). Nested spans are deeper than their request span. */
|
|
43
|
+
depth: number[];
|
|
44
|
+
}
|
|
45
|
+
export declare function requestMembership(spans: RequestSpanLink[]): RequestMembership;
|