@autter/otlp-ingester 1.7.0 → 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 +17 -3
- package/dist/auth.d.ts +15 -0
- package/dist/auth.js +19 -7
- package/dist/clickhouse.d.ts +3 -0
- package/dist/clickhouse.js +38 -1
- package/dist/compat-manifest.json +21 -0
- 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 +39 -11
- package/dist/normalize-browser.js +56 -10
- package/dist/normalize-otlp.js +165 -45
- 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 +3 -0
- package/dist/server.js +304 -98
- package/dist/sink.d.ts +14 -0
- package/dist/sink.js +15 -0
- 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/package.json +1 -1
|
@@ -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;
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
const UNSET = -2;
|
|
28
|
+
function isRemoteCallEdge(child, parent) {
|
|
29
|
+
return (child === "server" || child === "consumer") && (parent === "client" || parent === "producer");
|
|
30
|
+
}
|
|
31
|
+
export function requestMembership(spans) {
|
|
32
|
+
const byKey = new Map();
|
|
33
|
+
spans.forEach((span, i) => {
|
|
34
|
+
if (!span.traceId || !span.spanId)
|
|
35
|
+
return;
|
|
36
|
+
const key = `${span.traceId}\u0000${span.spanId}`;
|
|
37
|
+
if (!byKey.has(key))
|
|
38
|
+
byKey.set(key, i);
|
|
39
|
+
});
|
|
40
|
+
/** The parent the walk may step to, or -1. */
|
|
41
|
+
const parentOf = (i) => {
|
|
42
|
+
const span = spans[i];
|
|
43
|
+
if (!span.traceId || !span.parentSpanId)
|
|
44
|
+
return -1;
|
|
45
|
+
const p = byKey.get(`${span.traceId}\u0000${span.parentSpanId}`);
|
|
46
|
+
if (p === undefined || p === i)
|
|
47
|
+
return -1;
|
|
48
|
+
const parent = spans[p];
|
|
49
|
+
if (parent.process !== span.process)
|
|
50
|
+
return -1;
|
|
51
|
+
if (isRemoteCallEdge(span.kind, parent.kind))
|
|
52
|
+
return -1;
|
|
53
|
+
return p;
|
|
54
|
+
};
|
|
55
|
+
const requestOf = new Array(spans.length).fill(UNSET);
|
|
56
|
+
const depth = new Array(spans.length).fill(0);
|
|
57
|
+
for (let start = 0; start < spans.length; start++) {
|
|
58
|
+
if (requestOf[start] !== UNSET)
|
|
59
|
+
continue;
|
|
60
|
+
// Walk up until a resolved span, the top of the chain, or a cycle
|
|
61
|
+
// (malformed parent links).
|
|
62
|
+
const path = [];
|
|
63
|
+
const onPath = new Set();
|
|
64
|
+
let above = -1;
|
|
65
|
+
let aboveDepth = -1;
|
|
66
|
+
let cur = start;
|
|
67
|
+
for (;;) {
|
|
68
|
+
path.push(cur);
|
|
69
|
+
onPath.add(cur);
|
|
70
|
+
const p = parentOf(cur);
|
|
71
|
+
if (p < 0 || onPath.has(p))
|
|
72
|
+
break;
|
|
73
|
+
if (requestOf[p] !== UNSET) {
|
|
74
|
+
above = requestOf[p];
|
|
75
|
+
aboveDepth = depth[p];
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
cur = p;
|
|
79
|
+
}
|
|
80
|
+
// Resolve top-down: the outermost server span wins.
|
|
81
|
+
for (let k = path.length - 1; k >= 0; k--) {
|
|
82
|
+
const node = path[k];
|
|
83
|
+
if (above === -1 && spans[node].kind === "server")
|
|
84
|
+
above = node;
|
|
85
|
+
aboveDepth += 1;
|
|
86
|
+
requestOf[node] = above;
|
|
87
|
+
depth[node] = aboveDepth;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { requestOf, depth };
|
|
91
|
+
}
|
package/dist/sdk-versions.d.ts
CHANGED
|
@@ -8,7 +8,31 @@ export interface SdkSighting {
|
|
|
8
8
|
sdkName: string;
|
|
9
9
|
sdkVersion: string;
|
|
10
10
|
sdkLanguage: string;
|
|
11
|
+
/** How it arrived (telemetryVia); '' when unknown. */
|
|
12
|
+
via: TelemetryVia;
|
|
11
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* How telemetry reached the ingester:
|
|
16
|
+
* - `collector`: forwarded by an OpenTelemetry Collector or a similar agent —
|
|
17
|
+
* its User-Agent says so (any collector distribution: "OpenTelemetry
|
|
18
|
+
* Collector Contrib/0.120.0 (linux/amd64)", ADOT's "AWS OTel Collector",
|
|
19
|
+
* `otelcol-*` builds, Grafana Alloy / Agent, Fluent Bit, Vector), or the
|
|
20
|
+
* resource's `telemetry.distro.name` is an otelcol build (the Autter
|
|
21
|
+
* collector config inserts `otelcol-contrib` for senders that set none);
|
|
22
|
+
* - `direct`: an SDK exporter sent it itself (the `OTel-OTLP-Exporter-<lang>`
|
|
23
|
+
* or Go `OTel Go OTLP …` User-Agent, or an Autter SDK's distro without a
|
|
24
|
+
* collector User-Agent); browser events sent with a client key;
|
|
25
|
+
* - `relay`: browser events forwarded with a server key (the Autter relay);
|
|
26
|
+
* - '': nothing tells (another User-Agent, or none).
|
|
27
|
+
*/
|
|
28
|
+
export type TelemetryVia = "collector" | "direct" | "relay" | "";
|
|
29
|
+
export declare function telemetryVia(input: {
|
|
30
|
+
userAgent?: string | null;
|
|
31
|
+
/** The resource's `telemetry.distro.name`, when it has one. */
|
|
32
|
+
distroName?: string | null;
|
|
33
|
+
/** The browser route: decided by the key (client key = the browser itself). */
|
|
34
|
+
browserKeyScope?: "server" | "client" | "upload" | null;
|
|
35
|
+
}): TelemetryVia;
|
|
12
36
|
interface KeyValue {
|
|
13
37
|
key?: string;
|
|
14
38
|
value?: {
|
|
@@ -25,8 +49,15 @@ export interface OtlpResourceRequest {
|
|
|
25
49
|
resourceMetrics?: ResourceHolder[];
|
|
26
50
|
resourceLogs?: ResourceHolder[];
|
|
27
51
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
52
|
+
/**
|
|
53
|
+
* SDK identity per OTLP resource. Reads only a handful of resource keys.
|
|
54
|
+
* A resource with no SDK identity that came through a collector is still
|
|
55
|
+
* a sighting (empty name and version, `via: "collector"`), so a service fed
|
|
56
|
+
* by a collector receiver with no SDK (Prometheus, file logs) is seen too.
|
|
57
|
+
*/
|
|
58
|
+
export declare function sdkSightingsFromOtlp(request: OtlpResourceRequest | null | undefined, transport?: {
|
|
59
|
+
userAgent?: string | null;
|
|
60
|
+
}): SdkSighting[];
|
|
30
61
|
export declare const BROWSER_SDK_NAME = "@autter/runtime-browser";
|
|
31
62
|
export interface SdkVersionRow extends SdkSighting {
|
|
32
63
|
ingesterVersion: string;
|
|
@@ -52,6 +83,12 @@ export declare class SdkVersionTracker {
|
|
|
52
83
|
/** Fire-and-forget. Returns the write promise (resolved) for tests. */
|
|
53
84
|
observe(ctx: IngestContext, sightings: SdkSighting[]): Promise<void>;
|
|
54
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Migration 0019 column — shared with the baseline DDL. Not in the sorting
|
|
88
|
+
* key (that can't change on a deployed table): a service+SDK+version row
|
|
89
|
+
* says how it arrived most recently.
|
|
90
|
+
*/
|
|
91
|
+
export declare const SDK_VERSION_VIA_COLUMN = "via LowCardinality(String) DEFAULT ''";
|
|
55
92
|
export declare function sdkVersionTableDDL(db: string, ttlDays?: number): string;
|
|
56
93
|
/** One row per ingester version (latest report wins); not tenant data. */
|
|
57
94
|
export declare function ingesterInfoTableDDL(db: string): string;
|
package/dist/sdk-versions.js
CHANGED
|
@@ -12,9 +12,16 @@ import { createRequire } from "node:module";
|
|
|
12
12
|
* `opentelemetry-<language>`;
|
|
13
13
|
* - browser payloads: the `sdk` field sent by `@autter/runtime-browser`.
|
|
14
14
|
*
|
|
15
|
+
* And how the telemetry arrived (`via`, 1.8.0): `collector` when an
|
|
16
|
+
* OpenTelemetry Collector (or a similar agent) forwarded it, `direct` when
|
|
17
|
+
* an SDK exporter sent it straight to the ingester, `relay` for browser
|
|
18
|
+
* payloads forwarded by a server-side relay; '' when it can't be told. So
|
|
19
|
+
* an Autter SDK behind a collector keeps its own name and is still seen as
|
|
20
|
+
* collector-fed. See `telemetryVia`.
|
|
21
|
+
*
|
|
15
22
|
* Writes are deduplicated in memory: one row per (tenant, service,
|
|
16
|
-
* environment, sdk, version) per `refreshMs`, bounded map,
|
|
17
|
-
* a failed write is logged and never fails ingest.
|
|
23
|
+
* environment, sdk, version, via) per `refreshMs`, bounded map,
|
|
24
|
+
* fire-and-forget — a failed write is logged and never fails ingest.
|
|
18
25
|
*/
|
|
19
26
|
let cachedVersion = null;
|
|
20
27
|
/** This ingester's own version (package.json — present in the npm package
|
|
@@ -31,6 +38,20 @@ export function ingesterVersion() {
|
|
|
31
38
|
}
|
|
32
39
|
return cachedVersion;
|
|
33
40
|
}
|
|
41
|
+
const COLLECTOR_AGENT_RE = /(?:open[ -]?telemetry|\botel)[ -]?collector|^otelcol|^(?:grafana[ -])?alloy\/|grafana[ -]?agent|^fluent[ -]?bit|^vector\//i;
|
|
42
|
+
const COLLECTOR_DISTRO_RE = /^otelcol(?:\b|[-_])|opentelemetry[-_ ]collector/i;
|
|
43
|
+
const SDK_EXPORTER_AGENT_RE = /^OTel[- ]OTLP[- ]Exporter|^OTel \w+ OTLP/i;
|
|
44
|
+
export function telemetryVia(input) {
|
|
45
|
+
const agent = (input.userAgent ?? "").trim().slice(0, 300);
|
|
46
|
+
const distro = (input.distroName ?? "").trim();
|
|
47
|
+
if (COLLECTOR_AGENT_RE.test(agent) || COLLECTOR_DISTRO_RE.test(distro))
|
|
48
|
+
return "collector";
|
|
49
|
+
if (input.browserKeyScope)
|
|
50
|
+
return input.browserKeyScope === "client" ? "direct" : "relay";
|
|
51
|
+
if (SDK_EXPORTER_AGENT_RE.test(agent) || distro.startsWith("@autter/"))
|
|
52
|
+
return "direct";
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
34
55
|
const CLEAN = /^[\w.@/+:-]{1,100}$/;
|
|
35
56
|
function clean(value, max = 100) {
|
|
36
57
|
if (typeof value !== "string")
|
|
@@ -38,8 +59,13 @@ function clean(value, max = 100) {
|
|
|
38
59
|
const trimmed = value.trim().slice(0, max);
|
|
39
60
|
return CLEAN.test(trimmed) ? trimmed : "";
|
|
40
61
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
62
|
+
/**
|
|
63
|
+
* SDK identity per OTLP resource. Reads only a handful of resource keys.
|
|
64
|
+
* A resource with no SDK identity that came through a collector is still
|
|
65
|
+
* a sighting (empty name and version, `via: "collector"`), so a service fed
|
|
66
|
+
* by a collector receiver with no SDK (Prometheus, file logs) is seen too.
|
|
67
|
+
*/
|
|
68
|
+
export function sdkSightingsFromOtlp(request, transport = {}) {
|
|
43
69
|
if (!request || typeof request !== "object")
|
|
44
70
|
return [];
|
|
45
71
|
const holders = [
|
|
@@ -63,14 +89,20 @@ export function sdkSightingsFromOtlp(request) {
|
|
|
63
89
|
if (!service)
|
|
64
90
|
continue;
|
|
65
91
|
const language = clean(get["telemetry.sdk.language"]);
|
|
66
|
-
|
|
92
|
+
const distroName = clean(get["telemetry.distro.name"]);
|
|
93
|
+
let sdkName = distroName;
|
|
67
94
|
let sdkVersion = clean(get["telemetry.distro.version"]);
|
|
68
95
|
if (!sdkName || !sdkVersion) {
|
|
69
96
|
sdkVersion = clean(get["telemetry.sdk.version"]);
|
|
70
97
|
sdkName = sdkVersion ? `opentelemetry-${language || "unknown"}` : "";
|
|
71
98
|
}
|
|
72
|
-
|
|
73
|
-
|
|
99
|
+
const via = telemetryVia({ userAgent: transport.userAgent, distroName });
|
|
100
|
+
if (!sdkName || !sdkVersion) {
|
|
101
|
+
if (via !== "collector")
|
|
102
|
+
continue;
|
|
103
|
+
sdkName = "";
|
|
104
|
+
sdkVersion = "";
|
|
105
|
+
}
|
|
74
106
|
out.push({
|
|
75
107
|
service,
|
|
76
108
|
environment: clean(get["deployment.environment.name"]) ||
|
|
@@ -79,6 +111,7 @@ export function sdkSightingsFromOtlp(request) {
|
|
|
79
111
|
sdkName,
|
|
80
112
|
sdkVersion,
|
|
81
113
|
sdkLanguage: language,
|
|
114
|
+
via,
|
|
82
115
|
});
|
|
83
116
|
}
|
|
84
117
|
return out;
|
|
@@ -123,6 +156,7 @@ export class SdkVersionTracker {
|
|
|
123
156
|
sighting.environment,
|
|
124
157
|
sighting.sdkName,
|
|
125
158
|
sighting.sdkVersion,
|
|
159
|
+
sighting.via,
|
|
126
160
|
].join("\u0000");
|
|
127
161
|
const last = this.lastWritten.get(key);
|
|
128
162
|
if (last !== undefined && now - last < this.refreshMs)
|
|
@@ -146,12 +180,19 @@ export class SdkVersionTracker {
|
|
|
146
180
|
});
|
|
147
181
|
}
|
|
148
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Migration 0019 column — shared with the baseline DDL. Not in the sorting
|
|
185
|
+
* key (that can't change on a deployed table): a service+SDK+version row
|
|
186
|
+
* says how it arrived most recently.
|
|
187
|
+
*/
|
|
188
|
+
export const SDK_VERSION_VIA_COLUMN = "via LowCardinality(String) DEFAULT ''";
|
|
149
189
|
export function sdkVersionTableDDL(db, ttlDays = 90) {
|
|
150
190
|
return `CREATE TABLE IF NOT EXISTS ${db}.runtime_sdk_versions (
|
|
151
191
|
org_id String, repository_id String, service LowCardinality(String),
|
|
152
192
|
environment LowCardinality(String), sdk_name LowCardinality(String),
|
|
153
193
|
sdk_version String, sdk_language LowCardinality(String) DEFAULT '',
|
|
154
|
-
ingester_version String DEFAULT '', last_seen DateTime('UTC')
|
|
194
|
+
ingester_version String DEFAULT '', last_seen DateTime('UTC'),
|
|
195
|
+
${SDK_VERSION_VIA_COLUMN}
|
|
155
196
|
) ENGINE = ReplacingMergeTree(last_seen)
|
|
156
197
|
ORDER BY (org_id, repository_id, service, environment, sdk_name, sdk_version)
|
|
157
198
|
TTL last_seen + INTERVAL ${ttlDays} DAY`;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Express } from "express";
|
|
2
|
+
import { IngestDropCounter } from "./ingest-drops.js";
|
|
2
3
|
import { ClickHouseStore } from "./clickhouse.js";
|
|
3
4
|
import type { IngesterConfig } from "./config.js";
|
|
4
5
|
import { SinkForwarder } from "./sink.js";
|
|
@@ -10,6 +11,8 @@ export interface IngesterApp {
|
|
|
10
11
|
/** Record this ingester's version + schema level in ClickHouse (for the
|
|
11
12
|
* Autter dashboard). Called after the boot schema warm-up; throttled. */
|
|
12
13
|
reportIngesterInfo: () => Promise<void>;
|
|
14
|
+
/** Rejection and drop counts per tenant (runtime_ingest_drops); `stop()` writes what is buffered. */
|
|
15
|
+
drops: IngestDropCounter;
|
|
13
16
|
}
|
|
14
17
|
/** The only routes an upload key (`autter_rtu_…`, scope "upload") may call. */
|
|
15
18
|
export declare const UPLOAD_KEY_ROUTES: readonly ["/v1/sourcemaps", "/v1/sourcemaps/check"];
|