@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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { IngestContext } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* What the ingester refused or dropped, per tenant, per minute, by route and
|
|
4
|
+
* reason (EI-27): `runtime_ingest_drops` in ClickHouse, so the Autter
|
|
5
|
+
* dashboard can say why a repository's telemetry is missing instead of
|
|
6
|
+
* showing nothing.
|
|
7
|
+
*
|
|
8
|
+
* Four kinds of loss:
|
|
9
|
+
* - `rejected`: the whole request got a non-2xx answer and nothing from it
|
|
10
|
+
* was stored (bad key, wrong key type, rate limit, invalid or oversized
|
|
11
|
+
* payload, unsupported media type, storage down). 429 and 503 are retried
|
|
12
|
+
* by OTLP exporters, so those records usually arrive later; the others
|
|
13
|
+
* are not retried.
|
|
14
|
+
* - `dropped`: the request was accepted but some of its records were not
|
|
15
|
+
* stored (spans past the per-request limit, cumulative HTTP histograms,
|
|
16
|
+
* invalid browser events, …). OTLP answers carry them in `partialSuccess`.
|
|
17
|
+
* - `truncated`: records were stored with some attribute values cut or left
|
|
18
|
+
* out by the attribute bounds; `records` counts the values cut.
|
|
19
|
+
* - `unforwarded`: stored in ClickHouse, but the sink webhook gave up on
|
|
20
|
+
* the batch; the consumer recovers it by replaying ClickHouse.
|
|
21
|
+
*
|
|
22
|
+
* Counting never slows or fails ingest: `record()` is a synchronous map
|
|
23
|
+
* update, rows are written in batches every few seconds, a failed write is
|
|
24
|
+
* logged and dropped (never retried), and the buffer is bounded.
|
|
25
|
+
*/
|
|
26
|
+
export type DropKind = "rejected" | "dropped" | "truncated" | "unforwarded";
|
|
27
|
+
/** Every reason, its kind, and (for rejections) the HTTP status it answers with. */
|
|
28
|
+
export declare const DROP_REASONS: {
|
|
29
|
+
/** No key in `Authorization`, `x-autter-key` or `?key=`. */
|
|
30
|
+
readonly missing_key: {
|
|
31
|
+
readonly kind: "rejected";
|
|
32
|
+
readonly status: 401;
|
|
33
|
+
};
|
|
34
|
+
/** Not a configured key, and the validator did not accept it (revoked, mistyped, another repository's deployment). */
|
|
35
|
+
readonly unknown_key: {
|
|
36
|
+
readonly kind: "rejected";
|
|
37
|
+
readonly status: 401;
|
|
38
|
+
};
|
|
39
|
+
/** The key validator could not answer (unreachable, timeout, 5xx) and nothing was cached. */
|
|
40
|
+
readonly key_unverified: {
|
|
41
|
+
readonly kind: "rejected";
|
|
42
|
+
readonly status: 401;
|
|
43
|
+
};
|
|
44
|
+
/** An upload key (`autter_rtu_…`) on a telemetry route. */
|
|
45
|
+
readonly upload_key_not_allowed: {
|
|
46
|
+
readonly kind: "rejected";
|
|
47
|
+
readonly status: 403;
|
|
48
|
+
};
|
|
49
|
+
/** A client key (`autter_rtc_…`) on an OTLP or source map route. */
|
|
50
|
+
readonly client_key_not_allowed: {
|
|
51
|
+
readonly kind: "rejected";
|
|
52
|
+
readonly status: 403;
|
|
53
|
+
};
|
|
54
|
+
/** A client key sent from an origin missing from its allow-list. */
|
|
55
|
+
readonly origin_not_allowed: {
|
|
56
|
+
readonly kind: "rejected";
|
|
57
|
+
readonly status: 403;
|
|
58
|
+
};
|
|
59
|
+
/** A key whose scope this ingester does not know (never yielded by KeyResolver). */
|
|
60
|
+
readonly key_scope_not_allowed: {
|
|
61
|
+
readonly kind: "rejected";
|
|
62
|
+
readonly status: 403;
|
|
63
|
+
};
|
|
64
|
+
readonly rate_limited: {
|
|
65
|
+
readonly kind: "rejected";
|
|
66
|
+
readonly status: 429;
|
|
67
|
+
};
|
|
68
|
+
/** Malformed JSON or protobuf, a schema failure, or values the normaliser can't read. */
|
|
69
|
+
readonly invalid_payload: {
|
|
70
|
+
readonly kind: "rejected";
|
|
71
|
+
readonly status: 400;
|
|
72
|
+
};
|
|
73
|
+
/** The client went away before its body arrived. */
|
|
74
|
+
readonly request_aborted: {
|
|
75
|
+
readonly kind: "rejected";
|
|
76
|
+
readonly status: 400;
|
|
77
|
+
};
|
|
78
|
+
/** Body over MAX_BODY_BYTES (after gzip is inflated), or a source map over 32 MiB. */
|
|
79
|
+
readonly payload_too_large: {
|
|
80
|
+
readonly kind: "rejected";
|
|
81
|
+
readonly status: 413;
|
|
82
|
+
};
|
|
83
|
+
/** A content type or content encoding (zstd, br) the route does not take. */
|
|
84
|
+
readonly unsupported_media_type: {
|
|
85
|
+
readonly kind: "rejected";
|
|
86
|
+
readonly status: 415;
|
|
87
|
+
};
|
|
88
|
+
/** POST to a path the ingester does not serve (route "other"). */
|
|
89
|
+
readonly unknown_route: {
|
|
90
|
+
readonly kind: "rejected";
|
|
91
|
+
readonly status: 404;
|
|
92
|
+
};
|
|
93
|
+
readonly internal_error: {
|
|
94
|
+
readonly kind: "rejected";
|
|
95
|
+
readonly status: 500;
|
|
96
|
+
};
|
|
97
|
+
/** A ClickHouse write failed; exporters retry. */
|
|
98
|
+
readonly storage_unavailable: {
|
|
99
|
+
readonly kind: "rejected";
|
|
100
|
+
readonly status: 503;
|
|
101
|
+
};
|
|
102
|
+
/** Spans past the per-request limit (5000). */
|
|
103
|
+
readonly spans_over_limit: {
|
|
104
|
+
readonly kind: "dropped";
|
|
105
|
+
};
|
|
106
|
+
/** Log records past the per-request limit (2000). */
|
|
107
|
+
readonly log_records_over_limit: {
|
|
108
|
+
readonly kind: "dropped";
|
|
109
|
+
};
|
|
110
|
+
/** Data points of cumulative `http.server.duration` / `http.server.request.duration` histograms (only delta is summable). */
|
|
111
|
+
readonly cumulative_histogram: {
|
|
112
|
+
readonly kind: "dropped";
|
|
113
|
+
};
|
|
114
|
+
/** Memory/GC data points with a value, timestamp or unit out of range, or without service.name / service.instance.id. */
|
|
115
|
+
readonly memory_point_invalid: {
|
|
116
|
+
readonly kind: "dropped";
|
|
117
|
+
};
|
|
118
|
+
/** Memory/GC data points past 5000 in one request. */
|
|
119
|
+
readonly memory_points_over_limit: {
|
|
120
|
+
readonly kind: "dropped";
|
|
121
|
+
};
|
|
122
|
+
/** Browser events that failed validation (unknown type, bad timestamp, oversized field). */
|
|
123
|
+
readonly invalid_event: {
|
|
124
|
+
readonly kind: "dropped";
|
|
125
|
+
};
|
|
126
|
+
/** Browser events past 50 in one payload. */
|
|
127
|
+
readonly events_over_limit: {
|
|
128
|
+
readonly kind: "dropped";
|
|
129
|
+
};
|
|
130
|
+
/** Profile samples past the first 1000. */
|
|
131
|
+
readonly profile_samples_over_limit: {
|
|
132
|
+
readonly kind: "dropped";
|
|
133
|
+
};
|
|
134
|
+
readonly attributes_truncated: {
|
|
135
|
+
readonly kind: "truncated";
|
|
136
|
+
};
|
|
137
|
+
/** The sink retry buffer overflowed, or one batch was bigger than all of it. */
|
|
138
|
+
readonly sink_overflow: {
|
|
139
|
+
readonly kind: "unforwarded";
|
|
140
|
+
};
|
|
141
|
+
/** The sink consumer answered a permanent 4xx. */
|
|
142
|
+
readonly sink_rejected: {
|
|
143
|
+
readonly kind: "unforwarded";
|
|
144
|
+
};
|
|
145
|
+
/** SINK_MAX_ATTEMPTS deliveries failed. */
|
|
146
|
+
readonly sink_gave_up: {
|
|
147
|
+
readonly kind: "unforwarded";
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
export type DropReason = keyof typeof DROP_REASONS;
|
|
151
|
+
/** Routes counted by name; anything else is "other". `sink` is the webhook. */
|
|
152
|
+
export declare const DROP_ROUTES: readonly ["/v1/traces", "/v1/logs", "/v1/metrics", "/v1/browser", "/v1/profiles", "/v1/platform-events", "/v1/sourcemaps", "/v1/sourcemaps/check"];
|
|
153
|
+
export type DropRoute = (typeof DROP_ROUTES)[number] | "other" | "sink";
|
|
154
|
+
/** The route column for a request path (Express matches case-insensitively and ignores a trailing slash). */
|
|
155
|
+
export declare function dropRoute(path: string): DropRoute;
|
|
156
|
+
/** The HTTP status a reason is recorded with. `sink` has none (0). */
|
|
157
|
+
export declare function dropStatus(reason: DropReason, route: DropRoute): number;
|
|
158
|
+
/**
|
|
159
|
+
* sha256 (hex) of a key that did not resolve, when it has an Autter key
|
|
160
|
+
* prefix; "" otherwise. Autter stores the same hash for every key it issues
|
|
161
|
+
* (revoked ones included), so the platform can tell a repository that a
|
|
162
|
+
* revoked or rotated key is still sending, while the ingester never learns
|
|
163
|
+
* whose key it was. Anything without the prefix (another vendor's token, a
|
|
164
|
+
* password in the wrong field) is never hashed or stored.
|
|
165
|
+
*/
|
|
166
|
+
export declare function autterKeyHash(key: string): string;
|
|
167
|
+
/**
|
|
168
|
+
* Count the length-delimited fields at a nested path of field numbers
|
|
169
|
+
* (one set per level) without decoding the message: tags are read and
|
|
170
|
+
* everything else is skipped by length. O(body size); null when the bytes
|
|
171
|
+
* are not a well-formed message.
|
|
172
|
+
*/
|
|
173
|
+
export declare function countProtoPath(buf: Uint8Array, path: readonly (readonly number[])[]): number | null;
|
|
174
|
+
/**
|
|
175
|
+
* Records (spans, log records, data points, browser events) in a request
|
|
176
|
+
* body that was refused, where they can be known: 0 for a body never read
|
|
177
|
+
* (413, a bad encoding) or not understood. Profiles count samples, which a
|
|
178
|
+
* refused profile never had decoded, so 0; a source map upload or platform
|
|
179
|
+
* event is one record; a source map check carries none.
|
|
180
|
+
*/
|
|
181
|
+
export declare function recordsInBody(route: DropRoute, body: unknown, protobuf: boolean): number;
|
|
182
|
+
/** Spans in an OTLP traces request (JSON-shaped, as decoded). */
|
|
183
|
+
export declare function spansInRequest(request: unknown): number;
|
|
184
|
+
/** Data points in an OTLP metrics request (JSON-shaped, as decoded). */
|
|
185
|
+
export declare function dataPointsInRequest(request: unknown): number;
|
|
186
|
+
/**
|
|
187
|
+
* Data points of cumulative HTTP server duration histograms: the request
|
|
188
|
+
* rollups (normalizeMetrics) and the latency table (normalizeLatencyHistograms)
|
|
189
|
+
* both skip them, since cumulative points repeat lifetime totals on every
|
|
190
|
+
* export. Mirrors normalizeMetrics' check; keep the two in step.
|
|
191
|
+
*/
|
|
192
|
+
export declare function cumulativeHttpHistogramPoints(request: unknown): number;
|
|
193
|
+
/** What an accepted OTLP request did not store, for its `partial_success`. */
|
|
194
|
+
export interface PartialSuccess {
|
|
195
|
+
/** rejected_spans / rejected_log_records / rejected_data_points. */
|
|
196
|
+
rejected: number;
|
|
197
|
+
errorMessage: string;
|
|
198
|
+
}
|
|
199
|
+
/** Merge per-reason rejections into one partial success; null when nothing was rejected. */
|
|
200
|
+
export declare function partialSuccess(parts: Array<{
|
|
201
|
+
rejected: number;
|
|
202
|
+
message: string;
|
|
203
|
+
}>): PartialSuccess | null;
|
|
204
|
+
export declare const PARTIAL_MESSAGES: {
|
|
205
|
+
readonly spans: (rejected: number, limit: number) => string;
|
|
206
|
+
readonly logRecords: (rejected: number, limit: number) => string;
|
|
207
|
+
readonly cumulative: (rejected: number) => string;
|
|
208
|
+
readonly memoryInvalid: (rejected: number) => string;
|
|
209
|
+
readonly memoryOverLimit: (rejected: number, limit: number) => string;
|
|
210
|
+
};
|
|
211
|
+
/** A drop as recorded by the request path. */
|
|
212
|
+
export interface IngestDrop {
|
|
213
|
+
/** Who sent it; null when the key did not resolve (or there was none). */
|
|
214
|
+
tenant: Pick<IngestContext, "orgId" | "repositoryId"> | null;
|
|
215
|
+
route: DropRoute;
|
|
216
|
+
reason: DropReason;
|
|
217
|
+
/** Requests (or sink batches); default 1. */
|
|
218
|
+
requests?: number;
|
|
219
|
+
/** Records lost (values cut, for `attributes_truncated`); default 0. */
|
|
220
|
+
records?: number;
|
|
221
|
+
/** The response status, when it differs from the reason's own. */
|
|
222
|
+
status?: number;
|
|
223
|
+
/** Null tenant only: autterKeyHash() of the key that did not resolve. */
|
|
224
|
+
keyHash?: string;
|
|
225
|
+
}
|
|
226
|
+
/** One `runtime_ingest_drops` row: a tenant, minute, route and reason. */
|
|
227
|
+
export interface IngestDropRow {
|
|
228
|
+
orgId: string;
|
|
229
|
+
repositoryId: string;
|
|
230
|
+
minute: Date;
|
|
231
|
+
route: DropRoute;
|
|
232
|
+
reason: DropReason;
|
|
233
|
+
kind: DropKind;
|
|
234
|
+
status: number;
|
|
235
|
+
keyHash: string;
|
|
236
|
+
requests: number;
|
|
237
|
+
records: number;
|
|
238
|
+
}
|
|
239
|
+
export interface IngestDropCounterOptions {
|
|
240
|
+
/** How often buffered rows are written. Default 5 s. */
|
|
241
|
+
flushIntervalMs?: number;
|
|
242
|
+
/** Distinct rows buffered between writes; past it, new rows are not counted (and logged). Default 10 000. */
|
|
243
|
+
maxRows?: number;
|
|
244
|
+
/** Distinct key hashes buffered between writes; past it, a new one is counted with no hash. Default 1000. */
|
|
245
|
+
maxKeyHashes?: number;
|
|
246
|
+
/** A write that has not settled after this long counts as failed, so counting goes on. Default 30 s. */
|
|
247
|
+
writeTimeoutMs?: number;
|
|
248
|
+
/** False when there is no ClickHouse: nothing is buffered. */
|
|
249
|
+
enabled?: boolean;
|
|
250
|
+
now?: () => number;
|
|
251
|
+
log?: (message: string) => void;
|
|
252
|
+
}
|
|
253
|
+
export interface IngestDropCounterStats {
|
|
254
|
+
/** Rows waiting for the next write. */
|
|
255
|
+
buffered: number;
|
|
256
|
+
/** Rows written since start. */
|
|
257
|
+
written: number;
|
|
258
|
+
/** Writes that failed (their rows were dropped). */
|
|
259
|
+
failedWrites: number;
|
|
260
|
+
/** Rows dropped with a failed write. */
|
|
261
|
+
discardedRows: number;
|
|
262
|
+
/** Requests not counted because the buffer was full. */
|
|
263
|
+
overflowRequests: number;
|
|
264
|
+
}
|
|
265
|
+
export declare class IngestDropCounter {
|
|
266
|
+
private readonly write;
|
|
267
|
+
private rows;
|
|
268
|
+
private keyHashes;
|
|
269
|
+
private overflowRequests;
|
|
270
|
+
private overflowSinceLog;
|
|
271
|
+
private inFlight;
|
|
272
|
+
private timer;
|
|
273
|
+
private written;
|
|
274
|
+
private failedWrites;
|
|
275
|
+
private discardedRows;
|
|
276
|
+
private lastErrorLog;
|
|
277
|
+
private readonly flushIntervalMs;
|
|
278
|
+
private readonly maxRows;
|
|
279
|
+
private readonly maxKeyHashes;
|
|
280
|
+
private readonly writeTimeoutMs;
|
|
281
|
+
private readonly enabled;
|
|
282
|
+
private readonly now;
|
|
283
|
+
private readonly log;
|
|
284
|
+
constructor(write: (rows: IngestDropRow[]) => Promise<void>, options?: IngestDropCounterOptions);
|
|
285
|
+
/** Count a drop. Synchronous and O(1); never throws. */
|
|
286
|
+
record(drop: IngestDrop): void;
|
|
287
|
+
/** Write buffered rows every flushIntervalMs (unref'd: never keeps the process alive). */
|
|
288
|
+
start(): void;
|
|
289
|
+
/** Stop the timer and write what is buffered (shutdown). */
|
|
290
|
+
stop(): Promise<void>;
|
|
291
|
+
/**
|
|
292
|
+
* Write the buffered rows once. One write at a time: while one is in
|
|
293
|
+
* flight, rows keep accumulating (bounded) for the next. A failed or
|
|
294
|
+
* stalled write is logged and its rows dropped, never retried. Never
|
|
295
|
+
* rejects.
|
|
296
|
+
*/
|
|
297
|
+
flush(): Promise<void>;
|
|
298
|
+
stats(): IngestDropCounterStats;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Migration 0018 / baseline. A SummingMergeTree: readers `sum(requests)`,
|
|
302
|
+
* `sum(records)` and GROUP BY, since rows of one key collapse at merge time,
|
|
303
|
+
* eventually. `org_id = '' AND repository_id = ''` is the null tenant: keys
|
|
304
|
+
* that did not resolve (401), with `key_hash` set for Autter-format keys.
|
|
305
|
+
* `status` is the HTTP status answered (2xx for drops inside accepted
|
|
306
|
+
* requests, 0 for the sink).
|
|
307
|
+
*/
|
|
308
|
+
export declare function ingestDropsTableDDL(db: string): string;
|