@autter/otlp-ingester 0.1.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.
@@ -0,0 +1,80 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Deterministic error grouping. Two occurrences share an issue when they
4
+ * share source + service + error type + normalised message + top stack
5
+ * frames + normalised route. Volatile fragments (ids, numbers, minified
6
+ * line/column offsets) are stripped so re-deploys and per-user values don't
7
+ * fragment issues. This algorithm must stay in sync with the Autter backend
8
+ * so browser-relay and OTLP occurrences group identically.
9
+ */
10
+ const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
11
+ const LONG_HEX_RE = /\b[0-9a-f]{8,}\b/gi;
12
+ // No trailing \b: numbers glued to units ("4200ms", "3.5s", "512kb") must
13
+ // template too, or per-value messages fragment into separate fingerprints.
14
+ const NUMBER_RE = /\b\d+(\.\d+)?/g;
15
+ const QUOTED_RE = /(["'`])(?:\\.|(?!\1).)*\1/g;
16
+ export function normalizeMessage(message) {
17
+ return message
18
+ .slice(0, 500)
19
+ .replace(QUOTED_RE, "<str>")
20
+ .replace(UUID_RE, "<uuid>")
21
+ .replace(LONG_HEX_RE, "<hex>")
22
+ .replace(NUMBER_RE, "<n>")
23
+ .replace(/\s+/g, " ")
24
+ .trim();
25
+ }
26
+ /** Replace id-like path segments so /orders/812 and /orders/44 group. */
27
+ export function normalizeRoute(route) {
28
+ if (!route)
29
+ return "";
30
+ const path = route.split("?")[0] ?? "";
31
+ return path
32
+ .split("/")
33
+ .map((segment) => {
34
+ if (!segment)
35
+ return segment;
36
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment))
37
+ return ":id";
38
+ if (/^\d+$/.test(segment))
39
+ return ":id";
40
+ if (/^[0-9a-f]{8,}$/i.test(segment))
41
+ return ":id";
42
+ return segment;
43
+ })
44
+ .join("/");
45
+ }
46
+ const FRAME_LOCATION_RE = /:\d+(:\d+)?\)?$/;
47
+ export function normalizeStackFrames(stack, topN = 5) {
48
+ if (!stack)
49
+ return [];
50
+ return stack
51
+ .split("\n")
52
+ .map((line) => line.trim())
53
+ .filter((line) => /^at\s|@|^\s*File\s/.test(line) || /\.[jt]sx?/.test(line))
54
+ .slice(0, topN)
55
+ .map((line) => line
56
+ .replace(/\?[^:\s)]*/g, "")
57
+ .replace(FRAME_LOCATION_RE, "")
58
+ .replace(/\s+/g, " ")
59
+ .trim());
60
+ }
61
+ export function fingerprintOccurrence(input) {
62
+ const parts = [
63
+ input.source,
64
+ input.service,
65
+ input.errorType,
66
+ normalizeMessage(input.message),
67
+ ...normalizeStackFrames(input.stack),
68
+ normalizeRoute(input.route),
69
+ ];
70
+ return createHash("sha256").update(parts.join(" ")).digest("hex").slice(0, 32);
71
+ }
72
+ export function deriveFields(input) {
73
+ const topFrames = normalizeStackFrames(input.stack);
74
+ return {
75
+ routeNormalized: normalizeRoute(input.route),
76
+ messageNormalized: normalizeMessage(input.message),
77
+ topFrames,
78
+ firstFrame: topFrames[0] ?? "",
79
+ };
80
+ }
@@ -0,0 +1,3 @@
1
+ export { createIngesterApp } from "./server.js";
2
+ export { loadConfig } from "./config.js";
3
+ export * from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import { loadConfig } from "./config.js";
2
+ import { createIngesterApp } from "./server.js";
3
+ const config = loadConfig();
4
+ const { app, store } = createIngesterApp(config);
5
+ const server = app.listen(config.port, () => {
6
+ console.log(`autter otlp-ingester listening on :${config.port} ` +
7
+ `(clickhouse: ${config.clickhouseUrl ? "configured" : "NOT configured"})`);
8
+ });
9
+ // Warm the schema at boot so the first ingest request doesn't pay for DDL.
10
+ if (store.configured) {
11
+ store.ensureSchema().catch((err) => {
12
+ console.error("clickhouse schema bootstrap failed (will retry on first ingest):", err?.message ?? err);
13
+ });
14
+ }
15
+ async function shutdown(signal) {
16
+ console.log(`${signal} received, shutting down`);
17
+ server.close(() => {
18
+ void store.close().finally(() => process.exit(0));
19
+ });
20
+ setTimeout(() => process.exit(1), 10_000).unref();
21
+ }
22
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
23
+ process.on("SIGINT", () => void shutdown("SIGINT"));
24
+ export { createIngesterApp } from "./server.js";
25
+ export { loadConfig } from "./config.js";
26
+ export * from "./types.js";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ClickHouse schema migrations.
3
+ *
4
+ * The baseline schema in clickhouse.ts uses `CREATE TABLE IF NOT EXISTS`,
5
+ * which provisions FRESH databases but silently no-ops on existing ones —
6
+ * so schema changes to already-deployed databases go here. Migrations run
7
+ * automatically at ingester boot (and lazily before the first ingest),
8
+ * strictly in array order, and each applied id is recorded in
9
+ * `<db>.schema_migrations` so it runs exactly once per database.
10
+ *
11
+ * Rules for adding a migration:
12
+ * 1. Statements MUST be idempotent (`ADD COLUMN IF NOT EXISTS`,
13
+ * `DROP COLUMN IF EXISTS`, `MODIFY TTL`, …) — multiple ingester
14
+ * replicas may boot concurrently and race; idempotency makes the race
15
+ * harmless. The tracking table is bookkeeping, not a lock.
16
+ * 2. Additive only within a major version: new columns need a DEFAULT so
17
+ * old ingester replicas that are still running (rolling deploy) can
18
+ * keep inserting without naming them.
19
+ * 3. ALSO update the baseline in clickhouse.ts `schemaStatements()` so
20
+ * fresh databases come up with the final shape — a migration alone
21
+ * only fixes existing databases.
22
+ * 4. Never edit or reorder shipped migrations; append a corrective one.
23
+ *
24
+ * `{db}` is replaced with the configured database name at run time.
25
+ *
26
+ * Example (what adding a column looks like):
27
+ * {
28
+ * id: "0002-occurrences-sdk-version",
29
+ * statements: [
30
+ * `ALTER TABLE {db}.runtime_error_occurrences
31
+ * ADD COLUMN IF NOT EXISTS sdk_version String DEFAULT ''`,
32
+ * ],
33
+ * },
34
+ */
35
+ export interface Migration {
36
+ /** Unique, ordered id: "<serial>-<slug>". Never reuse or reorder. */
37
+ id: string;
38
+ statements: string[];
39
+ }
40
+ export declare const MIGRATIONS: Migration[];
41
+ /** The tracking table itself — created by the runner before anything else. */
42
+ export declare function migrationsTableDDL(db: string): string;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * ClickHouse schema migrations.
3
+ *
4
+ * The baseline schema in clickhouse.ts uses `CREATE TABLE IF NOT EXISTS`,
5
+ * which provisions FRESH databases but silently no-ops on existing ones —
6
+ * so schema changes to already-deployed databases go here. Migrations run
7
+ * automatically at ingester boot (and lazily before the first ingest),
8
+ * strictly in array order, and each applied id is recorded in
9
+ * `<db>.schema_migrations` so it runs exactly once per database.
10
+ *
11
+ * Rules for adding a migration:
12
+ * 1. Statements MUST be idempotent (`ADD COLUMN IF NOT EXISTS`,
13
+ * `DROP COLUMN IF EXISTS`, `MODIFY TTL`, …) — multiple ingester
14
+ * replicas may boot concurrently and race; idempotency makes the race
15
+ * harmless. The tracking table is bookkeeping, not a lock.
16
+ * 2. Additive only within a major version: new columns need a DEFAULT so
17
+ * old ingester replicas that are still running (rolling deploy) can
18
+ * keep inserting without naming them.
19
+ * 3. ALSO update the baseline in clickhouse.ts `schemaStatements()` so
20
+ * fresh databases come up with the final shape — a migration alone
21
+ * only fixes existing databases.
22
+ * 4. Never edit or reorder shipped migrations; append a corrective one.
23
+ *
24
+ * `{db}` is replaced with the configured database name at run time.
25
+ *
26
+ * Example (what adding a column looks like):
27
+ * {
28
+ * id: "0002-occurrences-sdk-version",
29
+ * statements: [
30
+ * `ALTER TABLE {db}.runtime_error_occurrences
31
+ * ADD COLUMN IF NOT EXISTS sdk_version String DEFAULT ''`,
32
+ * ],
33
+ * },
34
+ */
35
+ export const MIGRATIONS = [
36
+ // 0001 intentionally reserved as the baseline marker: databases created
37
+ // before the migration runner existed record it as applied without
38
+ // running anything (the baseline CREATEs already shaped them).
39
+ { id: "0001-baseline", statements: [] },
40
+ // Aggregation-ready occurrence shape: severity (warnings/info share the
41
+ // table with errors), pre-normalised route/message, extracted stack
42
+ // frames, and the request method — so later aggregations GROUP BY
43
+ // stored columns instead of re-parsing stacks/routes in SQL.
44
+ {
45
+ id: "0002-occurrences-aggregation-columns",
46
+ statements: [
47
+ `ALTER TABLE {db}.runtime_error_occurrences
48
+ ADD COLUMN IF NOT EXISTS severity LowCardinality(String) DEFAULT 'error' AFTER source`,
49
+ `ALTER TABLE {db}.runtime_error_occurrences
50
+ ADD COLUMN IF NOT EXISTS message_normalized String DEFAULT '' AFTER message`,
51
+ `ALTER TABLE {db}.runtime_error_occurrences
52
+ ADD COLUMN IF NOT EXISTS top_frames Array(String) DEFAULT [] AFTER stack`,
53
+ `ALTER TABLE {db}.runtime_error_occurrences
54
+ ADD COLUMN IF NOT EXISTS first_frame String DEFAULT '' AFTER top_frames`,
55
+ `ALTER TABLE {db}.runtime_error_occurrences
56
+ ADD COLUMN IF NOT EXISTS route_normalized String DEFAULT '' AFTER route`,
57
+ `ALTER TABLE {db}.runtime_error_occurrences
58
+ ADD COLUMN IF NOT EXISTS method LowCardinality(String) DEFAULT '' AFTER route_normalized`,
59
+ ],
60
+ },
61
+ // ZSTD compression on the fat text columns — stack traces and JSON
62
+ // attributes compress several times better under ZSTD than the LZ4
63
+ // default, and these columns dominate on-disk size. MODIFY COLUMN with
64
+ // a codec is idempotent and cheap: new parts use it immediately, old
65
+ // parts recompress on background merges.
66
+ {
67
+ id: "0003-compress-fat-columns",
68
+ statements: [
69
+ `ALTER TABLE {db}.runtime_error_occurrences
70
+ MODIFY COLUMN stack String DEFAULT '' CODEC(ZSTD(3))`,
71
+ `ALTER TABLE {db}.runtime_error_occurrences
72
+ MODIFY COLUMN message String CODEC(ZSTD(1))`,
73
+ `ALTER TABLE {db}.runtime_error_occurrences
74
+ MODIFY COLUMN attributes String DEFAULT '{}' CODEC(ZSTD(1))`,
75
+ `ALTER TABLE {db}.runtime_spans
76
+ MODIFY COLUMN attributes String DEFAULT '{}' CODEC(ZSTD(1))`,
77
+ ],
78
+ },
79
+ ];
80
+ /** The tracking table itself — created by the runner before anything else. */
81
+ export function migrationsTableDDL(db) {
82
+ return `CREATE TABLE IF NOT EXISTS ${db}.schema_migrations (
83
+ id String,
84
+ applied_at DateTime('UTC') DEFAULT now()
85
+ )
86
+ ENGINE = ReplacingMergeTree
87
+ ORDER BY id`;
88
+ }
@@ -0,0 +1,103 @@
1
+ import { z } from "zod";
2
+ import { type RuntimeMetricPoint, type RuntimeOccurrenceInput } from "./types.js";
3
+ export declare const browserPayloadSchema: z.ZodObject<{
4
+ version: z.ZodLiteral<1>;
5
+ sessionId: z.ZodOptional<z.ZodString>;
6
+ service: z.ZodString;
7
+ environment: z.ZodString;
8
+ release: z.ZodOptional<z.ZodString>;
9
+ events: z.ZodArray<z.ZodObject<{
10
+ id: z.ZodOptional<z.ZodString>;
11
+ type: z.ZodEnum<["exception", "unhandled_rejection", "message", "session_start", "track_event"]>;
12
+ timestamp: z.ZodString;
13
+ /** exception/message events: signal level. Defaults per type. */
14
+ severity: z.ZodOptional<z.ZodEnum<["fatal", "error", "warning", "info"]>>;
15
+ message: z.ZodDefault<z.ZodString>;
16
+ /** track_event only: the event name (counted, never free-form PII). */
17
+ name: z.ZodOptional<z.ZodString>;
18
+ stack: z.ZodOptional<z.ZodString>;
19
+ errorType: z.ZodOptional<z.ZodString>;
20
+ filename: z.ZodOptional<z.ZodString>;
21
+ line: z.ZodOptional<z.ZodNumber>;
22
+ column: z.ZodOptional<z.ZodNumber>;
23
+ /** Path only; query strings are stripped defensively anyway. */
24
+ route: z.ZodOptional<z.ZodString>;
25
+ context: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ type: "message" | "exception" | "unhandled_rejection" | "session_start" | "track_event";
28
+ message: string;
29
+ timestamp: string;
30
+ id?: string | undefined;
31
+ severity?: "fatal" | "error" | "warning" | "info" | undefined;
32
+ name?: string | undefined;
33
+ stack?: string | undefined;
34
+ errorType?: string | undefined;
35
+ filename?: string | undefined;
36
+ line?: number | undefined;
37
+ column?: number | undefined;
38
+ route?: string | undefined;
39
+ context?: Record<string, unknown> | undefined;
40
+ }, {
41
+ type: "message" | "exception" | "unhandled_rejection" | "session_start" | "track_event";
42
+ timestamp: string;
43
+ id?: string | undefined;
44
+ message?: string | undefined;
45
+ severity?: "fatal" | "error" | "warning" | "info" | undefined;
46
+ name?: string | undefined;
47
+ stack?: string | undefined;
48
+ errorType?: string | undefined;
49
+ filename?: string | undefined;
50
+ line?: number | undefined;
51
+ column?: number | undefined;
52
+ route?: string | undefined;
53
+ context?: Record<string, unknown> | undefined;
54
+ }>, "many">;
55
+ }, "strip", z.ZodTypeAny, {
56
+ version: 1;
57
+ service: string;
58
+ environment: string;
59
+ events: {
60
+ type: "message" | "exception" | "unhandled_rejection" | "session_start" | "track_event";
61
+ message: string;
62
+ timestamp: string;
63
+ id?: string | undefined;
64
+ severity?: "fatal" | "error" | "warning" | "info" | undefined;
65
+ name?: string | undefined;
66
+ stack?: string | undefined;
67
+ errorType?: string | undefined;
68
+ filename?: string | undefined;
69
+ line?: number | undefined;
70
+ column?: number | undefined;
71
+ route?: string | undefined;
72
+ context?: Record<string, unknown> | undefined;
73
+ }[];
74
+ sessionId?: string | undefined;
75
+ release?: string | undefined;
76
+ }, {
77
+ version: 1;
78
+ service: string;
79
+ environment: string;
80
+ events: {
81
+ type: "message" | "exception" | "unhandled_rejection" | "session_start" | "track_event";
82
+ timestamp: string;
83
+ id?: string | undefined;
84
+ message?: string | undefined;
85
+ severity?: "fatal" | "error" | "warning" | "info" | undefined;
86
+ name?: string | undefined;
87
+ stack?: string | undefined;
88
+ errorType?: string | undefined;
89
+ filename?: string | undefined;
90
+ line?: number | undefined;
91
+ column?: number | undefined;
92
+ route?: string | undefined;
93
+ context?: Record<string, unknown> | undefined;
94
+ }[];
95
+ sessionId?: string | undefined;
96
+ release?: string | undefined;
97
+ }>;
98
+ export type BrowserPayload = z.infer<typeof browserPayloadSchema>;
99
+ export interface NormalizedBrowser {
100
+ occurrences: RuntimeOccurrenceInput[];
101
+ metricPoints: RuntimeMetricPoint[];
102
+ }
103
+ export declare function normalizeBrowserPayload(payload: BrowserPayload): NormalizedBrowser;
@@ -0,0 +1,113 @@
1
+ import { z } from "zod";
2
+ import { asSeverity, } from "./types.js";
3
+ /**
4
+ * The compact browser payload (`version: 1`) emitted by
5
+ * `@autter/runtime-browser` and forwarded by the customer's same-origin
6
+ * relay. The schema is a privacy gate as much as a validator: anything not
7
+ * whitelisted here never reaches storage.
8
+ */
9
+ const browserEventSchema = z.object({
10
+ id: z.string().max(64).optional(),
11
+ type: z.enum([
12
+ "exception",
13
+ "unhandled_rejection",
14
+ "message",
15
+ "session_start",
16
+ "track_event",
17
+ ]),
18
+ timestamp: z.string().datetime(),
19
+ /** exception/message events: signal level. Defaults per type. */
20
+ severity: z.enum(["fatal", "error", "warning", "info"]).optional(),
21
+ message: z.string().max(4000).default(""),
22
+ /** track_event only: the event name (counted, never free-form PII). */
23
+ name: z.string().max(200).optional(),
24
+ stack: z.string().max(32000).optional(),
25
+ errorType: z.string().max(200).optional(),
26
+ filename: z.string().max(1000).optional(),
27
+ line: z.number().int().nonnegative().optional(),
28
+ column: z.number().int().nonnegative().optional(),
29
+ /** Path only; query strings are stripped defensively anyway. */
30
+ route: z.string().max(1000).optional(),
31
+ context: z.record(z.unknown()).optional(),
32
+ });
33
+ export const browserPayloadSchema = z.object({
34
+ version: z.literal(1),
35
+ sessionId: z.string().max(100).optional(),
36
+ service: z.string().min(1).max(200),
37
+ environment: z.string().min(1).max(100),
38
+ release: z.string().max(200).optional(),
39
+ events: z.array(browserEventSchema).max(50),
40
+ });
41
+ const TYPE_TO_ERROR_TYPE = {
42
+ exception: "Error",
43
+ unhandled_rejection: "UnhandledRejection",
44
+ message: "Message",
45
+ };
46
+ /** Default severity per event type when the SDK doesn't say. */
47
+ const TYPE_TO_SEVERITY = {
48
+ exception: "error",
49
+ unhandled_rejection: "error",
50
+ message: "warning",
51
+ };
52
+ export function normalizeBrowserPayload(payload) {
53
+ const occurrences = [];
54
+ const rollups = new Map();
55
+ function bumpRollup(route, occurredAt, field) {
56
+ const bucketAt = new Date(Math.floor(occurredAt.getTime() / 60_000) * 60_000);
57
+ const key = `${route} ${bucketAt.getTime()}`;
58
+ const existing = rollups.get(key);
59
+ if (existing) {
60
+ existing[field] += 1;
61
+ return;
62
+ }
63
+ rollups.set(key, {
64
+ service: payload.service,
65
+ environment: payload.environment,
66
+ release: payload.release ?? null,
67
+ route,
68
+ bucketAt,
69
+ requestCount: field === "requestCount" ? 1 : 0,
70
+ errorCount: 0,
71
+ durationSumMs: 0,
72
+ sessionCount: field === "sessionCount" ? 1 : 0,
73
+ });
74
+ }
75
+ for (const event of payload.events) {
76
+ const occurredAt = new Date(event.timestamp);
77
+ if (event.type === "session_start") {
78
+ bumpRollup("", occurredAt, "sessionCount");
79
+ continue;
80
+ }
81
+ // Coarse usage counters: track_event("checkout_opened") becomes a
82
+ // request_count increment on the synthetic route "event:checkout_opened".
83
+ if (event.type === "track_event") {
84
+ const name = (event.name ?? event.message ?? "").slice(0, 200);
85
+ if (name)
86
+ bumpRollup(`event:${name}`, occurredAt, "requestCount");
87
+ continue;
88
+ }
89
+ occurrences.push({
90
+ source: "browser",
91
+ severity: asSeverity(event.severity, TYPE_TO_SEVERITY[event.type] ?? "error"),
92
+ service: payload.service,
93
+ environment: payload.environment,
94
+ release: payload.release ?? null,
95
+ errorType: event.errorType ?? TYPE_TO_ERROR_TYPE[event.type] ?? "Error",
96
+ message: event.message || "Unknown error",
97
+ stack: event.stack ?? null,
98
+ route: event.route ? (event.route.split("?")[0] ?? null) : null,
99
+ method: null,
100
+ statusCode: null,
101
+ traceId: null,
102
+ sessionId: payload.sessionId ?? null,
103
+ attributes: {
104
+ ...(event.filename ? { filename: event.filename.split("?")[0] } : {}),
105
+ ...(event.line !== undefined ? { line: event.line } : {}),
106
+ ...(event.column !== undefined ? { column: event.column } : {}),
107
+ ...(event.context ? { context: event.context } : {}),
108
+ },
109
+ occurredAt,
110
+ });
111
+ }
112
+ return { occurrences, metricPoints: [...rollups.values()] };
113
+ }
@@ -0,0 +1,84 @@
1
+ import { type RuntimeMetricPoint, type RuntimeOccurrenceInput, type RuntimeSpanRow } from "./types.js";
2
+ /**
3
+ * OTLP/HTTP JSON → runtime signal. Structural types cover only the fields
4
+ * we read (the full OTLP schema is large and versioned; unknown fields pass
5
+ * through untouched). Protobuf decode is Milestone 1 — see docs/PLAN.md.
6
+ *
7
+ * Note: OTLP/JSON encodes trace/span ids as hex strings and enum fields as
8
+ * either numbers or `SPAN_KIND_*` / `STATUS_CODE_*` strings depending on the
9
+ * serialiser — both are accepted.
10
+ */
11
+ interface OtlpKeyValue {
12
+ key?: string;
13
+ value?: {
14
+ stringValue?: string;
15
+ intValue?: string | number;
16
+ doubleValue?: number;
17
+ boolValue?: boolean;
18
+ };
19
+ }
20
+ interface OtlpEvent {
21
+ name?: string;
22
+ timeUnixNano?: string | number;
23
+ attributes?: OtlpKeyValue[];
24
+ }
25
+ interface OtlpSpan {
26
+ traceId?: string;
27
+ spanId?: string;
28
+ parentSpanId?: string;
29
+ name?: string;
30
+ kind?: number | string;
31
+ startTimeUnixNano?: string | number;
32
+ endTimeUnixNano?: string | number;
33
+ attributes?: OtlpKeyValue[];
34
+ status?: {
35
+ code?: number | string;
36
+ message?: string;
37
+ };
38
+ events?: OtlpEvent[];
39
+ }
40
+ interface OtlpResource {
41
+ attributes?: OtlpKeyValue[];
42
+ }
43
+ export interface OtlpTraceRequest {
44
+ resourceSpans?: Array<{
45
+ resource?: OtlpResource;
46
+ scopeSpans?: Array<{
47
+ spans?: OtlpSpan[];
48
+ }>;
49
+ }>;
50
+ }
51
+ interface OtlpDataPoint {
52
+ attributes?: OtlpKeyValue[];
53
+ timeUnixNano?: string | number;
54
+ count?: string | number;
55
+ sum?: number;
56
+ asInt?: string | number;
57
+ asDouble?: number;
58
+ }
59
+ export interface OtlpMetricsRequest {
60
+ resourceMetrics?: Array<{
61
+ resource?: OtlpResource;
62
+ scopeMetrics?: Array<{
63
+ metrics?: Array<{
64
+ name?: string;
65
+ unit?: string;
66
+ histogram?: {
67
+ dataPoints?: OtlpDataPoint[];
68
+ };
69
+ sum?: {
70
+ dataPoints?: OtlpDataPoint[];
71
+ };
72
+ }>;
73
+ }>;
74
+ }>;
75
+ }
76
+ export interface NormalizedTraces {
77
+ occurrences: RuntimeOccurrenceInput[];
78
+ spans: RuntimeSpanRow[];
79
+ metricPoints: RuntimeMetricPoint[];
80
+ spanCount: number;
81
+ }
82
+ export declare function normalizeTraces(request: OtlpTraceRequest): NormalizedTraces;
83
+ export declare function normalizeMetrics(request: OtlpMetricsRequest): RuntimeMetricPoint[];
84
+ export {};