@autter/otlp-ingester 1.3.4 → 1.5.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 +50 -0
- package/dist/clickhouse.d.ts +35 -0
- package/dist/clickhouse.js +156 -2
- package/dist/compat-manifest.json +94 -0
- package/dist/compat.d.ts +147 -0
- package/dist/compat.js +219 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +18 -0
- package/dist/context.d.ts +19 -0
- package/dist/context.js +102 -0
- package/dist/error-fields.d.ts +41 -0
- package/dist/error-fields.js +103 -0
- package/dist/fingerprint.d.ts +29 -0
- package/dist/fingerprint.js +45 -0
- 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 +98 -0
- package/dist/logs.js +261 -0
- package/dist/migrations.d.ts +3 -0
- package/dist/migrations.js +44 -0
- package/dist/normalize-browser.d.ts +16 -4
- package/dist/normalize-browser.js +22 -18
- package/dist/normalize-otlp.d.ts +2 -6
- package/dist/normalize-otlp.js +22 -9
- package/dist/otlp-proto.d.ts +2 -0
- package/dist/otlp-proto.js +21 -0
- 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 +171 -5
- package/dist/sink.d.ts +9 -0
- package/dist/sink.js +26 -4
- package/dist/types.d.ts +18 -0
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -7,5 +7,8 @@ export interface IngesterApp {
|
|
|
7
7
|
store: ClickHouseStore;
|
|
8
8
|
/** Present when AUTTER_SINK_URL is configured. */
|
|
9
9
|
sink: SinkForwarder | null;
|
|
10
|
+
/** Record this ingester's version + schema level in ClickHouse (for the
|
|
11
|
+
* Autter dashboard). Called after the boot schema warm-up; throttled. */
|
|
12
|
+
reportIngesterInfo: () => Promise<void>;
|
|
10
13
|
}
|
|
11
14
|
export declare function createIngesterApp(config: IngesterConfig): IngesterApp;
|
package/dist/server.js
CHANGED
|
@@ -2,15 +2,25 @@ import express from "express";
|
|
|
2
2
|
import { KeyResolver, RateLimiter } from "./auth.js";
|
|
3
3
|
import { ClickHouseStore } from "./clickhouse.js";
|
|
4
4
|
import { normalizeLatencyHistograms } from "./latency.js";
|
|
5
|
-
import { deriveFields,
|
|
5
|
+
import { deriveFields, occurrenceFingerprint, occurrenceIdFor, } from "./fingerprint.js";
|
|
6
|
+
import { logPromotionCandidates, promotionLookups, promotionRollups, } from "./log-promotion.js";
|
|
6
7
|
import { browserPayloadSchema, normalizeBrowserPayload, } from "./normalize-browser.js";
|
|
7
8
|
import { normalizeMetrics, normalizeTraces, } from "./normalize-otlp.js";
|
|
8
|
-
import { decodeMetricsRequest, decodeTraceRequest } from "./otlp-proto.js";
|
|
9
|
+
import { decodeMetricsRequest, decodeTraceRequest, decodeLogsRequest } from "./otlp-proto.js";
|
|
10
|
+
import { normalizeLogs } from "./logs.js";
|
|
9
11
|
import { decodeProfile } from "./profiles.js";
|
|
10
12
|
import { normalizeMemoryMetrics, normalizePlatformEvent, platformEventSchema } from "./memory.js";
|
|
11
13
|
import { validateSourceMap } from "./source-maps.js";
|
|
12
14
|
import { SinkForwarder } from "./sink.js";
|
|
15
|
+
import { configureRedaction, scrubOccurrence } from "./redact.js";
|
|
16
|
+
import { buildCompatReport, evaluateCompat, INGESTER_VERSION_HEADER, } from "./compat.js";
|
|
17
|
+
import { MIGRATIONS } from "./migrations.js";
|
|
18
|
+
import { BROWSER_SDK_NAME, ingesterVersion, SdkVersionTracker, sdkSightingsFromOtlp, } from "./sdk-versions.js";
|
|
19
|
+
/** Re-report the ingester version at most this often (keeps the row inside
|
|
20
|
+
* its TTL for long-running ingesters). */
|
|
21
|
+
const INGESTER_INFO_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
13
22
|
export function createIngesterApp(config) {
|
|
23
|
+
configureRedaction(config);
|
|
14
24
|
const store = new ClickHouseStore(config);
|
|
15
25
|
// Fingerprinted occurrences feed the consumer's issue grouping, metric
|
|
16
26
|
// points feed the request/error-rate rollups, LLM calls feed spend
|
|
@@ -19,8 +29,54 @@ export function createIngesterApp(config) {
|
|
|
19
29
|
const keys = new KeyResolver(config);
|
|
20
30
|
const serverRateLimiter = new RateLimiter(config.rateLimitPerMinute);
|
|
21
31
|
const clientRateLimiter = new RateLimiter(config.clientRateLimitPerMinute);
|
|
32
|
+
// ClickHouse dedupe lookups made by /v1/logs promotion, per tenant. Far
|
|
33
|
+
// tighter than the request limit: each lookup is a query, not an insert.
|
|
34
|
+
const promotionLookupLimiter = new RateLimiter(config.promotionLookupsPerMinute ?? 30);
|
|
35
|
+
const version = ingesterVersion();
|
|
36
|
+
const sdkVersions = new SdkVersionTracker((ctx, rows) => store.insertSdkVersions(ctx, rows));
|
|
37
|
+
const allMigrationIds = MIGRATIONS.map((migration) => migration.id);
|
|
38
|
+
const compatReport = () => {
|
|
39
|
+
const schema = store.schemaStatus();
|
|
40
|
+
return buildCompatReport({
|
|
41
|
+
version,
|
|
42
|
+
schemaStatus: schema.status,
|
|
43
|
+
applied: schema.applied,
|
|
44
|
+
allMigrations: allMigrationIds,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
let lastInfoReport = 0;
|
|
48
|
+
async function reportIngesterInfo() {
|
|
49
|
+
if (!store.configured)
|
|
50
|
+
return;
|
|
51
|
+
if (Date.now() - lastInfoReport < INGESTER_INFO_INTERVAL_MS)
|
|
52
|
+
return;
|
|
53
|
+
lastInfoReport = Date.now();
|
|
54
|
+
try {
|
|
55
|
+
// Only reached from boot and authenticated ingest, which run the
|
|
56
|
+
// (memoized) schema bootstrap anyway.
|
|
57
|
+
await store.ensureSchema();
|
|
58
|
+
const report = compatReport();
|
|
59
|
+
await store.insertIngesterInfo({ version, schemaLevel: report.schema.level, report });
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
// Retry in a minute, not on every request.
|
|
63
|
+
lastInfoReport = Date.now() - INGESTER_INFO_INTERVAL_MS + 60_000;
|
|
64
|
+
console.warn("ingester info report failed:", err?.message ?? err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Fire-and-forget after a successful ingest: never delays or fails it. */
|
|
68
|
+
function recordSdks(ctx, request) {
|
|
69
|
+
void sdkVersions.observe(ctx, sdkSightingsFromOtlp(request));
|
|
70
|
+
void reportIngesterInfo();
|
|
71
|
+
}
|
|
22
72
|
const app = express();
|
|
23
73
|
app.disable("x-powered-by");
|
|
74
|
+
// Every response names the ingester version, so SDKs (and the browser
|
|
75
|
+
// relay) learn it for free from responses they already receive.
|
|
76
|
+
app.use((_req, res, next) => {
|
|
77
|
+
res.setHeader(INGESTER_VERSION_HEADER, version);
|
|
78
|
+
next();
|
|
79
|
+
});
|
|
24
80
|
app.use(express.raw({ limit: "1mb", type: ["application/x-pprof"] }));
|
|
25
81
|
app.use("/v1/sourcemaps", express.json({ limit: "5mb" }));
|
|
26
82
|
app.use(express.json({
|
|
@@ -48,6 +104,7 @@ export function createIngesterApp(config) {
|
|
|
48
104
|
res.setHeader("access-control-allow-origin", "*");
|
|
49
105
|
res.setHeader("access-control-allow-methods", "POST, OPTIONS");
|
|
50
106
|
res.setHeader("access-control-allow-headers", "content-type, authorization, x-autter-key");
|
|
107
|
+
res.setHeader("access-control-expose-headers", INGESTER_VERSION_HEADER);
|
|
51
108
|
res.setHeader("access-control-max-age", "86400");
|
|
52
109
|
if (req.method === "OPTIONS") {
|
|
53
110
|
res.status(204).end();
|
|
@@ -71,6 +128,36 @@ export function createIngesterApp(config) {
|
|
|
71
128
|
res.status(503).json({ ok: false, clickhouse: "down", ...sinkStats });
|
|
72
129
|
}
|
|
73
130
|
});
|
|
131
|
+
/**
|
|
132
|
+
* Version compatibility (public, like /healthz; no tenant data): this
|
|
133
|
+
* ingester's version, its ClickHouse schema level, and every feature in
|
|
134
|
+
* the compat manifest with whether it is available here. Optional
|
|
135
|
+
* `?features=a,b&sdk=<name>@<version>` adds the evaluated `issues`.
|
|
136
|
+
* Never triggers schema DDL.
|
|
137
|
+
*/
|
|
138
|
+
app.get("/v1/compat", (req, res) => {
|
|
139
|
+
const report = compatReport();
|
|
140
|
+
const features = typeof req.query.features === "string"
|
|
141
|
+
? req.query.features.split(",").map((f) => f.trim()).filter(Boolean).slice(0, 50)
|
|
142
|
+
: [];
|
|
143
|
+
const sdkParam = typeof req.query.sdk === "string" ? req.query.sdk.slice(0, 120) : "";
|
|
144
|
+
const at = sdkParam.lastIndexOf("@");
|
|
145
|
+
const sdk = at > 0 ? { name: sdkParam.slice(0, at), version: sdkParam.slice(at + 1) } : null;
|
|
146
|
+
const body = features.length || sdk
|
|
147
|
+
? {
|
|
148
|
+
...report,
|
|
149
|
+
issues: evaluateCompat({
|
|
150
|
+
features: features.length
|
|
151
|
+
? features
|
|
152
|
+
: report.features.filter((f) => sdk && f.sdks[sdk.name]).map((f) => f.id),
|
|
153
|
+
ingester: { version, schema: { status: report.schema.status, applied: report.schema.applied } },
|
|
154
|
+
sdk,
|
|
155
|
+
}),
|
|
156
|
+
}
|
|
157
|
+
: report;
|
|
158
|
+
res.setHeader("cache-control", "no-store");
|
|
159
|
+
res.status(200).json(body);
|
|
160
|
+
});
|
|
74
161
|
/** Auth + scope + rate limit; returns null (response sent) on failure. */
|
|
75
162
|
async function authenticate(req, res, surface) {
|
|
76
163
|
// A storage-less ingester must refuse, not accept-and-drop: exporters
|
|
@@ -122,16 +209,48 @@ export function createIngesterApp(config) {
|
|
|
122
209
|
* the sink consumer's per-occurrence dedupe holds across transport
|
|
123
210
|
* retries and duplicated ClickHouse rows stay identifiable. */
|
|
124
211
|
function fingerprintAll(ctx, inputs) {
|
|
125
|
-
return inputs.map((
|
|
126
|
-
|
|
212
|
+
return inputs.map((raw, index) => {
|
|
213
|
+
// Second line of defence, before fingerprinting, storage, and the
|
|
214
|
+
// sink (which feeds LLM fix generation): scrub secrets from the
|
|
215
|
+
// free-text fields old SDKs and third-party OTLP senders pass
|
|
216
|
+
// through verbatim. Attributes were already sanitised by the
|
|
217
|
+
// normalisers (sanitizeRuntimeContext).
|
|
218
|
+
const input = scrubOccurrence(raw);
|
|
219
|
+
const { fingerprint, scheme } = occurrenceFingerprint(input);
|
|
127
220
|
return {
|
|
128
221
|
...input,
|
|
129
222
|
occurrenceId: occurrenceIdFor(ctx, input, fingerprint, index),
|
|
130
223
|
fingerprint,
|
|
224
|
+
fingerprintScheme: scheme,
|
|
131
225
|
...deriveFields(input),
|
|
132
226
|
};
|
|
133
227
|
});
|
|
134
228
|
}
|
|
229
|
+
/** Fingerprint log-promotion candidates and drop the ones whose trace
|
|
230
|
+
* already produced an occurrence (in this batch, or within ±60 s). Ids
|
|
231
|
+
* are assigned BEFORE the lookup so they stay stable across retries
|
|
232
|
+
* whatever the lookup returns. */
|
|
233
|
+
async function promoteLogs(ctx, logs) {
|
|
234
|
+
const candidates = logPromotionCandidates(logs);
|
|
235
|
+
if (candidates.length === 0)
|
|
236
|
+
return [];
|
|
237
|
+
const fingerprinted = fingerprintAll(ctx, candidates);
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
const ownIds = fingerprinted.map((o) => o.occurrenceId);
|
|
240
|
+
for (const lookup of promotionLookups(candidates)) {
|
|
241
|
+
if (!promotionLookupLimiter.allow(`${ctx.orgId}:${ctx.repositoryId}`))
|
|
242
|
+
break;
|
|
243
|
+
try {
|
|
244
|
+
const found = await store.recentOccurrenceTraceIds(ctx, lookup.traceIds, lookup.from, lookup.to, ownIds);
|
|
245
|
+
for (const traceId of found)
|
|
246
|
+
seen.add(traceId);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
console.warn("log promotion dedupe lookup failed — promoting without it:", err instanceof Error ? err.message : err);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return fingerprinted.filter((o) => !o.traceId || !seen.has(o.traceId));
|
|
253
|
+
}
|
|
135
254
|
function storageError(res, err) {
|
|
136
255
|
console.error("clickhouse write failed:", err);
|
|
137
256
|
res.status(503).json({ error: "storage unavailable, retry later" });
|
|
@@ -260,6 +379,42 @@ export function createIngesterApp(config) {
|
|
|
260
379
|
return;
|
|
261
380
|
}
|
|
262
381
|
sink?.enqueue(ctx, fingerprinted, metricPoints, llmCalls);
|
|
382
|
+
recordSdks(ctx, request);
|
|
383
|
+
otlpSuccess(req, res);
|
|
384
|
+
});
|
|
385
|
+
app.post("/v1/logs", async (req, res) => {
|
|
386
|
+
const ctx = await authenticate(req, res, "otlp");
|
|
387
|
+
if (!ctx)
|
|
388
|
+
return;
|
|
389
|
+
let logs;
|
|
390
|
+
let logsRequest;
|
|
391
|
+
try {
|
|
392
|
+
logsRequest = req.is("application/x-protobuf") ? decodeLogsRequest(req.body) : req.body;
|
|
393
|
+
logs = normalizeLogs(logsRequest);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
res.status(400).json({ error: "invalid OTLP logs payload" });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// Logs are diagnostic evidence; exceptions and failed outcomes normally
|
|
400
|
+
// reach grouping through the trace path. The exception: logger-only /
|
|
401
|
+
// edge SDKs mark span-less error records `autter.capture.mode = "log"`,
|
|
402
|
+
// and those are promoted to occurrences here — see log-promotion.ts.
|
|
403
|
+
const promoted = await promoteLogs(ctx, logs);
|
|
404
|
+
const metricPoints = promotionRollups(promoted);
|
|
405
|
+
try {
|
|
406
|
+
await Promise.all([
|
|
407
|
+
store.insertLogs(ctx, logs),
|
|
408
|
+
store.insertOccurrences(ctx, promoted),
|
|
409
|
+
store.insertMetricPoints(ctx, metricPoints),
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
storageError(res, err);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
sink?.enqueue(ctx, promoted, metricPoints);
|
|
417
|
+
recordSdks(ctx, logsRequest);
|
|
263
418
|
otlpSuccess(req, res);
|
|
264
419
|
});
|
|
265
420
|
app.post("/v1/metrics", async (req, res) => {
|
|
@@ -291,6 +446,7 @@ export function createIngesterApp(config) {
|
|
|
291
446
|
return;
|
|
292
447
|
}
|
|
293
448
|
sink?.enqueue(ctx, [], metricPoints);
|
|
449
|
+
recordSdks(ctx, request);
|
|
294
450
|
otlpSuccess(req, res);
|
|
295
451
|
});
|
|
296
452
|
app.post("/v1/browser", async (req, res) => {
|
|
@@ -329,6 +485,16 @@ export function createIngesterApp(config) {
|
|
|
329
485
|
return;
|
|
330
486
|
}
|
|
331
487
|
sink?.enqueue(ctx, fingerprinted, metricPoints);
|
|
488
|
+
if (parsed.data.sdk) {
|
|
489
|
+
void sdkVersions.observe(ctx, [{
|
|
490
|
+
service: parsed.data.service,
|
|
491
|
+
environment: parsed.data.environment,
|
|
492
|
+
sdkName: BROWSER_SDK_NAME,
|
|
493
|
+
sdkVersion: parsed.data.sdk,
|
|
494
|
+
sdkLanguage: "webjs",
|
|
495
|
+
}]);
|
|
496
|
+
}
|
|
497
|
+
void reportIngesterInfo();
|
|
332
498
|
res.status(202).json({ accepted: fingerprinted.length });
|
|
333
499
|
});
|
|
334
500
|
// Body-parser errors (oversized/malformed JSON) → clean 4xx, not a stack.
|
|
@@ -346,5 +512,5 @@ export function createIngesterApp(config) {
|
|
|
346
512
|
console.error("unhandled error:", err);
|
|
347
513
|
res.status(500).json({ error: "internal error" });
|
|
348
514
|
});
|
|
349
|
-
return { app, store, sink };
|
|
515
|
+
return { app, store, sink, reportIngesterInfo };
|
|
350
516
|
}
|
package/dist/sink.d.ts
CHANGED
|
@@ -101,3 +101,12 @@ export declare class SinkForwarder {
|
|
|
101
101
|
private scheduleWake;
|
|
102
102
|
private send;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Wire shape of one occurrence (payload `version: 1`). 1.5.0 adds, all
|
|
106
|
+
* optional and omitted when absent: errorCode, why, fix, link, expected,
|
|
107
|
+
* requestId, traceId, route, method, statusCode, fingerprintScheme. The
|
|
108
|
+
* request-context four were previously spread as `null` when unknown and
|
|
109
|
+
* are now simply left out, matching the documented optional-string shape.
|
|
110
|
+
* Older consumers ignore unknown keys, so the version stays 1.
|
|
111
|
+
*/
|
|
112
|
+
export declare function sinkOccurrence(o: RuntimeOccurrence): Record<string, unknown>;
|
package/dist/sink.js
CHANGED
|
@@ -108,10 +108,7 @@ export class SinkForwarder {
|
|
|
108
108
|
batchId,
|
|
109
109
|
orgId: ctx.orgId,
|
|
110
110
|
repositoryId: ctx.repositoryId,
|
|
111
|
-
occurrences: occurrences.map(
|
|
112
|
-
...o,
|
|
113
|
-
occurredAt: o.occurredAt.toISOString(),
|
|
114
|
-
})),
|
|
111
|
+
occurrences: occurrences.map(sinkOccurrence),
|
|
115
112
|
metrics: metricPoints.map((p) => ({
|
|
116
113
|
...p,
|
|
117
114
|
bucketAt: p.bucketAt.toISOString(),
|
|
@@ -298,6 +295,31 @@ export class SinkForwarder {
|
|
|
298
295
|
this.scheduleWake();
|
|
299
296
|
}
|
|
300
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Wire shape of one occurrence (payload `version: 1`). 1.5.0 adds, all
|
|
300
|
+
* optional and omitted when absent: errorCode, why, fix, link, expected,
|
|
301
|
+
* requestId, traceId, route, method, statusCode, fingerprintScheme. The
|
|
302
|
+
* request-context four were previously spread as `null` when unknown and
|
|
303
|
+
* are now simply left out, matching the documented optional-string shape.
|
|
304
|
+
* Older consumers ignore unknown keys, so the version stays 1.
|
|
305
|
+
*/
|
|
306
|
+
export function sinkOccurrence(o) {
|
|
307
|
+
const { traceId, route, method, statusCode, errorCode, why, fix, link, expected, requestId, ...rest } = o;
|
|
308
|
+
return {
|
|
309
|
+
...rest,
|
|
310
|
+
occurredAt: o.occurredAt.toISOString(),
|
|
311
|
+
...(traceId ? { traceId } : {}),
|
|
312
|
+
...(route ? { route } : {}),
|
|
313
|
+
...(method ? { method } : {}),
|
|
314
|
+
...(statusCode != null ? { statusCode } : {}),
|
|
315
|
+
...(errorCode ? { errorCode } : {}),
|
|
316
|
+
...(why ? { why } : {}),
|
|
317
|
+
...(fix ? { fix } : {}),
|
|
318
|
+
...(link ? { link } : {}),
|
|
319
|
+
...(expected ? { expected: true } : {}),
|
|
320
|
+
...(requestId ? { requestId } : {}),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
301
323
|
/** ISO range across every signal in the batch — the replay hint on drops. */
|
|
302
324
|
function signalRange(occurrences, metricPoints, llmCalls) {
|
|
303
325
|
const timestamps = [
|
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,22 @@ export interface RuntimeOccurrenceInput {
|
|
|
28
28
|
sessionId: string | null;
|
|
29
29
|
attributes: Record<string, unknown> | null;
|
|
30
30
|
occurredAt: Date;
|
|
31
|
+
/**
|
|
32
|
+
* Declared error metadata (`autter.error.*`, see error-fields.ts) —
|
|
33
|
+
* optional and omitted when absent. A valid `errorCode` switches the
|
|
34
|
+
* fingerprint to the source-independent "code-v1" scheme.
|
|
35
|
+
*/
|
|
36
|
+
errorCode?: string;
|
|
37
|
+
/** Declared cause (≤1000 chars). */
|
|
38
|
+
why?: string;
|
|
39
|
+
/** Declared remedy (≤1000 chars). */
|
|
40
|
+
fix?: string;
|
|
41
|
+
/** Docs link — http(s) only, ≤500 chars. */
|
|
42
|
+
link?: string;
|
|
43
|
+
/** Expected business failure (declines, validation): recorded, never paged. */
|
|
44
|
+
expected?: boolean;
|
|
45
|
+
/** `autter.request.id` — joins the occurrence to its request summary. */
|
|
46
|
+
requestId?: string;
|
|
31
47
|
}
|
|
32
48
|
/**
|
|
33
49
|
* Derived, aggregation-ready fields — computed once at ingest (from the
|
|
@@ -37,6 +53,8 @@ export interface RuntimeOccurrenceInput {
|
|
|
37
53
|
export interface RuntimeOccurrence extends RuntimeOccurrenceInput {
|
|
38
54
|
occurrenceId: string;
|
|
39
55
|
fingerprint: string;
|
|
56
|
+
/** "code-v1" when grouped by a valid error code, else "message-v1". */
|
|
57
|
+
fingerprintScheme: "message-v1" | "code-v1";
|
|
40
58
|
/** normalizeRoute(route) — "/users/:id/orders/:id"; GROUP BY-safe. */
|
|
41
59
|
routeNormalized: string;
|
|
42
60
|
/** normalizeMessage(message) — ids/numbers/strings templated out. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autter/otlp-ingester",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Self-hostable OTLP + browser-error ingest service for Autter Runtime: normalises telemetry into a per-repo ClickHouse data model",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|