@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
package/README.md
CHANGED
|
@@ -60,6 +60,19 @@ The validator webhook may return the same extra fields:
|
|
|
60
60
|
`{ orgId, repositoryId, scope?, allowedOrigins? }`, with `scope` one of
|
|
61
61
|
`server`, `client`, `upload`.
|
|
62
62
|
|
|
63
|
+
### Rejections and drops (1.8.0)
|
|
64
|
+
|
|
65
|
+
Everything the ingester refuses (401, 403, 404, 413, 415, 429, 400, 503) or
|
|
66
|
+
does not store from an accepted request (spans past 5,000, log records past
|
|
67
|
+
2,000, cumulative HTTP histograms, invalid browser events, …) is counted per
|
|
68
|
+
repository, minute, route and reason in `runtime_ingest_drops`; keys that
|
|
69
|
+
don't resolve count against an empty tenant with the sha256 of the key when
|
|
70
|
+
it is an Autter key. OTLP answers name dropped records in `partial_success`
|
|
71
|
+
(JSON and protobuf), so collectors log them. Counts are written every 5 s in
|
|
72
|
+
one batch, bounded in memory, and a failed write is logged and dropped:
|
|
73
|
+
counting never slows or fails ingest. Reasons and queries:
|
|
74
|
+
[docs/ARCHITECTURE.md § Rejections and drops](../../docs/ARCHITECTURE.md#rejections-and-drops).
|
|
75
|
+
|
|
63
76
|
### Version compatibility
|
|
64
77
|
|
|
65
78
|
`GET /v1/compat` (public, no tenant data) returns this ingester's version,
|
|
@@ -67,9 +80,10 @@ its ClickHouse schema state, and which features from
|
|
|
67
80
|
`src/compat-manifest.json` it supports. Add
|
|
68
81
|
`?features=operation_logging&sdk=@autter/runtime-node@1.3.0` to get the evaluated
|
|
69
82
|
incompatibilities. Every response carries `x-autter-ingester-version`. The
|
|
70
|
-
ingester records the SDK name and version each service sends with
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
ingester records the SDK name and version each service sends with, and
|
|
84
|
+
whether it came through an OpenTelemetry Collector (`runtime_sdk_versions`,
|
|
85
|
+
`via` from 1.8.0), and its own version (`runtime_ingester_info`), and the
|
|
86
|
+
Autter dashboard reads both. See [docs/COMPATIBILITY.md](../../docs/COMPATIBILITY.md).
|
|
73
87
|
|
|
74
88
|
## Configuration
|
|
75
89
|
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { Request } from "express";
|
|
2
2
|
import { type IngesterConfig } from "./config.js";
|
|
3
3
|
import type { IngestContext } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Why a key did not resolve: `unknown_key` — not a static key and the
|
|
6
|
+
* validator did not accept it; `key_unverified` — the validator could not
|
|
7
|
+
* answer (unreachable, timed out or a 5xx) and no earlier answer was cached.
|
|
8
|
+
*/
|
|
9
|
+
export type KeyFailure = "unknown_key" | "key_unverified";
|
|
10
|
+
export type KeyResolution = {
|
|
11
|
+
ctx: IngestContext;
|
|
12
|
+
failure?: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
ctx: null;
|
|
15
|
+
failure: KeyFailure;
|
|
16
|
+
};
|
|
4
17
|
export declare class KeyResolver {
|
|
5
18
|
private readonly config;
|
|
6
19
|
private readonly staticKeys;
|
|
@@ -8,6 +21,8 @@ export declare class KeyResolver {
|
|
|
8
21
|
constructor(config: IngesterConfig);
|
|
9
22
|
extractKey(req: Request): string | null;
|
|
10
23
|
resolve(key: string): Promise<IngestContext | null>;
|
|
24
|
+
/** `resolve`, plus why a key that did not resolve failed (for drop counts). */
|
|
25
|
+
resolveDetailed(key: string): Promise<KeyResolution>;
|
|
11
26
|
}
|
|
12
27
|
/** Fixed-window per-key rate limiter (single-node; Redis backing is M1). */
|
|
13
28
|
export declare class RateLimiter {
|
package/dist/auth.js
CHANGED
|
@@ -36,15 +36,20 @@ export class KeyResolver {
|
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
38
|
async resolve(key) {
|
|
39
|
+
return (await this.resolveDetailed(key)).ctx;
|
|
40
|
+
}
|
|
41
|
+
/** `resolve`, plus why a key that did not resolve failed (for drop counts). */
|
|
42
|
+
async resolveDetailed(key) {
|
|
39
43
|
const staticCtx = this.staticKeys.get(key);
|
|
40
44
|
if (staticCtx)
|
|
41
|
-
return staticCtx;
|
|
45
|
+
return { ctx: staticCtx };
|
|
42
46
|
if (!this.config.keyValidatorUrl)
|
|
43
|
-
return null;
|
|
47
|
+
return { ctx: null, failure: "unknown_key" };
|
|
44
48
|
const cached = this.cache.get(key);
|
|
45
49
|
if (cached && cached.expiresAt > Date.now())
|
|
46
|
-
return cached
|
|
50
|
+
return resolution(cached);
|
|
47
51
|
let ctx = null;
|
|
52
|
+
let failure = "unknown_key";
|
|
48
53
|
try {
|
|
49
54
|
const response = await fetch(this.config.keyValidatorUrl, {
|
|
50
55
|
method: "POST",
|
|
@@ -74,24 +79,31 @@ export class KeyResolver {
|
|
|
74
79
|
};
|
|
75
80
|
}
|
|
76
81
|
}
|
|
82
|
+
else if (response.status >= 500) {
|
|
83
|
+
failure = "key_unverified";
|
|
84
|
+
}
|
|
77
85
|
}
|
|
78
86
|
catch {
|
|
79
87
|
// Validator unreachable: fail closed for unknown keys, but reuse a
|
|
80
88
|
// stale cache entry if we have one so transient validator outages
|
|
81
89
|
// don't drop telemetry from known-good keys.
|
|
82
90
|
if (cached)
|
|
83
|
-
return cached
|
|
84
|
-
return null;
|
|
91
|
+
return resolution(cached);
|
|
92
|
+
return { ctx: null, failure: "key_unverified" };
|
|
85
93
|
}
|
|
86
|
-
|
|
94
|
+
const entry = { ctx, expiresAt: Date.now() + VALIDATOR_CACHE_TTL_MS, ...(ctx ? {} : { failure }) };
|
|
95
|
+
this.cache.set(key, entry);
|
|
87
96
|
if (this.cache.size > 10_000) {
|
|
88
97
|
const oldest = this.cache.keys().next().value;
|
|
89
98
|
if (oldest !== undefined)
|
|
90
99
|
this.cache.delete(oldest);
|
|
91
100
|
}
|
|
92
|
-
return
|
|
101
|
+
return resolution(entry);
|
|
93
102
|
}
|
|
94
103
|
}
|
|
104
|
+
function resolution(entry) {
|
|
105
|
+
return entry.ctx ? { ctx: entry.ctx } : { ctx: null, failure: entry.failure ?? "unknown_key" };
|
|
106
|
+
}
|
|
95
107
|
/** Fixed-window per-key rate limiter (single-node; Redis backing is M1). */
|
|
96
108
|
export class RateLimiter {
|
|
97
109
|
limitPerMinute;
|
package/dist/clickhouse.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type SourceMapCheck, type ValidSourceMap } from "./source-maps.js";
|
|
|
6
6
|
import { type SourceMapKey } from "./source-maps-lookup.js";
|
|
7
7
|
import { type RuntimeLogRecord } from "./logs.js";
|
|
8
8
|
import { type SdkVersionRow } from "./sdk-versions.js";
|
|
9
|
+
import { type IngestDropRow } from "./ingest-drops.js";
|
|
9
10
|
import type { SchemaStatus } from "./compat.js";
|
|
10
11
|
import { type Migration } from "./migrations.js";
|
|
11
12
|
import type { IngestContext, RuntimeLlmCall, RuntimeMetricPoint, RuntimeOccurrence, RuntimeSpanRow } from "./types.js";
|
|
@@ -54,6 +55,8 @@ export declare class ClickHouseStore {
|
|
|
54
55
|
applied: string[];
|
|
55
56
|
};
|
|
56
57
|
insertSdkVersions(ctx: IngestContext, rows: SdkVersionRow[]): Promise<void>;
|
|
58
|
+
/** Rejection and drop counts (ingest-drops.ts); rows carry their own tenant. */
|
|
59
|
+
insertIngestDrops(rows: IngestDropRow[]): Promise<void>;
|
|
57
60
|
/** Record this ingester's version and schema level (global, not tenant data). */
|
|
58
61
|
insertIngesterInfo(info: {
|
|
59
62
|
version: string;
|
package/dist/clickhouse.js
CHANGED
|
@@ -7,7 +7,9 @@ import { attributesWithBreadcrumbs } from "./breadcrumbs.js";
|
|
|
7
7
|
import { lookupSourceMap, presentSourceMaps } from "./source-maps-lookup.js";
|
|
8
8
|
import { logTableDDL, requestRollupTableDDL, } from "./logs.js";
|
|
9
9
|
import { ingesterInfoTableDDL, sdkVersionTableDDL, } from "./sdk-versions.js";
|
|
10
|
-
import {
|
|
10
|
+
import { ingestDropsTableDDL } from "./ingest-drops.js";
|
|
11
|
+
import { inAppFrameResolution } from "./symbolicate.js";
|
|
12
|
+
import { MIGRATIONS, OCCURRENCE_CODE_COLUMNS, OCCURRENCE_FRAME_RESOLUTION_COLUMNS, OCCURRENCE_GROUPING_COLUMNS, OCCURRENCE_REQUEST_ID_INDEX, migrationsTableDDL, } from "./migrations.js";
|
|
11
13
|
/**
|
|
12
14
|
* ClickHouse persistence. The schema is idempotent and applied lazily once
|
|
13
15
|
* per process. Every row is keyed by (org_id, repository_id) — tenant
|
|
@@ -22,6 +24,14 @@ const INSERT_SETTINGS = {
|
|
|
22
24
|
/** Source maps above this go in with a plain insert (below ClickHouse's
|
|
23
25
|
* default async_insert_max_data_size of 10 MiB). */
|
|
24
26
|
const ASYNC_INSERT_MAX_MAP_CHARS = 8 * 1024 * 1024;
|
|
27
|
+
/** Migration 0020 columns: browser occurrences only (server frames are not symbolicated). */
|
|
28
|
+
function occurrenceFrameResolution(o) {
|
|
29
|
+
const counts = o.source === "browser" ? inAppFrameResolution(o.frames) : { resolved: 0, unresolved: 0 };
|
|
30
|
+
return {
|
|
31
|
+
in_app_frames_resolved: Math.min(65_535, counts.resolved),
|
|
32
|
+
in_app_frames_unresolved: Math.min(65_535, counts.unresolved),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
25
35
|
/** "YYYY-MM-DD hh:mm:ss.sss" (UTC) — the DateTime64 query-parameter form. */
|
|
26
36
|
function chDateTime(date) {
|
|
27
37
|
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
@@ -74,6 +84,7 @@ export class ClickHouseStore {
|
|
|
74
84
|
requestRollupTableDDL(db),
|
|
75
85
|
sdkVersionTableDDL(db, metricsTtlDays),
|
|
76
86
|
ingesterInfoTableDDL(db),
|
|
87
|
+
ingestDropsTableDDL(db),
|
|
77
88
|
`CREATE TABLE IF NOT EXISTS ${db}.runtime_error_occurrences (
|
|
78
89
|
org_id String,
|
|
79
90
|
repository_id String,
|
|
@@ -101,6 +112,7 @@ export class ClickHouseStore {
|
|
|
101
112
|
ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
|
|
102
113
|
${OCCURRENCE_CODE_COLUMNS.join(",\n\t\t\t\t")},
|
|
103
114
|
${OCCURRENCE_GROUPING_COLUMNS.join(",\n\t\t\t\t")},
|
|
115
|
+
${OCCURRENCE_FRAME_RESOLUTION_COLUMNS.join(",\n\t\t\t\t")},
|
|
104
116
|
${OCCURRENCE_REQUEST_ID_INDEX}
|
|
105
117
|
)
|
|
106
118
|
ENGINE = MergeTree
|
|
@@ -317,6 +329,30 @@ export class ClickHouseStore {
|
|
|
317
329
|
sdk_language: row.sdkLanguage,
|
|
318
330
|
ingester_version: row.ingesterVersion,
|
|
319
331
|
last_seen: row.seenAt.toISOString(),
|
|
332
|
+
via: row.via,
|
|
333
|
+
})),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/** Rejection and drop counts (ingest-drops.ts); rows carry their own tenant. */
|
|
337
|
+
async insertIngestDrops(rows) {
|
|
338
|
+
if (!rows.length || !this.configured)
|
|
339
|
+
return;
|
|
340
|
+
await this.ensureSchema();
|
|
341
|
+
await this.getClient().insert({
|
|
342
|
+
table: this.table("runtime_ingest_drops"),
|
|
343
|
+
format: "JSONEachRow",
|
|
344
|
+
clickhouse_settings: INSERT_SETTINGS,
|
|
345
|
+
values: rows.map((row) => ({
|
|
346
|
+
org_id: row.orgId,
|
|
347
|
+
repository_id: row.repositoryId,
|
|
348
|
+
minute: row.minute.toISOString(),
|
|
349
|
+
route: row.route,
|
|
350
|
+
reason: row.reason,
|
|
351
|
+
kind: row.kind,
|
|
352
|
+
status: row.status,
|
|
353
|
+
key_hash: row.keyHash,
|
|
354
|
+
requests: row.requests,
|
|
355
|
+
records: row.records,
|
|
320
356
|
})),
|
|
321
357
|
});
|
|
322
358
|
}
|
|
@@ -352,6 +388,7 @@ export class ClickHouseStore {
|
|
|
352
388
|
format: "JSONEachRow",
|
|
353
389
|
clickhouse_settings: INSERT_SETTINGS,
|
|
354
390
|
values: occurrences.map((o) => ({
|
|
391
|
+
...occurrenceFrameResolution(o),
|
|
355
392
|
org_id: ctx.orgId,
|
|
356
393
|
repository_id: ctx.repositoryId,
|
|
357
394
|
occurrence_id: o.occurrenceId,
|
|
@@ -134,6 +134,27 @@
|
|
|
134
134
|
"migrations": ["0017-source-maps-debug-id"],
|
|
135
135
|
"route": "/v1/sourcemaps/check",
|
|
136
136
|
"sdks": {}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"id": "ingest_drop_counts",
|
|
140
|
+
"title": "Rejected and dropped records per repository",
|
|
141
|
+
"ingester": "1.8.0",
|
|
142
|
+
"migrations": ["0018-runtime-ingest-drops"],
|
|
143
|
+
"sdks": {}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"id": "telemetry_sender_via",
|
|
147
|
+
"title": "How each service's telemetry arrives (SDK, OpenTelemetry Collector or browser relay)",
|
|
148
|
+
"ingester": "1.8.0",
|
|
149
|
+
"migrations": ["0019-sdk-versions-via"],
|
|
150
|
+
"sdks": {}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "frame_resolution_counts",
|
|
154
|
+
"title": "Resolved and unresolved browser frames per occurrence",
|
|
155
|
+
"ingester": "1.8.0",
|
|
156
|
+
"migrations": ["0020-occurrence-frame-resolution"],
|
|
157
|
+
"sdks": {}
|
|
137
158
|
}
|
|
138
159
|
]
|
|
139
160
|
}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Run a synchronous normaliser and return what it cut along with its result. */
|
|
2
|
+
export declare function countTruncations<T>(normalise: () => T): {
|
|
3
|
+
result: T;
|
|
4
|
+
truncated: number;
|
|
5
|
+
};
|
|
1
6
|
/** Bounded, defensive privacy boundary for custom telemetry from any OTLP SDK. */
|
|
2
7
|
export declare function sanitizeRuntimeContext(input: unknown): Record<string, unknown>;
|
|
3
8
|
export interface OtlpValue {
|
package/dist/context.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { isSensitiveKey, scrubText } from "./redact.js";
|
|
2
2
|
import { BREADCRUMBS_ATTRIBUTE } from "./breadcrumbs.js";
|
|
3
|
+
/**
|
|
4
|
+
* Attribute values the bounds below cut or left out (a string past its
|
|
5
|
+
* length cap, list items past 64, map keys past 128, nesting past 6, the
|
|
6
|
+
* 512-value budget), counted since the process started. The normalisers are
|
|
7
|
+
* synchronous, so the difference across one call is that call's own count:
|
|
8
|
+
* see `countTruncations`. Privacy rewrites (redaction, query stripping) are
|
|
9
|
+
* not truncation and are not counted.
|
|
10
|
+
*/
|
|
11
|
+
let truncatedValues = 0;
|
|
12
|
+
/** Run a synchronous normaliser and return what it cut along with its result. */
|
|
13
|
+
export function countTruncations(normalise) {
|
|
14
|
+
const before = truncatedValues;
|
|
15
|
+
const result = normalise();
|
|
16
|
+
return { result, truncated: truncatedValues - before };
|
|
17
|
+
}
|
|
3
18
|
/** Bounded, defensive privacy boundary for custom telemetry from any OTLP SDK. */
|
|
4
19
|
export function sanitizeRuntimeContext(input) {
|
|
5
20
|
let budget = 512;
|
|
@@ -15,8 +30,10 @@ export function sanitizeRuntimeContext(input) {
|
|
|
15
30
|
return cut <= 0 ? url : url.slice(0, url.length - rest.length + cut);
|
|
16
31
|
});
|
|
17
32
|
const visit = (value, key, depth) => {
|
|
18
|
-
if (--budget < 0 || depth > 6)
|
|
33
|
+
if (--budget < 0 || depth > 6) {
|
|
34
|
+
truncatedValues += 1;
|
|
19
35
|
return "[truncated]";
|
|
36
|
+
}
|
|
20
37
|
if (/__proto__|constructor|prototype/i.test(key))
|
|
21
38
|
return undefined;
|
|
22
39
|
const usage =
|
|
@@ -44,6 +61,8 @@ export function sanitizeRuntimeContext(input) {
|
|
|
44
61
|
// Cut before scrubbing so scrub work is bounded by the stored size;
|
|
45
62
|
// the slack keeps a secret straddling the cut whole for the scrubber.
|
|
46
63
|
const limit = /stack/i.test(key) ? 32000 : 2048;
|
|
64
|
+
if (text.length > limit)
|
|
65
|
+
truncatedValues += 1;
|
|
47
66
|
return scrub(text.slice(0, limit + 512)).slice(0, limit);
|
|
48
67
|
}
|
|
49
68
|
if (typeof value === "number")
|
|
@@ -55,11 +74,19 @@ export function sanitizeRuntimeContext(input) {
|
|
|
55
74
|
if (seen.has(value))
|
|
56
75
|
return "[circular]";
|
|
57
76
|
seen.add(value);
|
|
58
|
-
if (Array.isArray(value))
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
if (value.length > 64)
|
|
79
|
+
truncatedValues += value.length - 64;
|
|
59
80
|
return value.slice(0, 64).map((v) => visit(v, key, depth + 1));
|
|
81
|
+
}
|
|
60
82
|
const result = {};
|
|
61
|
-
|
|
83
|
+
const entries = Object.entries(value);
|
|
84
|
+
if (entries.length > 128)
|
|
85
|
+
truncatedValues += entries.length - 128;
|
|
86
|
+
for (const [k, v] of entries.slice(0, 128)) {
|
|
62
87
|
const safe = visit(v, k, depth + 1);
|
|
88
|
+
if (k.length > 200)
|
|
89
|
+
truncatedValues += 1;
|
|
63
90
|
if (safe !== undefined)
|
|
64
91
|
result[k.slice(0, 200)] = safe;
|
|
65
92
|
}
|
|
@@ -72,8 +99,12 @@ export function sanitizeRuntimeContext(input) {
|
|
|
72
99
|
}
|
|
73
100
|
export function decodeOtlpAttributes(attributes) {
|
|
74
101
|
const decode = (v, depth = 0) => {
|
|
75
|
-
if (!v
|
|
102
|
+
if (!v)
|
|
103
|
+
return undefined;
|
|
104
|
+
if (depth > 6) {
|
|
105
|
+
truncatedValues += 1;
|
|
76
106
|
return undefined;
|
|
107
|
+
}
|
|
77
108
|
if (v.stringValue !== undefined)
|
|
78
109
|
return v.stringValue;
|
|
79
110
|
if (v.intValue !== undefined)
|
|
@@ -82,17 +113,25 @@ export function decodeOtlpAttributes(attributes) {
|
|
|
82
113
|
return v.doubleValue;
|
|
83
114
|
if (v.boolValue !== undefined)
|
|
84
115
|
return v.boolValue;
|
|
85
|
-
if (v.arrayValue)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
if (v.arrayValue) {
|
|
117
|
+
const values = v.arrayValue.values ?? [];
|
|
118
|
+
if (values.length > 64)
|
|
119
|
+
truncatedValues += values.length - 64;
|
|
120
|
+
return values.slice(0, 64).map((value) => decode(value, depth + 1));
|
|
121
|
+
}
|
|
122
|
+
if (v.kvlistValue) {
|
|
123
|
+
const values = v.kvlistValue.values ?? [];
|
|
124
|
+
if (values.length > 128)
|
|
125
|
+
truncatedValues += values.length - 128;
|
|
126
|
+
return Object.fromEntries(values
|
|
91
127
|
.slice(0, 128)
|
|
92
128
|
.filter((a) => a.key && !/^(?:__proto__|constructor|prototype)$/.test(a.key))
|
|
93
129
|
.map((a) => [a.key, decode(a.value, depth + 1)]));
|
|
130
|
+
}
|
|
94
131
|
return undefined;
|
|
95
132
|
};
|
|
133
|
+
if (Array.isArray(attributes) && attributes.length > 128)
|
|
134
|
+
truncatedValues += attributes.length - 128;
|
|
96
135
|
return sanitizeRuntimeContext(Object.fromEntries((attributes ?? [])
|
|
97
136
|
.slice()
|
|
98
137
|
.sort((a, b) => Number(/^autter\.(?:operation|event)\./.test(b.key ?? "")) -
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createIngesterApp } from "./server.js";
|
|
2
2
|
export { loadConfig } from "./config.js";
|
|
3
3
|
export * from "./compat.js";
|
|
4
|
-
export { SinkForwarder, type SinkStats, type SinkTuning } from "./sink.js";
|
|
4
|
+
export { SinkForwarder, type SinkDrop, type SinkStats, type SinkTuning } from "./sink.js";
|
|
5
|
+
export { DROP_REASONS, IngestDropCounter, type DropKind, type DropReason, type DropRoute, type IngestDrop, type IngestDropRow, } from "./ingest-drops.js";
|
|
5
6
|
export * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { loadConfig } from "./config.js";
|
|
|
2
2
|
import { createIngesterApp } from "./server.js";
|
|
3
3
|
import { ingesterVersion } from "./sdk-versions.js";
|
|
4
4
|
const config = loadConfig();
|
|
5
|
-
const { app, store, sink, reportIngesterInfo } = createIngesterApp(config);
|
|
5
|
+
const { app, store, sink, reportIngesterInfo, drops } = createIngesterApp(config);
|
|
6
6
|
const server = app.listen(config.port, () => {
|
|
7
7
|
console.log(`autter otlp-ingester ${ingesterVersion()} listening on :${config.port} ` +
|
|
8
8
|
`(clickhouse: ${config.clickhouseUrl ? "configured" : "NOT configured"})`);
|
|
@@ -25,7 +25,12 @@ async function shutdown(signal) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
server.close(() => {
|
|
28
|
-
|
|
28
|
+
// Write the rejection/drop counts still buffered (best effort: a
|
|
29
|
+
// failed write is logged, never retried).
|
|
30
|
+
void drops
|
|
31
|
+
.stop()
|
|
32
|
+
.finally(() => store.close())
|
|
33
|
+
.finally(() => process.exit(0));
|
|
29
34
|
});
|
|
30
35
|
setTimeout(() => process.exit(1), 10_000).unref();
|
|
31
36
|
}
|
|
@@ -35,4 +40,5 @@ export { createIngesterApp } from "./server.js";
|
|
|
35
40
|
export { loadConfig } from "./config.js";
|
|
36
41
|
export * from "./compat.js";
|
|
37
42
|
export { SinkForwarder } from "./sink.js";
|
|
43
|
+
export { DROP_REASONS, IngestDropCounter, } from "./ingest-drops.js";
|
|
38
44
|
export * from "./types.js";
|