@autter/otlp-ingester 1.4.0 → 1.6.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 +51 -0
- package/dist/breadcrumbs.d.ts +41 -0
- package/dist/breadcrumbs.js +151 -0
- package/dist/clickhouse.d.ts +42 -5
- package/dist/clickhouse.js +168 -7
- package/dist/compat-manifest.json +123 -0
- package/dist/compat.d.ts +149 -0
- package/dist/compat.js +221 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +19 -0
- package/dist/context.js +24 -11
- package/dist/error-fields.d.ts +41 -0
- package/dist/error-fields.js +103 -0
- package/dist/fingerprint-override.d.ts +42 -0
- package/dist/fingerprint-override.js +76 -0
- package/dist/fingerprint-v3.d.ts +54 -0
- package/dist/fingerprint-v3.js +96 -0
- package/dist/fingerprint.d.ts +54 -0
- package/dist/fingerprint.js +46 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -3
- package/dist/llm.js +16 -3
- package/dist/log-promotion.d.ts +55 -0
- package/dist/log-promotion.js +119 -0
- package/dist/logs.d.ts +57 -1
- package/dist/logs.js +148 -11
- package/dist/memory.d.ts +2 -2
- package/dist/migrations.d.ts +13 -0
- package/dist/migrations.js +75 -2
- package/dist/normalize-browser.d.ts +67 -22
- package/dist/normalize-browser.js +46 -19
- package/dist/normalize-otlp.js +18 -2
- package/dist/redact.d.ts +33 -0
- package/dist/redact.js +96 -0
- package/dist/sdk-versions.d.ts +58 -0
- package/dist/sdk-versions.js +167 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +219 -15
- package/dist/sink.d.ts +13 -0
- package/dist/sink.js +37 -4
- package/dist/source-maps-lookup.d.ts +31 -0
- package/dist/source-maps-lookup.js +64 -0
- package/dist/source-maps.d.ts +63 -2
- package/dist/source-maps.js +121 -13
- package/dist/stack-fixtures.d.ts +10 -2
- package/dist/stack-fixtures.js +13 -3
- package/dist/stack-frames.d.ts +48 -0
- package/dist/stack-frames.js +441 -0
- package/dist/symbolicate.d.ts +79 -0
- package/dist/symbolicate.js +257 -0
- package/dist/types.d.ts +86 -0
- package/package.json +2 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { normalizeRoute } from "./fingerprint.js";
|
|
2
|
+
/**
|
|
3
|
+
* Log promotion — errors from SDKs running WITHOUT a trace pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Logger-only mode (`initAutterLogging`) and `@autter/runtime-edge` have no
|
|
6
|
+
* span to record an exception on, so they emit an error log record with
|
|
7
|
+
* `exception.*` and `autter.capture.mode = "log"`. /v1/logs turns those
|
|
8
|
+
* records into ordinary server occurrences (fingerprinted, stored in
|
|
9
|
+
* runtime_error_occurrences, forwarded to the sink) — otherwise their
|
|
10
|
+
* errors would never reach issue grouping.
|
|
11
|
+
*
|
|
12
|
+
* Dedupe, so one failure never becomes two occurrences through two export
|
|
13
|
+
* paths:
|
|
14
|
+
* - in-batch: at most one promoted occurrence per trace id (first wins);
|
|
15
|
+
* - cross-batch: a trace id that already produced an occurrence for this
|
|
16
|
+
* tenant within ±PROMOTION_DEDUPE_WINDOW_MS (ClickHouse lookup) is skipped.
|
|
17
|
+
* Records without a trace id are always promoted. The lookup is
|
|
18
|
+
* best-effort and bounded (see promotionLookups, plus a per-tenant lookup
|
|
19
|
+
* budget in server.ts): when it fails, is skipped or is over budget the
|
|
20
|
+
* record is promoted anyway — a rare duplicate beats a silently dropped
|
|
21
|
+
* error.
|
|
22
|
+
*/
|
|
23
|
+
export const PROMOTION_DEDUPE_WINDOW_MS = 60_000;
|
|
24
|
+
/** Widest single lookup window. Candidate timestamps come from the client,
|
|
25
|
+
* so one batch could otherwise ask ClickHouse to scan an arbitrary range. */
|
|
26
|
+
export const MAX_LOOKUP_SPAN_MS = 10 * 60_000;
|
|
27
|
+
/** At most this many lookup windows (queries) per request. */
|
|
28
|
+
export const MAX_LOOKUP_WINDOWS = 4;
|
|
29
|
+
/** Candidates outside [now − 24 h, now + 5 min] are never looked up: their
|
|
30
|
+
* trace cannot plausibly have a recent occurrence to dedupe against. */
|
|
31
|
+
export const LOOKUP_PAST_MS = 24 * 60 * 60_000;
|
|
32
|
+
export const LOOKUP_FUTURE_MS = 5 * 60_000;
|
|
33
|
+
/** Promotion candidates in record order, deduped by trace id in-batch. */
|
|
34
|
+
export function logPromotionCandidates(rows) {
|
|
35
|
+
const traces = new Set();
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
const occurrence = row.occurrence;
|
|
39
|
+
if (!occurrence)
|
|
40
|
+
continue;
|
|
41
|
+
if (occurrence.traceId) {
|
|
42
|
+
if (traces.has(occurrence.traceId))
|
|
43
|
+
continue;
|
|
44
|
+
traces.add(occurrence.traceId);
|
|
45
|
+
}
|
|
46
|
+
out.push(occurrence);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Bounded dedupe lookups for the candidates that carry a trace id: each
|
|
52
|
+
* window is [oldest − 60 s, newest + 60 s] over candidates taken newest first
|
|
53
|
+
* and spans at most MAX_LOOKUP_SPAN_MS; at most MAX_LOOKUP_WINDOWS windows are
|
|
54
|
+
* planned.
|
|
55
|
+
* Candidates outside the plausible time range or beyond the window cap are
|
|
56
|
+
* not looked up — they are still deduped in-batch and promoted.
|
|
57
|
+
*/
|
|
58
|
+
export function promotionLookups(occurrences, now = Date.now()) {
|
|
59
|
+
const eligible = occurrences
|
|
60
|
+
.filter((o) => !!o.traceId)
|
|
61
|
+
.map((o) => ({ traceId: o.traceId, at: o.occurredAt.getTime() }))
|
|
62
|
+
.filter((o) => o.at >= now - LOOKUP_PAST_MS && o.at <= now + LOOKUP_FUTURE_MS)
|
|
63
|
+
// Newest first: recent records are the likeliest duplicates of an
|
|
64
|
+
// occurrence the trace path just wrote, so they get the windows.
|
|
65
|
+
.sort((a, b) => b.at - a.at);
|
|
66
|
+
const lookups = [];
|
|
67
|
+
let start = 0;
|
|
68
|
+
while (start < eligible.length && lookups.length < MAX_LOOKUP_WINDOWS) {
|
|
69
|
+
const newest = eligible[start].at;
|
|
70
|
+
let end = start;
|
|
71
|
+
while (end + 1 < eligible.length &&
|
|
72
|
+
newest - eligible[end + 1].at + 2 * PROMOTION_DEDUPE_WINDOW_MS <= MAX_LOOKUP_SPAN_MS)
|
|
73
|
+
end++;
|
|
74
|
+
lookups.push({
|
|
75
|
+
from: new Date(eligible[end].at - PROMOTION_DEDUPE_WINDOW_MS),
|
|
76
|
+
to: new Date(newest + PROMOTION_DEDUPE_WINDOW_MS),
|
|
77
|
+
traceIds: [...new Set(eligible.slice(start, end + 1).map((o) => o.traceId))],
|
|
78
|
+
});
|
|
79
|
+
start = end + 1;
|
|
80
|
+
}
|
|
81
|
+
return lookups;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* One event / one error event per promoted occurrence — the same counting
|
|
85
|
+
* the trace path applies to occurrences outside a server span, so issue
|
|
86
|
+
* counters for logger-only services don't read 0.
|
|
87
|
+
*/
|
|
88
|
+
export function promotionRollups(occurrences) {
|
|
89
|
+
const rollups = new Map();
|
|
90
|
+
for (const occ of occurrences) {
|
|
91
|
+
const bucketAt = new Date(Math.floor(occ.occurredAt.getTime() / 60_000) * 60_000);
|
|
92
|
+
const route = normalizeRoute(occ.route);
|
|
93
|
+
const key = [
|
|
94
|
+
occ.service,
|
|
95
|
+
occ.environment,
|
|
96
|
+
occ.release ?? "",
|
|
97
|
+
route,
|
|
98
|
+
bucketAt.getTime(),
|
|
99
|
+
].join(" ");
|
|
100
|
+
const existing = rollups.get(key);
|
|
101
|
+
if (existing) {
|
|
102
|
+
existing.requestCount += 1;
|
|
103
|
+
existing.errorCount += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
rollups.set(key, {
|
|
107
|
+
service: occ.service,
|
|
108
|
+
environment: occ.environment,
|
|
109
|
+
release: occ.release,
|
|
110
|
+
route,
|
|
111
|
+
bucketAt,
|
|
112
|
+
requestCount: 1,
|
|
113
|
+
errorCount: 1,
|
|
114
|
+
durationSumMs: 0,
|
|
115
|
+
sessionCount: 0,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return [...rollups.values()];
|
|
119
|
+
}
|
package/dist/logs.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type OtlpAttribute, type OtlpValue } from "./context.js";
|
|
2
|
+
import type { RuntimeOccurrenceInput } from "./types.js";
|
|
2
3
|
export interface RuntimeLogRecord {
|
|
3
4
|
id: string;
|
|
4
5
|
service: string;
|
|
@@ -15,6 +16,28 @@ export interface RuntimeLogRecord {
|
|
|
15
16
|
durationMs: number;
|
|
16
17
|
attributes: Record<string, unknown>;
|
|
17
18
|
occurredAt: Date;
|
|
19
|
+
/**
|
|
20
|
+
* Lifted wide-event columns (migration 0012). `kind` is "request" for
|
|
21
|
+
* request summaries, "operation" for operation summaries (including
|
|
22
|
+
* pre-1.5.0 SDKs that never sent `autter.operation.kind`), "" for plain
|
|
23
|
+
* log records. The rest default to ""/0 when the record doesn't carry them.
|
|
24
|
+
*/
|
|
25
|
+
kind: "" | "request" | "operation";
|
|
26
|
+
requestId: string;
|
|
27
|
+
/** `http.route`, query-stripped and id-normalised. */
|
|
28
|
+
route: string;
|
|
29
|
+
statusCode: number;
|
|
30
|
+
/** `autter.error.code`, only when it matches CODE_PATTERN. */
|
|
31
|
+
errorCode: string;
|
|
32
|
+
aiCostUsd: number;
|
|
33
|
+
aiCalls: number;
|
|
34
|
+
/**
|
|
35
|
+
* Not persisted on runtime_logs. Set for logger-only/edge error records
|
|
36
|
+
* (`autter.capture.mode = "log"`, severity ≥ error, `exception.*`
|
|
37
|
+
* present): the occurrence /v1/logs promotes into issue grouping —
|
|
38
|
+
* see log-promotion.ts.
|
|
39
|
+
*/
|
|
40
|
+
occurrence?: RuntimeOccurrenceInput;
|
|
18
41
|
}
|
|
19
42
|
export interface OtlpLogsRequest {
|
|
20
43
|
resourceLogs?: Array<{
|
|
@@ -39,4 +62,37 @@ export interface OtlpLogsRequest {
|
|
|
39
62
|
}>;
|
|
40
63
|
}
|
|
41
64
|
export declare function normalizeLogs(request: OtlpLogsRequest): RuntimeLogRecord[];
|
|
42
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Baseline for fresh databases. The 0012 columns and request-id skip index
|
|
67
|
+
* are included so a fresh DB matches a migrated one. `ttlDays` is
|
|
68
|
+
* LOG_TTL_DAYS; existing tables are brought in line at boot
|
|
69
|
+
* (ClickHouseStore.applyLogTtl), not here.
|
|
70
|
+
*/
|
|
71
|
+
export declare function logTableDDL(db: string, ttlDays?: number): string;
|
|
72
|
+
/** Migration 0012 columns — shared by the baseline DDL and the migration. */
|
|
73
|
+
export declare const LOG_REQUEST_COLUMNS: Array<{
|
|
74
|
+
name: string;
|
|
75
|
+
ddl: string;
|
|
76
|
+
}>;
|
|
77
|
+
export declare const LOG_REQUEST_ID_INDEX = "INDEX idx_logs_request_id request_id TYPE bloom_filter GRANULARITY 4";
|
|
78
|
+
/**
|
|
79
|
+
* Per-minute request rollup (migration 0014), fed by runtime_request_1m_mv
|
|
80
|
+
* from `runtime_logs` rows with kind = 'request'. Route stats read this
|
|
81
|
+
* instead of scanning raw summaries:
|
|
82
|
+
* sum(request_count), sum(failed_count), sum(duration_sum_ms),
|
|
83
|
+
* quantilesMerge(0.5, 0.95)(duration_quantiles)
|
|
84
|
+
*/
|
|
85
|
+
export declare function requestRollupTableDDL(db: string): string;
|
|
86
|
+
/**
|
|
87
|
+
* The view only sees rows inserted AFTER it exists (no backfill) and is
|
|
88
|
+
* created by migration 0014 only — not the baseline — because it reads the
|
|
89
|
+
* 0012 columns, which an existing runtime_logs table lacks until migrations
|
|
90
|
+
* run. runtime_logs is a ReplacingMergeTree keyed by a content-derived
|
|
91
|
+
* event_id, so a retried batch collapses there on merge — but the view
|
|
92
|
+
* aggregates every INSERT, so retries can count a summary twice. The
|
|
93
|
+
* rollup is therefore approximate under exporter retries (rare: a retry
|
|
94
|
+
* needs a 503 or a lost 2xx); exact per-request answers come from
|
|
95
|
+
* runtime_logs itself. `method` is read from the attributes JSON since
|
|
96
|
+
* runtime_logs has no method column.
|
|
97
|
+
*/
|
|
98
|
+
export declare function requestRollupViewDDL(db: string): string;
|
package/dist/logs.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { decodeOtlpAttributes, sanitizeRuntimeContext, } from "./context.js";
|
|
3
|
+
import { declaredLink, liftErrorFields } from "./error-fields.js";
|
|
4
|
+
import { fingerprintOverrideOf } from "./fingerprint-override.js";
|
|
3
5
|
import { normalizeRoute } from "./fingerprint.js";
|
|
4
6
|
export function normalizeLogs(request) {
|
|
5
7
|
if (!request || !Array.isArray(request.resourceLogs))
|
|
@@ -73,6 +75,57 @@ export function normalizeLogs(request) {
|
|
|
73
75
|
: "";
|
|
74
76
|
if (typeof attributes.path === "string")
|
|
75
77
|
attributes.path = normalizeRoute(attributes.path.split(/[?#]/)[0]);
|
|
78
|
+
const service = String(info["service.name"] ?? "unknown").slice(0, 200);
|
|
79
|
+
const environment = String(info["deployment.environment.name"] ??
|
|
80
|
+
info["deployment.environment"] ??
|
|
81
|
+
"production").slice(0, 100);
|
|
82
|
+
const release = String(info["service.version"] ?? "").slice(0, 200);
|
|
83
|
+
const declared = liftErrorFields(attributes);
|
|
84
|
+
// The sanitiser strips URL fragments; the declared docs link keeps
|
|
85
|
+
// its #anchor from the raw attribute (still validated).
|
|
86
|
+
const rawLink = declaredLink(record.attributes?.find((a) => a?.key === "autter.error.link")
|
|
87
|
+
?.value?.stringValue);
|
|
88
|
+
if (rawLink)
|
|
89
|
+
declared.link = rawLink;
|
|
90
|
+
const rawRoute = typeof attributes["http.route"] === "string"
|
|
91
|
+
? attributes["http.route"].split(/[?#]/)[0]
|
|
92
|
+
: "";
|
|
93
|
+
const route = normalizeRoute(rawRoute).slice(0, 500);
|
|
94
|
+
const status = Number(attributes["http.response.status_code"]);
|
|
95
|
+
const statusCode = Number.isInteger(status) && status > 0 && status < 1000 ? status : 0;
|
|
96
|
+
const declaredKind = attributes["autter.operation.kind"];
|
|
97
|
+
const type = attributes["autter.event.type"] === "operation" ? "operation" : "log";
|
|
98
|
+
const ai = attributes["autter.operation.ai"];
|
|
99
|
+
const aiRollup = ai && typeof ai === "object" && !Array.isArray(ai)
|
|
100
|
+
? ai
|
|
101
|
+
: {};
|
|
102
|
+
const occurrence = attributes["autter.capture.mode"] === "log" &&
|
|
103
|
+
(severity === "error" || severity === "fatal") &&
|
|
104
|
+
hasException(attributes)
|
|
105
|
+
? {
|
|
106
|
+
source: "server",
|
|
107
|
+
severity: severity,
|
|
108
|
+
service,
|
|
109
|
+
environment,
|
|
110
|
+
release: release || null,
|
|
111
|
+
errorType: String(attributes["exception.type"] || "Error").slice(0, 200),
|
|
112
|
+
message: String(attributes["exception.message"] || message || "Unknown error").slice(0, 4000),
|
|
113
|
+
stack: typeof attributes["exception.stacktrace"] === "string"
|
|
114
|
+
? attributes["exception.stacktrace"]
|
|
115
|
+
: null,
|
|
116
|
+
route: rawRoute || stringOrNull(attributes["url.path"]),
|
|
117
|
+
method: typeof attributes["http.request.method"] === "string"
|
|
118
|
+
? attributes["http.request.method"].toUpperCase().slice(0, 16)
|
|
119
|
+
: null,
|
|
120
|
+
statusCode: statusCode || null,
|
|
121
|
+
traceId: traceId || null,
|
|
122
|
+
sessionId: null,
|
|
123
|
+
attributes,
|
|
124
|
+
occurredAt,
|
|
125
|
+
...declared,
|
|
126
|
+
...fingerprintOverrideOf(attributes["autter.fingerprint"]),
|
|
127
|
+
}
|
|
128
|
+
: undefined;
|
|
76
129
|
rows.push({
|
|
77
130
|
id: createHash("sha256")
|
|
78
131
|
.update(JSON.stringify([
|
|
@@ -86,31 +139,55 @@ export function normalizeLogs(request) {
|
|
|
86
139
|
]))
|
|
87
140
|
.digest("hex")
|
|
88
141
|
.slice(0, 32),
|
|
89
|
-
service
|
|
90
|
-
environment
|
|
91
|
-
|
|
92
|
-
"production").slice(0, 100),
|
|
93
|
-
release: String(info["service.version"] ?? "").slice(0, 200),
|
|
142
|
+
service,
|
|
143
|
+
environment,
|
|
144
|
+
release,
|
|
94
145
|
traceId,
|
|
95
146
|
spanId,
|
|
96
147
|
operationId,
|
|
97
148
|
operation: String(attributes["autter.operation.name"] ?? "").slice(0, 200),
|
|
98
|
-
type
|
|
99
|
-
? "operation"
|
|
100
|
-
: "log",
|
|
149
|
+
type,
|
|
101
150
|
severity,
|
|
102
151
|
message,
|
|
103
152
|
outcome: String(attributes["autter.operation.outcome"] ?? "").slice(0, 30),
|
|
104
153
|
durationMs: Math.max(0, Number(attributes["autter.operation.duration_ms"]) || 0),
|
|
105
154
|
attributes,
|
|
106
155
|
occurredAt,
|
|
156
|
+
kind: declaredKind === "request" || declaredKind === "operation"
|
|
157
|
+
? declaredKind
|
|
158
|
+
: type === "operation"
|
|
159
|
+
? "operation"
|
|
160
|
+
: "",
|
|
161
|
+
requestId: declared.requestId ?? "",
|
|
162
|
+
route,
|
|
163
|
+
statusCode,
|
|
164
|
+
errorCode: declared.errorCode ?? "",
|
|
165
|
+
aiCostUsd: nonNegative(attributes["autter.operation.ai.cost_usd"] ?? aiRollup.cost_usd),
|
|
166
|
+
aiCalls: Math.min(4_294_967_295, Math.round(nonNegative(attributes["autter.operation.ai.calls"] ?? aiRollup.calls))),
|
|
167
|
+
...(occurrence ? { occurrence } : {}),
|
|
107
168
|
});
|
|
108
169
|
}
|
|
109
170
|
}
|
|
110
171
|
}
|
|
111
172
|
return rows;
|
|
112
173
|
}
|
|
113
|
-
|
|
174
|
+
function hasException(attributes) {
|
|
175
|
+
return ["exception.type", "exception.message", "exception.stacktrace"].some((key) => typeof attributes[key] === "string" && attributes[key] !== "");
|
|
176
|
+
}
|
|
177
|
+
function stringOrNull(value) {
|
|
178
|
+
return typeof value === "string" && value ? value.split(/[?#]/)[0] : null;
|
|
179
|
+
}
|
|
180
|
+
function nonNegative(value) {
|
|
181
|
+
const n = Number(value);
|
|
182
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Baseline for fresh databases. The 0012 columns and request-id skip index
|
|
186
|
+
* are included so a fresh DB matches a migrated one. `ttlDays` is
|
|
187
|
+
* LOG_TTL_DAYS; existing tables are brought in line at boot
|
|
188
|
+
* (ClickHouseStore.applyLogTtl), not here.
|
|
189
|
+
*/
|
|
190
|
+
export function logTableDDL(db, ttlDays = 14) {
|
|
114
191
|
return `CREATE TABLE IF NOT EXISTS ${db}.runtime_logs (
|
|
115
192
|
org_id String, repository_id String, event_id String,
|
|
116
193
|
service LowCardinality(String), environment LowCardinality(String), release String DEFAULT '',
|
|
@@ -119,8 +196,68 @@ export function logTableDDL(db) {
|
|
|
119
196
|
severity LowCardinality(String) DEFAULT 'info', message String CODEC(ZSTD(1)),
|
|
120
197
|
outcome LowCardinality(String) DEFAULT '', duration_ms Float64 DEFAULT 0,
|
|
121
198
|
attributes String DEFAULT '{}' CODEC(ZSTD(1)), occurred_at DateTime64(3, 'UTC'),
|
|
122
|
-
ingested_at DateTime64(3, 'UTC') DEFAULT now64(3)
|
|
199
|
+
ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
|
|
200
|
+
${LOG_REQUEST_COLUMNS.map((column) => column.ddl).join(", ")},
|
|
201
|
+
${LOG_REQUEST_ID_INDEX}
|
|
123
202
|
) ENGINE = ReplacingMergeTree PARTITION BY toDate(occurred_at)
|
|
124
203
|
ORDER BY (org_id, repository_id, occurred_at, event_id)
|
|
125
|
-
TTL toDateTime(occurred_at) + INTERVAL
|
|
204
|
+
TTL toDateTime(occurred_at) + INTERVAL ${ttlDays} DAY`;
|
|
205
|
+
}
|
|
206
|
+
/** Migration 0012 columns — shared by the baseline DDL and the migration. */
|
|
207
|
+
export const LOG_REQUEST_COLUMNS = [
|
|
208
|
+
{ name: "kind", ddl: "kind LowCardinality(String) DEFAULT ''" },
|
|
209
|
+
{ name: "request_id", ddl: "request_id String DEFAULT ''" },
|
|
210
|
+
{ name: "route", ddl: "route String DEFAULT ''" },
|
|
211
|
+
{ name: "status_code", ddl: "status_code UInt16 DEFAULT 0" },
|
|
212
|
+
{ name: "error_code", ddl: "error_code String DEFAULT ''" },
|
|
213
|
+
{ name: "ai_cost_usd", ddl: "ai_cost_usd Float64 DEFAULT 0" },
|
|
214
|
+
{ name: "ai_calls", ddl: "ai_calls UInt32 DEFAULT 0" },
|
|
215
|
+
];
|
|
216
|
+
export const LOG_REQUEST_ID_INDEX = "INDEX idx_logs_request_id request_id TYPE bloom_filter GRANULARITY 4";
|
|
217
|
+
/**
|
|
218
|
+
* Per-minute request rollup (migration 0014), fed by runtime_request_1m_mv
|
|
219
|
+
* from `runtime_logs` rows with kind = 'request'. Route stats read this
|
|
220
|
+
* instead of scanning raw summaries:
|
|
221
|
+
* sum(request_count), sum(failed_count), sum(duration_sum_ms),
|
|
222
|
+
* quantilesMerge(0.5, 0.95)(duration_quantiles)
|
|
223
|
+
*/
|
|
224
|
+
export function requestRollupTableDDL(db) {
|
|
225
|
+
return `CREATE TABLE IF NOT EXISTS ${db}.runtime_request_1m (
|
|
226
|
+
org_id String, repository_id String,
|
|
227
|
+
service LowCardinality(String), environment LowCardinality(String),
|
|
228
|
+
route String, method LowCardinality(String), bucket_at DateTime('UTC'),
|
|
229
|
+
request_count SimpleAggregateFunction(sum, UInt64),
|
|
230
|
+
failed_count SimpleAggregateFunction(sum, UInt64),
|
|
231
|
+
duration_sum_ms SimpleAggregateFunction(sum, Float64),
|
|
232
|
+
duration_quantiles AggregateFunction(quantiles(0.5, 0.95), Float64)
|
|
233
|
+
) ENGINE = AggregatingMergeTree PARTITION BY toYYYYMM(bucket_at)
|
|
234
|
+
ORDER BY (org_id, repository_id, service, environment, route, method, bucket_at)
|
|
235
|
+
TTL bucket_at + INTERVAL 90 DAY`;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The view only sees rows inserted AFTER it exists (no backfill) and is
|
|
239
|
+
* created by migration 0014 only — not the baseline — because it reads the
|
|
240
|
+
* 0012 columns, which an existing runtime_logs table lacks until migrations
|
|
241
|
+
* run. runtime_logs is a ReplacingMergeTree keyed by a content-derived
|
|
242
|
+
* event_id, so a retried batch collapses there on merge — but the view
|
|
243
|
+
* aggregates every INSERT, so retries can count a summary twice. The
|
|
244
|
+
* rollup is therefore approximate under exporter retries (rare: a retry
|
|
245
|
+
* needs a 503 or a lost 2xx); exact per-request answers come from
|
|
246
|
+
* runtime_logs itself. `method` is read from the attributes JSON since
|
|
247
|
+
* runtime_logs has no method column.
|
|
248
|
+
*/
|
|
249
|
+
export function requestRollupViewDDL(db) {
|
|
250
|
+
return `CREATE MATERIALIZED VIEW IF NOT EXISTS ${db}.runtime_request_1m_mv
|
|
251
|
+
TO ${db}.runtime_request_1m AS
|
|
252
|
+
SELECT
|
|
253
|
+
org_id, repository_id, service, environment, route,
|
|
254
|
+
upper(JSONExtractString(attributes, 'http.request.method')) AS method,
|
|
255
|
+
toDateTime(toStartOfMinute(occurred_at), 'UTC') AS bucket_at,
|
|
256
|
+
count() AS request_count,
|
|
257
|
+
countIf(outcome = 'failed') AS failed_count,
|
|
258
|
+
sum(duration_ms) AS duration_sum_ms,
|
|
259
|
+
quantilesState(0.5, 0.95)(duration_ms) AS duration_quantiles
|
|
260
|
+
FROM ${db}.runtime_logs
|
|
261
|
+
WHERE kind = 'request'
|
|
262
|
+
GROUP BY org_id, repository_id, service, environment, route, method, bucket_at`;
|
|
126
263
|
}
|
package/dist/memory.d.ts
CHANGED
|
@@ -24,16 +24,16 @@ export declare const platformEventSchema: z.ZodObject<{
|
|
|
24
24
|
service: string;
|
|
25
25
|
environment: string;
|
|
26
26
|
release: string;
|
|
27
|
+
occurredAt: string;
|
|
27
28
|
kind: "oom_kill" | "restart";
|
|
28
29
|
instanceId: string;
|
|
29
30
|
platform: "ecs" | "kubernetes" | "other";
|
|
30
|
-
occurredAt: string;
|
|
31
31
|
}, {
|
|
32
32
|
service: string;
|
|
33
|
+
occurredAt: string;
|
|
33
34
|
kind: "oom_kill" | "restart";
|
|
34
35
|
instanceId: string;
|
|
35
36
|
platform: "ecs" | "kubernetes" | "other";
|
|
36
|
-
occurredAt: string;
|
|
37
37
|
environment?: string | undefined;
|
|
38
38
|
release?: string | undefined;
|
|
39
39
|
}>;
|
package/dist/migrations.d.ts
CHANGED
|
@@ -32,6 +32,19 @@
|
|
|
32
32
|
* ],
|
|
33
33
|
* },
|
|
34
34
|
*/
|
|
35
|
+
/** Migration 0013 columns — shared with the baseline DDL in clickhouse.ts. */
|
|
36
|
+
export declare const OCCURRENCE_CODE_COLUMNS: string[];
|
|
37
|
+
export declare const OCCURRENCE_REQUEST_ID_INDEX = "INDEX idx_occ_request_id request_id TYPE bloom_filter GRANULARITY 4";
|
|
38
|
+
/**
|
|
39
|
+
* Migration 0016 columns — shared with the baseline DDL in clickhouse.ts.
|
|
40
|
+
* `frames`: the structured (symbolicated) frames as JSON, '' when none.
|
|
41
|
+
* `fingerprints`: every identity candidate (manual-v1, code-v1, frames-v3,
|
|
42
|
+
* message-v1 — highest precedence first, ending with the legacy one), so
|
|
43
|
+
* readers find an issue keyed by ANY scheme with
|
|
44
|
+
* `(fingerprint = X OR has(fingerprints, X))`; rows written before 0016
|
|
45
|
+
* read `[]` and still match on the legacy `fingerprint`.
|
|
46
|
+
*/
|
|
47
|
+
export declare const OCCURRENCE_GROUPING_COLUMNS: string[];
|
|
35
48
|
export interface Migration {
|
|
36
49
|
/** Unique, ordered id: "<serial>-<slug>". Never reuse or reorder. */
|
|
37
50
|
id: string;
|
package/dist/migrations.js
CHANGED
|
@@ -34,9 +34,34 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { latencyTableDDL } from "./latency.js";
|
|
36
36
|
import { profileTableDDL } from "./profiles.js";
|
|
37
|
-
import { sourceMapTableDDL } from "./source-maps.js";
|
|
38
|
-
import { logTableDDL } from "./logs.js";
|
|
37
|
+
import { SOURCE_MAP_DEBUG_COLUMNS, SOURCE_MAP_DEBUG_ID_INDEX, sourceMapTableDDL, } from "./source-maps.js";
|
|
38
|
+
import { LOG_REQUEST_COLUMNS, LOG_REQUEST_ID_INDEX, logTableDDL, requestRollupTableDDL, requestRollupViewDDL, } from "./logs.js";
|
|
39
39
|
import { memoryTableDDL, platformEventTableDDL } from "./memory.js";
|
|
40
|
+
import { ingesterInfoTableDDL, sdkVersionTableDDL } from "./sdk-versions.js";
|
|
41
|
+
/** Migration 0013 columns — shared with the baseline DDL in clickhouse.ts. */
|
|
42
|
+
export const OCCURRENCE_CODE_COLUMNS = [
|
|
43
|
+
"error_code String DEFAULT ''",
|
|
44
|
+
"error_why String DEFAULT ''",
|
|
45
|
+
"error_fix String DEFAULT ''",
|
|
46
|
+
"error_link String DEFAULT ''",
|
|
47
|
+
"expected UInt8 DEFAULT 0",
|
|
48
|
+
"request_id String DEFAULT ''",
|
|
49
|
+
"fingerprint_scheme LowCardinality(String) DEFAULT 'message-v1'",
|
|
50
|
+
];
|
|
51
|
+
export const OCCURRENCE_REQUEST_ID_INDEX = "INDEX idx_occ_request_id request_id TYPE bloom_filter GRANULARITY 4";
|
|
52
|
+
/**
|
|
53
|
+
* Migration 0016 columns — shared with the baseline DDL in clickhouse.ts.
|
|
54
|
+
* `frames`: the structured (symbolicated) frames as JSON, '' when none.
|
|
55
|
+
* `fingerprints`: every identity candidate (manual-v1, code-v1, frames-v3,
|
|
56
|
+
* message-v1 — highest precedence first, ending with the legacy one), so
|
|
57
|
+
* readers find an issue keyed by ANY scheme with
|
|
58
|
+
* `(fingerprint = X OR has(fingerprints, X))`; rows written before 0016
|
|
59
|
+
* read `[]` and still match on the legacy `fingerprint`.
|
|
60
|
+
*/
|
|
61
|
+
export const OCCURRENCE_GROUPING_COLUMNS = [
|
|
62
|
+
"frames String DEFAULT '' CODEC(ZSTD(3))",
|
|
63
|
+
"fingerprints Array(String) DEFAULT []",
|
|
64
|
+
];
|
|
40
65
|
export const MIGRATIONS = [
|
|
41
66
|
// 0001 intentionally reserved as the baseline marker: databases created
|
|
42
67
|
// before the migration runner existed record it as applied without
|
|
@@ -129,6 +154,54 @@ export const MIGRATIONS = [
|
|
|
129
154
|
`ALTER TABLE {db}.runtime_memory_samples ADD COLUMN IF NOT EXISTS temporality LowCardinality(String) DEFAULT 'gauge'`,
|
|
130
155
|
] },
|
|
131
156
|
{ id: "0011-runtime-logs", statements: [logTableDDL("{db}")] },
|
|
157
|
+
// Request wide events: request summaries share runtime_logs with
|
|
158
|
+
// operations and plain logs; the lifted columns make "requests on this
|
|
159
|
+
// route", "everything for request X" and "coded failures" plain column
|
|
160
|
+
// filters. The skip index only covers parts written after it exists
|
|
161
|
+
// (no MATERIALIZE INDEX mutation) — older rows predate request ids.
|
|
162
|
+
{
|
|
163
|
+
id: "0012-runtime-logs-requests",
|
|
164
|
+
statements: [
|
|
165
|
+
...LOG_REQUEST_COLUMNS.map((column) => `ALTER TABLE {db}.runtime_logs ADD COLUMN IF NOT EXISTS ${column.ddl}`),
|
|
166
|
+
`ALTER TABLE {db}.runtime_logs ADD ${LOG_REQUEST_ID_INDEX.replace("INDEX ", "INDEX IF NOT EXISTS ")}`,
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
// Declared error metadata and code-based grouping (fingerprint_scheme
|
|
170
|
+
// "code-v1" vs the historical "message-v1").
|
|
171
|
+
{
|
|
172
|
+
id: "0013-occurrence-codes",
|
|
173
|
+
statements: [
|
|
174
|
+
...OCCURRENCE_CODE_COLUMNS.map((column) => `ALTER TABLE {db}.runtime_error_occurrences ADD COLUMN IF NOT EXISTS ${column}`),
|
|
175
|
+
`ALTER TABLE {db}.runtime_error_occurrences ADD ${OCCURRENCE_REQUEST_ID_INDEX.replace("INDEX ", "INDEX IF NOT EXISTS ")}`,
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
// Per-minute route rollup of request summaries. Table first, then the
|
|
179
|
+
// view (which needs both the table and the 0012 columns).
|
|
180
|
+
{
|
|
181
|
+
id: "0014-runtime-request-1m",
|
|
182
|
+
statements: [requestRollupTableDDL("{db}"), requestRollupViewDDL("{db}")],
|
|
183
|
+
},
|
|
184
|
+
// Version compatibility: SDK name/version seen per service, and this
|
|
185
|
+
// ingester's own version + schema level for the Autter dashboard.
|
|
186
|
+
{ id: "0015-runtime-compat", statements: [sdkVersionTableDDL("{db}"), ingesterInfoTableDDL("{db}")] },
|
|
187
|
+
// Structured stacks and multi-scheme identity (frames-v3): the platform
|
|
188
|
+
// resolves an issue from any candidate, so occurrence lookups by an
|
|
189
|
+
// issue's fingerprint must match every candidate a row carried.
|
|
190
|
+
{
|
|
191
|
+
id: "0016-occurrence-frames-fingerprints",
|
|
192
|
+
statements: OCCURRENCE_GROUPING_COLUMNS.map((column) => `ALTER TABLE {db}.runtime_error_occurrences ADD COLUMN IF NOT EXISTS ${column}`),
|
|
193
|
+
},
|
|
194
|
+
// Source map debug ids (EI-13): the bundle's debug id, so a map matches
|
|
195
|
+
// by the bytes it was built from whatever release the SDK reports, plus
|
|
196
|
+
// the stored map's sha256 so CI skips maps it already uploaded. The
|
|
197
|
+
// sorting key is unchanged: uploads still need a release.
|
|
198
|
+
{
|
|
199
|
+
id: "0017-source-maps-debug-id",
|
|
200
|
+
statements: [
|
|
201
|
+
...SOURCE_MAP_DEBUG_COLUMNS.map((column) => `ALTER TABLE {db}.runtime_source_maps ADD COLUMN IF NOT EXISTS ${column}`),
|
|
202
|
+
`ALTER TABLE {db}.runtime_source_maps ADD ${SOURCE_MAP_DEBUG_ID_INDEX.replace("INDEX ", "INDEX IF NOT EXISTS ")}`,
|
|
203
|
+
],
|
|
204
|
+
},
|
|
132
205
|
];
|
|
133
206
|
/** The tracking table itself — created by the runner before anything else. */
|
|
134
207
|
export function migrationsTableDDL(db) {
|