@autter/otlp-ingester 1.3.2 → 1.3.4
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 +4 -2
- package/dist/clickhouse.d.ts +3 -0
- package/dist/clickhouse.js +28 -1
- package/dist/memory.d.ts +45 -0
- package/dist/memory.js +114 -0
- package/dist/migrations.js +8 -0
- package/dist/normalize-browser.d.ts +11 -11
- package/dist/normalize-browser.js +3 -0
- package/dist/normalize-otlp.d.ts +4 -0
- package/dist/normalize-otlp.js +61 -0
- package/dist/otlp-proto.js +3 -1
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +9 -6
- package/dist/server.js +29 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @autter/otlp-ingester
|
|
2
2
|
|
|
3
|
-
Self-hostable ingest service for Autter Runtime. Receives OTLP/HTTP (JSON)
|
|
3
|
+
Self-hostable ingest service for Autter Runtime. Receives OTLP/HTTP (protobuf or JSON)
|
|
4
4
|
traces and metrics plus compact browser error payloads, normalises them into
|
|
5
5
|
one per-repo signal model, fingerprints errors, and writes ClickHouse.
|
|
6
6
|
|
|
@@ -9,7 +9,9 @@ one per-repo signal model, fingerprints errors, and writes ClickHouse.
|
|
|
9
9
|
| Route | Payload | Purpose |
|
|
10
10
|
| --- | --- | --- |
|
|
11
11
|
| `POST /v1/traces` | OTLP/JSON `ExportTraceServiceRequest` | Error spans → occurrences; all spans → `runtime_spans`; server spans → usage rollups; GenAI spans → `runtime_llm_calls` |
|
|
12
|
-
| `POST /v1/metrics` | OTLP
|
|
12
|
+
| `POST /v1/metrics` | OTLP `ExportMetricsServiceRequest` | HTTP-server duration histograms → usage rollups; portable process memory/GC metrics → per-instance memory samples |
|
|
13
|
+
| `POST /v1/platform-events` | JSON, server key | ECS/Kubernetes OOM kills and restarts → memory incident correlation |
|
|
14
|
+
| `POST /v1/profiles` | Symbolized pprof, server key | CPU/in-use heap profile samples |
|
|
13
15
|
| `POST /v1/browser` | Browser payload `version: 1` | Errors/rejections → occurrences; session pings → rollups |
|
|
14
16
|
| `GET /healthz` | — | Liveness + ClickHouse reachability |
|
|
15
17
|
|
package/dist/clickhouse.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IngesterConfig } from "./config.js";
|
|
2
2
|
import { type LatencyHistogram } from "./latency.js";
|
|
3
3
|
import { type ProfileSample } from "./profiles.js";
|
|
4
|
+
import { type MemorySample, type PlatformEvent } from "./memory.js";
|
|
4
5
|
import { type Migration } from "./migrations.js";
|
|
5
6
|
import type { IngestContext, RuntimeLlmCall, RuntimeMetricPoint, RuntimeOccurrence, RuntimeSpanRow } from "./types.js";
|
|
6
7
|
export declare class ClickHouseStore {
|
|
@@ -25,6 +26,8 @@ export declare class ClickHouseStore {
|
|
|
25
26
|
insertOccurrences(ctx: IngestContext, occurrences: RuntimeOccurrence[]): Promise<void>;
|
|
26
27
|
insertSpans(ctx: IngestContext, spans: RuntimeSpanRow[]): Promise<void>;
|
|
27
28
|
insertProfileSamples(ctx: IngestContext, samples: ProfileSample[]): Promise<void>;
|
|
29
|
+
insertMemorySamples(ctx: IngestContext, samples: MemorySample[]): Promise<void>;
|
|
30
|
+
insertPlatformEvent(ctx: IngestContext, event: PlatformEvent): Promise<void>;
|
|
28
31
|
insertSourceMap(ctx: IngestContext, sourceMap: {
|
|
29
32
|
release: string;
|
|
30
33
|
filename: string;
|
package/dist/clickhouse.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createClient, } from "@clickhouse/client";
|
|
2
2
|
import { latencyTableDDL } from "./latency.js";
|
|
3
3
|
import { profileTableDDL } from "./profiles.js";
|
|
4
|
+
import { memoryTableDDL, platformEventTableDDL } from "./memory.js";
|
|
4
5
|
import { sourceMapTableDDL } from "./source-maps.js";
|
|
5
6
|
import { MIGRATIONS, migrationsTableDDL, } from "./migrations.js";
|
|
6
7
|
/**
|
|
@@ -49,6 +50,8 @@ export class ClickHouseStore {
|
|
|
49
50
|
`CREATE DATABASE IF NOT EXISTS ${db}`,
|
|
50
51
|
latencyTableDDL(db, metricsTtlDays),
|
|
51
52
|
profileTableDDL(db),
|
|
53
|
+
memoryTableDDL(db),
|
|
54
|
+
platformEventTableDDL(db),
|
|
52
55
|
sourceMapTableDDL(db),
|
|
53
56
|
`CREATE TABLE IF NOT EXISTS ${db}.runtime_error_occurrences (
|
|
54
57
|
org_id String,
|
|
@@ -275,12 +278,36 @@ export class ClickHouseStore {
|
|
|
275
278
|
org_id: ctx.orgId, repository_id: ctx.repositoryId,
|
|
276
279
|
profile_id: sample.profileId, sample_index: sample.sampleIndex, service: sample.service,
|
|
277
280
|
environment: sample.environment, release: sample.release,
|
|
278
|
-
trace_id: sample.traceId, observed_at: sample.observedAt.toISOString(),
|
|
281
|
+
trace_id: sample.traceId, instance_id: sample.instanceId, observed_at: sample.observedAt.toISOString(),
|
|
279
282
|
sample_type: sample.sampleType, unit: sample.unit,
|
|
280
283
|
stack: sample.stack, value: sample.value,
|
|
281
284
|
})),
|
|
282
285
|
});
|
|
283
286
|
}
|
|
287
|
+
async insertMemorySamples(ctx, samples) {
|
|
288
|
+
if (!samples.length)
|
|
289
|
+
return;
|
|
290
|
+
if (!this.configured)
|
|
291
|
+
throw new Error("CLICKHOUSE_URL is not configured");
|
|
292
|
+
await this.ensureSchema();
|
|
293
|
+
await this.getClient().insert({ table: this.table("runtime_memory_samples"), format: "JSONEachRow",
|
|
294
|
+
clickhouse_settings: INSERT_SETTINGS,
|
|
295
|
+
values: samples.map((s) => ({ org_id: ctx.orgId, repository_id: ctx.repositoryId,
|
|
296
|
+
service: s.service, environment: s.environment, release: s.release,
|
|
297
|
+
instance_id: s.instanceId, metric: s.metric, value: s.value, temporality: s.temporality,
|
|
298
|
+
observed_at: s.observedAt.toISOString() })) });
|
|
299
|
+
}
|
|
300
|
+
async insertPlatformEvent(ctx, event) {
|
|
301
|
+
if (!this.configured)
|
|
302
|
+
throw new Error("CLICKHOUSE_URL is not configured");
|
|
303
|
+
await this.ensureSchema();
|
|
304
|
+
await this.getClient().insert({ table: this.table("runtime_platform_events"), format: "JSONEachRow",
|
|
305
|
+
clickhouse_settings: INSERT_SETTINGS,
|
|
306
|
+
values: [{ org_id: ctx.orgId, repository_id: ctx.repositoryId, event_id: event.eventId,
|
|
307
|
+
service: event.service, environment: event.environment, release: event.release,
|
|
308
|
+
instance_id: event.instanceId, platform: event.platform, kind: event.kind,
|
|
309
|
+
occurred_at: event.occurredAt }] });
|
|
310
|
+
}
|
|
284
311
|
async insertSourceMap(ctx, sourceMap) {
|
|
285
312
|
await this.ensureSchema();
|
|
286
313
|
await this.getClient().insert({
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { OtlpMetricsRequest } from "./normalize-otlp.js";
|
|
3
|
+
export type MemoryMetric = "rss" | "heap_used" | "heap_limit" | "memory_limit" | "heap_after_gc" | "gc_count" | "gc_duration_ms";
|
|
4
|
+
export interface MemorySample {
|
|
5
|
+
service: string;
|
|
6
|
+
environment: string;
|
|
7
|
+
release: string;
|
|
8
|
+
instanceId: string;
|
|
9
|
+
metric: MemoryMetric;
|
|
10
|
+
value: number;
|
|
11
|
+
observedAt: Date;
|
|
12
|
+
temporality: "gauge" | "delta" | "cumulative";
|
|
13
|
+
}
|
|
14
|
+
export declare function normalizeMemoryMetrics(request: OtlpMetricsRequest): MemorySample[];
|
|
15
|
+
export declare const platformEventSchema: z.ZodObject<{
|
|
16
|
+
service: z.ZodString;
|
|
17
|
+
environment: z.ZodDefault<z.ZodString>;
|
|
18
|
+
release: z.ZodDefault<z.ZodString>;
|
|
19
|
+
instanceId: z.ZodString;
|
|
20
|
+
platform: z.ZodEnum<["ecs", "kubernetes", "other"]>;
|
|
21
|
+
kind: z.ZodEnum<["oom_kill", "restart"]>;
|
|
22
|
+
occurredAt: z.ZodString;
|
|
23
|
+
}, "strip", z.ZodTypeAny, {
|
|
24
|
+
service: string;
|
|
25
|
+
environment: string;
|
|
26
|
+
release: string;
|
|
27
|
+
kind: "oom_kill" | "restart";
|
|
28
|
+
instanceId: string;
|
|
29
|
+
platform: "ecs" | "kubernetes" | "other";
|
|
30
|
+
occurredAt: string;
|
|
31
|
+
}, {
|
|
32
|
+
service: string;
|
|
33
|
+
kind: "oom_kill" | "restart";
|
|
34
|
+
instanceId: string;
|
|
35
|
+
platform: "ecs" | "kubernetes" | "other";
|
|
36
|
+
occurredAt: string;
|
|
37
|
+
environment?: string | undefined;
|
|
38
|
+
release?: string | undefined;
|
|
39
|
+
}>;
|
|
40
|
+
export type PlatformEvent = z.infer<typeof platformEventSchema> & {
|
|
41
|
+
eventId: string;
|
|
42
|
+
};
|
|
43
|
+
export declare function normalizePlatformEvent(input: z.infer<typeof platformEventSchema>): PlatformEvent | null;
|
|
44
|
+
export declare function memoryTableDDL(db: string): string;
|
|
45
|
+
export declare function platformEventTableDDL(db: string): string;
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function attrsOf(attributes) {
|
|
4
|
+
return new Map((attributes ?? []).flatMap((a) => {
|
|
5
|
+
const v = a.value?.stringValue ?? a.value?.intValue ?? a.value?.doubleValue;
|
|
6
|
+
return a.key && v !== undefined ? [[a.key, String(v)]] : [];
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
function metricOf(name, attrs) {
|
|
10
|
+
switch (name) {
|
|
11
|
+
case "autter.process.memory.rss":
|
|
12
|
+
case "process.runtime.nodejs.memory.rss": return "rss";
|
|
13
|
+
case "autter.process.memory.heap.used":
|
|
14
|
+
case "process.runtime.nodejs.memory.heap.used": return "heap_used";
|
|
15
|
+
case "autter.process.memory.heap.limit":
|
|
16
|
+
case "process.runtime.nodejs.memory.heap.limit": return "heap_limit";
|
|
17
|
+
case "autter.process.memory.limit":
|
|
18
|
+
case "container.memory.limit": return "memory_limit";
|
|
19
|
+
case "autter.process.memory.heap.after_gc": return "heap_after_gc";
|
|
20
|
+
case "autter.process.gc.count":
|
|
21
|
+
case "process.runtime.nodejs.gc.count": return "gc_count";
|
|
22
|
+
case "autter.process.gc.duration":
|
|
23
|
+
case "process.runtime.nodejs.gc.duration": return "gc_duration_ms";
|
|
24
|
+
case "process.memory.usage": {
|
|
25
|
+
const type = attrs.get("type") ?? attrs.get("state");
|
|
26
|
+
return type === "rss" || type === "physical" ? "rss" : type === "heap_used" ? "heap_used" : null;
|
|
27
|
+
}
|
|
28
|
+
default: return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function normalizeMemoryMetrics(request) {
|
|
32
|
+
const out = [];
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
for (const rm of request.resourceMetrics ?? []) {
|
|
35
|
+
const resource = attrsOf(rm.resource?.attributes);
|
|
36
|
+
const instanceId = resource.get("service.instance.id")?.slice(0, 128) ?? "";
|
|
37
|
+
const service = resource.get("service.name")?.slice(0, 200) ?? "";
|
|
38
|
+
if (!instanceId || !service)
|
|
39
|
+
continue;
|
|
40
|
+
const environment = (resource.get("deployment.environment.name") ?? resource.get("deployment.environment") ?? "production").slice(0, 100);
|
|
41
|
+
const release = (resource.get("service.version") ?? "").slice(0, 200);
|
|
42
|
+
for (const scope of rm.scopeMetrics ?? [])
|
|
43
|
+
for (const metric of scope.metrics ?? []) {
|
|
44
|
+
const name = metric.name ?? "";
|
|
45
|
+
const points = metric.gauge?.dataPoints ?? metric.sum?.dataPoints ?? [];
|
|
46
|
+
for (const point of points) {
|
|
47
|
+
const kind = metricOf(name, attrsOf(point.attributes));
|
|
48
|
+
if (!kind)
|
|
49
|
+
continue;
|
|
50
|
+
const counter = kind === "gc_count" || kind === "gc_duration_ms";
|
|
51
|
+
const aggregation = metric.sum?.aggregationTemporality;
|
|
52
|
+
const temporality = counter && (aggregation === 2 || aggregation === "AGGREGATION_TEMPORALITY_CUMULATIVE")
|
|
53
|
+
? "cumulative" : counter ? "delta" : "gauge";
|
|
54
|
+
const value = Number(point.asDouble ?? point.asInt);
|
|
55
|
+
let observedMs = 0;
|
|
56
|
+
try {
|
|
57
|
+
observedMs = Number(BigInt(String(point.timeUnixNano ?? "0")) / 1000000n);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!Number.isFinite(value) || value < 0 || value > 1e15 || observedMs < now - 30 * 86400_000 || observedMs > now + 300_000)
|
|
63
|
+
continue;
|
|
64
|
+
const isBytes = kind === "rss" || kind === "heap_used" || kind === "heap_limit" || kind === "memory_limit" || kind === "heap_after_gc";
|
|
65
|
+
const unit = metric.unit ?? "By";
|
|
66
|
+
if (isBytes && !["By", "byte", "bytes", "KiBy", "MiBy"].includes(unit))
|
|
67
|
+
continue;
|
|
68
|
+
// Standard OTel runtimes report GC duration in seconds; Autter uses ms.
|
|
69
|
+
out.push({ service, environment, release, instanceId, metric: kind,
|
|
70
|
+
value: isBytes ? value * (unit === "KiBy" ? 1024 : unit === "MiBy" ? 1048576 : 1)
|
|
71
|
+
: kind === "gc_duration_ms" && unit === "s" ? value * 1000 : value,
|
|
72
|
+
observedAt: new Date(observedMs), temporality });
|
|
73
|
+
if (out.length >= 5000)
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
export const platformEventSchema = z.object({
|
|
81
|
+
service: z.string().regex(/^[a-zA-Z0-9._-]{1,200}$/),
|
|
82
|
+
environment: z.string().min(1).max(100).default("production"),
|
|
83
|
+
release: z.string().max(200).default(""),
|
|
84
|
+
instanceId: z.string().min(1).max(128),
|
|
85
|
+
platform: z.enum(["ecs", "kubernetes", "other"]),
|
|
86
|
+
kind: z.enum(["oom_kill", "restart"]),
|
|
87
|
+
occurredAt: z.string().datetime(),
|
|
88
|
+
});
|
|
89
|
+
export function normalizePlatformEvent(input) {
|
|
90
|
+
const time = Date.parse(input.occurredAt);
|
|
91
|
+
if (time < Date.now() - 30 * 86400_000 || time > Date.now() + 300_000)
|
|
92
|
+
return null;
|
|
93
|
+
const eventId = createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 32);
|
|
94
|
+
return { ...input, eventId };
|
|
95
|
+
}
|
|
96
|
+
export function memoryTableDDL(db) {
|
|
97
|
+
return `CREATE TABLE IF NOT EXISTS ${db}.runtime_memory_samples (
|
|
98
|
+
org_id String, repository_id String, service LowCardinality(String),
|
|
99
|
+
environment LowCardinality(String), release String DEFAULT '', instance_id String,
|
|
100
|
+
metric LowCardinality(String), value Float64, temporality LowCardinality(String) DEFAULT 'gauge',
|
|
101
|
+
observed_at DateTime64(3, 'UTC')
|
|
102
|
+
) ENGINE = ReplacingMergeTree PARTITION BY toDate(observed_at)
|
|
103
|
+
ORDER BY (org_id, repository_id, service, environment, instance_id, metric, observed_at)
|
|
104
|
+
TTL toDateTime(observed_at) + INTERVAL 14 DAY`;
|
|
105
|
+
}
|
|
106
|
+
export function platformEventTableDDL(db) {
|
|
107
|
+
return `CREATE TABLE IF NOT EXISTS ${db}.runtime_platform_events (
|
|
108
|
+
org_id String, repository_id String, event_id String, service LowCardinality(String),
|
|
109
|
+
environment LowCardinality(String), release String DEFAULT '', instance_id String,
|
|
110
|
+
platform LowCardinality(String), kind LowCardinality(String), occurred_at DateTime64(3, 'UTC')
|
|
111
|
+
) ENGINE = ReplacingMergeTree PARTITION BY toDate(occurred_at)
|
|
112
|
+
ORDER BY (org_id, repository_id, event_id)
|
|
113
|
+
TTL toDateTime(occurred_at) + INTERVAL 30 DAY`;
|
|
114
|
+
}
|
package/dist/migrations.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { latencyTableDDL } from "./latency.js";
|
|
36
36
|
import { profileTableDDL } from "./profiles.js";
|
|
37
37
|
import { sourceMapTableDDL } from "./source-maps.js";
|
|
38
|
+
import { memoryTableDDL, platformEventTableDDL } from "./memory.js";
|
|
38
39
|
export const MIGRATIONS = [
|
|
39
40
|
// 0001 intentionally reserved as the baseline marker: databases created
|
|
40
41
|
// before the migration runner existed record it as applied without
|
|
@@ -119,6 +120,13 @@ export const MIGRATIONS = [
|
|
|
119
120
|
{ id: "0005-latency-histograms", statements: [latencyTableDDL("{db}")] },
|
|
120
121
|
{ id: "0006-profile-samples", statements: [profileTableDDL("{db}")] },
|
|
121
122
|
{ id: "0007-source-maps", statements: [sourceMapTableDDL("{db}")] },
|
|
123
|
+
{ id: "0008-memory-signals", statements: [memoryTableDDL("{db}"), platformEventTableDDL("{db}")] },
|
|
124
|
+
{ id: "0009-profile-instance", statements: [
|
|
125
|
+
`ALTER TABLE {db}.runtime_profile_samples ADD COLUMN IF NOT EXISTS instance_id String DEFAULT ''`,
|
|
126
|
+
] },
|
|
127
|
+
{ id: "0010-memory-temporality", statements: [
|
|
128
|
+
`ALTER TABLE {db}.runtime_memory_samples ADD COLUMN IF NOT EXISTS temporality LowCardinality(String) DEFAULT 'gauge'`,
|
|
129
|
+
] },
|
|
122
130
|
];
|
|
123
131
|
/** The tracking table itself — created by the runner before anything else. */
|
|
124
132
|
export function migrationsTableDDL(db) {
|
|
@@ -8,7 +8,7 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
8
8
|
release: z.ZodOptional<z.ZodString>;
|
|
9
9
|
events: z.ZodArray<z.ZodObject<{
|
|
10
10
|
id: z.ZodOptional<z.ZodString>;
|
|
11
|
-
type: z.ZodEnum<["exception", "unhandled_rejection", "message", "session_start", "track_event", "outcome", "request_failure", "timing"]>;
|
|
11
|
+
type: z.ZodEnum<["exception", "unhandled_rejection", "message", "session_start", "track_event", "outcome", "request_failure", "csp_violation", "timing"]>;
|
|
12
12
|
timestamp: z.ZodString;
|
|
13
13
|
/** exception/message events: signal level. Defaults per type. */
|
|
14
14
|
severity: z.ZodOptional<z.ZodEnum<["fatal", "error", "warning", "info"]>>;
|
|
@@ -25,10 +25,11 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
25
25
|
route: z.ZodOptional<z.ZodString>;
|
|
26
26
|
context: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
27
27
|
}, "strip", z.ZodTypeAny, {
|
|
28
|
-
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "timing";
|
|
28
|
+
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "csp_violation" | "timing";
|
|
29
29
|
message: string;
|
|
30
30
|
timestamp: string;
|
|
31
31
|
durationMs?: number | undefined;
|
|
32
|
+
route?: string | undefined;
|
|
32
33
|
filename?: string | undefined;
|
|
33
34
|
id?: string | undefined;
|
|
34
35
|
severity?: "fatal" | "error" | "warning" | "info" | undefined;
|
|
@@ -37,22 +38,21 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
37
38
|
errorType?: string | undefined;
|
|
38
39
|
line?: number | undefined;
|
|
39
40
|
column?: number | undefined;
|
|
40
|
-
route?: string | undefined;
|
|
41
41
|
context?: Record<string, unknown> | undefined;
|
|
42
42
|
}, {
|
|
43
|
-
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "timing";
|
|
43
|
+
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "csp_violation" | "timing";
|
|
44
44
|
timestamp: string;
|
|
45
45
|
durationMs?: number | undefined;
|
|
46
|
+
route?: string | undefined;
|
|
47
|
+
message?: string | undefined;
|
|
46
48
|
filename?: string | undefined;
|
|
47
49
|
id?: string | undefined;
|
|
48
|
-
message?: string | undefined;
|
|
49
50
|
severity?: "fatal" | "error" | "warning" | "info" | undefined;
|
|
50
51
|
name?: string | undefined;
|
|
51
52
|
stack?: string | undefined;
|
|
52
53
|
errorType?: string | undefined;
|
|
53
54
|
line?: number | undefined;
|
|
54
55
|
column?: number | undefined;
|
|
55
|
-
route?: string | undefined;
|
|
56
56
|
context?: Record<string, unknown> | undefined;
|
|
57
57
|
}>, "many">;
|
|
58
58
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -60,10 +60,11 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
60
60
|
environment: string;
|
|
61
61
|
version: 1;
|
|
62
62
|
events: {
|
|
63
|
-
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "timing";
|
|
63
|
+
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "csp_violation" | "timing";
|
|
64
64
|
message: string;
|
|
65
65
|
timestamp: string;
|
|
66
66
|
durationMs?: number | undefined;
|
|
67
|
+
route?: string | undefined;
|
|
67
68
|
filename?: string | undefined;
|
|
68
69
|
id?: string | undefined;
|
|
69
70
|
severity?: "fatal" | "error" | "warning" | "info" | undefined;
|
|
@@ -72,7 +73,6 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
72
73
|
errorType?: string | undefined;
|
|
73
74
|
line?: number | undefined;
|
|
74
75
|
column?: number | undefined;
|
|
75
|
-
route?: string | undefined;
|
|
76
76
|
context?: Record<string, unknown> | undefined;
|
|
77
77
|
}[];
|
|
78
78
|
release?: string | undefined;
|
|
@@ -82,19 +82,19 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
82
82
|
environment: string;
|
|
83
83
|
version: 1;
|
|
84
84
|
events: {
|
|
85
|
-
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "timing";
|
|
85
|
+
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "csp_violation" | "timing";
|
|
86
86
|
timestamp: string;
|
|
87
87
|
durationMs?: number | undefined;
|
|
88
|
+
route?: string | undefined;
|
|
89
|
+
message?: string | undefined;
|
|
88
90
|
filename?: string | undefined;
|
|
89
91
|
id?: string | undefined;
|
|
90
|
-
message?: string | undefined;
|
|
91
92
|
severity?: "fatal" | "error" | "warning" | "info" | undefined;
|
|
92
93
|
name?: string | undefined;
|
|
93
94
|
stack?: string | undefined;
|
|
94
95
|
errorType?: string | undefined;
|
|
95
96
|
line?: number | undefined;
|
|
96
97
|
column?: number | undefined;
|
|
97
|
-
route?: string | undefined;
|
|
98
98
|
context?: Record<string, unknown> | undefined;
|
|
99
99
|
}[];
|
|
100
100
|
release?: string | undefined;
|
|
@@ -17,6 +17,7 @@ const browserEventSchema = z.object({
|
|
|
17
17
|
"track_event",
|
|
18
18
|
"outcome",
|
|
19
19
|
"request_failure",
|
|
20
|
+
"csp_violation",
|
|
20
21
|
"timing",
|
|
21
22
|
]),
|
|
22
23
|
timestamp: z.string().datetime(),
|
|
@@ -49,6 +50,7 @@ const TYPE_TO_ERROR_TYPE = {
|
|
|
49
50
|
message: "Message",
|
|
50
51
|
outcome: "OutcomeFailure",
|
|
51
52
|
request_failure: "HttpRequestError",
|
|
53
|
+
csp_violation: "CspViolation",
|
|
52
54
|
};
|
|
53
55
|
// Content-level gate for the free-form `context` bag. The schema whitelist
|
|
54
56
|
// above is structural; this masks obvious PII/secrets inside whatever a
|
|
@@ -78,6 +80,7 @@ const TYPE_TO_SEVERITY = {
|
|
|
78
80
|
message: "warning",
|
|
79
81
|
outcome: "error",
|
|
80
82
|
request_failure: "error",
|
|
83
|
+
csp_violation: "error",
|
|
81
84
|
};
|
|
82
85
|
export function normalizeBrowserPayload(payload) {
|
|
83
86
|
const occurrences = [];
|
package/dist/normalize-otlp.d.ts
CHANGED
|
@@ -66,12 +66,16 @@ export interface OtlpMetricsRequest {
|
|
|
66
66
|
metrics?: Array<{
|
|
67
67
|
name?: string;
|
|
68
68
|
unit?: string;
|
|
69
|
+
gauge?: {
|
|
70
|
+
dataPoints?: OtlpDataPoint[];
|
|
71
|
+
};
|
|
69
72
|
histogram?: {
|
|
70
73
|
dataPoints?: OtlpDataPoint[];
|
|
71
74
|
aggregationTemporality?: string | number;
|
|
72
75
|
};
|
|
73
76
|
sum?: {
|
|
74
77
|
dataPoints?: OtlpDataPoint[];
|
|
78
|
+
aggregationTemporality?: string | number;
|
|
75
79
|
};
|
|
76
80
|
}>;
|
|
77
81
|
}>;
|
package/dist/normalize-otlp.js
CHANGED
|
@@ -96,6 +96,7 @@ function severityOf(eventAttrs, spanAttrs) {
|
|
|
96
96
|
const MAX_SPANS_PER_REQUEST = 5000;
|
|
97
97
|
export function normalizeTraces(request) {
|
|
98
98
|
const occurrences = [];
|
|
99
|
+
const occurrenceSpanIds = [];
|
|
99
100
|
const spans = [];
|
|
100
101
|
const llmCalls = [];
|
|
101
102
|
const rollups = new Map();
|
|
@@ -143,6 +144,7 @@ export function normalizeTraces(request) {
|
|
|
143
144
|
for (const event of exceptionEvents) {
|
|
144
145
|
const eventAttrs = attrMap(event.attributes);
|
|
145
146
|
const handled = eventAttrs.get("autter.handled") === "true";
|
|
147
|
+
occurrenceSpanIds.push(span.spanId ?? "");
|
|
146
148
|
occurrences.push({
|
|
147
149
|
source: "server",
|
|
148
150
|
severity: severityOf(eventAttrs, attrs),
|
|
@@ -175,6 +177,7 @@ export function normalizeTraces(request) {
|
|
|
175
177
|
for (const event of failedOutcomes) {
|
|
176
178
|
const outcome = attrMap(event.attributes);
|
|
177
179
|
const name = (outcome.get("autter.outcome.name") ?? "operation").replace(EMAIL_VALUE_RE, "[redacted]").slice(0, 200);
|
|
180
|
+
occurrenceSpanIds.push(span.spanId ?? "");
|
|
178
181
|
occurrences.push({
|
|
179
182
|
source: "server", severity: "error", service: resource.service,
|
|
180
183
|
environment: resource.environment, release: resource.release,
|
|
@@ -186,6 +189,7 @@ export function normalizeTraces(request) {
|
|
|
186
189
|
});
|
|
187
190
|
}
|
|
188
191
|
if (exceptionEvents.length === 0 && failedOutcomes.length === 0 && isError) {
|
|
192
|
+
occurrenceSpanIds.push(span.spanId ?? "");
|
|
189
193
|
occurrences.push({
|
|
190
194
|
source: "server",
|
|
191
195
|
severity: severityOf(new Map(), attrs),
|
|
@@ -270,6 +274,7 @@ export function normalizeTraces(request) {
|
|
|
270
274
|
}
|
|
271
275
|
}
|
|
272
276
|
}
|
|
277
|
+
inheritHttpFromParent(occurrences, occurrenceSpanIds, spans);
|
|
273
278
|
return {
|
|
274
279
|
occurrences,
|
|
275
280
|
spans,
|
|
@@ -278,6 +283,62 @@ export function normalizeTraces(request) {
|
|
|
278
283
|
spanCount,
|
|
279
284
|
};
|
|
280
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Exception spans are often children named "Error" with no HTTP attributes.
|
|
288
|
+
* Copy method, route, status, and duration from the server span in the same
|
|
289
|
+
* trace so the issue can name the API that failed.
|
|
290
|
+
*/
|
|
291
|
+
function inheritHttpFromParent(occurrences, occurrenceSpanIds, spans) {
|
|
292
|
+
const byId = new Map();
|
|
293
|
+
for (const span of spans) {
|
|
294
|
+
if (span.spanId)
|
|
295
|
+
byId.set(span.spanId, span);
|
|
296
|
+
}
|
|
297
|
+
for (let i = 0; i < occurrences.length; i++) {
|
|
298
|
+
const occurrence = occurrences[i];
|
|
299
|
+
const spanId = occurrenceSpanIds[i];
|
|
300
|
+
if (!occurrence || !spanId)
|
|
301
|
+
continue;
|
|
302
|
+
const http = httpServerAncestor(byId, spanId);
|
|
303
|
+
if (!http)
|
|
304
|
+
continue;
|
|
305
|
+
if (!occurrence.route && http.route)
|
|
306
|
+
occurrence.route = http.route;
|
|
307
|
+
const method = methodFromSpan(http);
|
|
308
|
+
if (!occurrence.method && method)
|
|
309
|
+
occurrence.method = method;
|
|
310
|
+
if ((occurrence.statusCode == null || occurrence.statusCode === 0) &&
|
|
311
|
+
http.statusCode != null &&
|
|
312
|
+
http.statusCode > 0) {
|
|
313
|
+
occurrence.statusCode = http.statusCode;
|
|
314
|
+
}
|
|
315
|
+
if (http.durationMs > 0) {
|
|
316
|
+
const attributes = { ...(occurrence.attributes ?? {}) };
|
|
317
|
+
if (attributes["http.server.duration_ms"] == null) {
|
|
318
|
+
attributes["http.server.duration_ms"] = Math.round(http.durationMs);
|
|
319
|
+
occurrence.attributes = attributes;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function httpServerAncestor(byId, spanId) {
|
|
325
|
+
let current = byId.get(spanId) ?? null;
|
|
326
|
+
const seen = new Set();
|
|
327
|
+
while (current && current.spanId && !seen.has(current.spanId)) {
|
|
328
|
+
seen.add(current.spanId);
|
|
329
|
+
if (current.kind === "server")
|
|
330
|
+
return current;
|
|
331
|
+
const parentId = current.parentSpanId;
|
|
332
|
+
if (!parentId)
|
|
333
|
+
return null;
|
|
334
|
+
current = byId.get(parentId) ?? null;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
function methodFromSpan(span) {
|
|
339
|
+
const raw = span.attributes?.["http.request.method"];
|
|
340
|
+
return typeof raw === "string" && raw.trim() ? raw.trim().toUpperCase().slice(0, 16) : null;
|
|
341
|
+
}
|
|
281
342
|
function minuteBucket(date) {
|
|
282
343
|
return new Date(Math.floor(date.getTime() / 60_000) * 60_000);
|
|
283
344
|
}
|
package/dist/otlp-proto.js
CHANGED
|
@@ -54,10 +54,12 @@ message ScopeMetrics { repeated Metric metrics = 2; }
|
|
|
54
54
|
message Metric {
|
|
55
55
|
string name = 1;
|
|
56
56
|
string unit = 3;
|
|
57
|
+
Gauge gauge = 5;
|
|
57
58
|
Sum sum = 7;
|
|
58
59
|
Histogram histogram = 9;
|
|
59
60
|
}
|
|
60
|
-
message
|
|
61
|
+
message Gauge { repeated NumberDataPoint data_points = 1; }
|
|
62
|
+
message Sum { repeated NumberDataPoint data_points = 1; int32 aggregation_temporality = 2; }
|
|
61
63
|
message NumberDataPoint {
|
|
62
64
|
fixed64 time_unix_nano = 3;
|
|
63
65
|
double as_double = 4;
|
package/dist/profiles.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface ProfileSample {
|
|
|
5
5
|
environment: string;
|
|
6
6
|
release: string;
|
|
7
7
|
traceId: string;
|
|
8
|
+
instanceId: string;
|
|
8
9
|
observedAt: Date;
|
|
9
10
|
sampleType: string;
|
|
10
11
|
unit: string;
|
|
@@ -17,4 +18,5 @@ export declare function decodeProfile(body: Buffer, meta: {
|
|
|
17
18
|
environment: string;
|
|
18
19
|
release: string;
|
|
19
20
|
traceId: string;
|
|
21
|
+
instanceId: string;
|
|
20
22
|
}): ProfileSample[];
|
package/dist/profiles.js
CHANGED
|
@@ -21,7 +21,7 @@ export function profileTableDDL(db) {
|
|
|
21
21
|
return `CREATE TABLE IF NOT EXISTS ${db}.runtime_profile_samples (
|
|
22
22
|
org_id String, repository_id String, profile_id String, sample_index UInt16,
|
|
23
23
|
service LowCardinality(String), environment LowCardinality(String), release String,
|
|
24
|
-
trace_id String DEFAULT '', observed_at DateTime64(3, 'UTC'),
|
|
24
|
+
trace_id String DEFAULT '', instance_id String DEFAULT '', observed_at DateTime64(3, 'UTC'),
|
|
25
25
|
sample_type LowCardinality(String), unit LowCardinality(String),
|
|
26
26
|
stack Array(String), value UInt64
|
|
27
27
|
) ENGINE = MergeTree PARTITION BY toDate(observed_at)
|
|
@@ -37,18 +37,21 @@ export function decodeProfile(body, meta) {
|
|
|
37
37
|
const strings = profile.stringTable ?? [];
|
|
38
38
|
const locations = new Map((profile.location ?? []).map((location) => [location.id, location]));
|
|
39
39
|
const functions = new Map((profile.function ?? []).map((fn) => [fn.id, fn]));
|
|
40
|
-
const
|
|
40
|
+
const typeIndex = Math.max(0, (profile.sampleType ?? []).findIndex((t) => strings[Number(t.type ?? 0)] === "inuse_space"));
|
|
41
|
+
const type = profile.sampleType?.[typeIndex];
|
|
41
42
|
const observedMs = Number(BigInt(profile.timeNanos ?? "0") / 1000000n);
|
|
42
43
|
const observedAt = observedMs > 0 && observedMs <= Date.now() + 300_000 ? new Date(observedMs) : new Date();
|
|
43
44
|
const profileId = createHash("sha256").update(body).digest("hex").slice(0, 32);
|
|
44
45
|
const samples = [];
|
|
45
46
|
for (const [sampleIndex, sample] of (profile.sample ?? []).slice(0, 1000).entries()) {
|
|
46
47
|
const stack = (sample.locationId ?? []).slice(0, 64).flatMap((id) => {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
48
|
+
const locLine = locations.get(id)?.line?.[0];
|
|
49
|
+
const fn = locLine?.functionId ? functions.get(locLine.functionId) : undefined;
|
|
50
|
+
const name = fn ? strings[Number(fn.name ?? 0)] : undefined;
|
|
51
|
+
const filename = fn ? strings[Number(fn.filename ?? 0)] : undefined;
|
|
52
|
+
return name ? [`${name.slice(0, 150)}${filename ? ` (${filename.slice(0, 300)}:${Number(locLine?.line ?? 0)})` : ""}`] : [];
|
|
50
53
|
});
|
|
51
|
-
const value = Number(sample.value?.[
|
|
54
|
+
const value = Number(sample.value?.[typeIndex] ?? 0);
|
|
52
55
|
if (!stack.length || !Number.isSafeInteger(value) || value <= 0)
|
|
53
56
|
continue;
|
|
54
57
|
samples.push({ profileId, sampleIndex, ...meta, observedAt,
|
package/dist/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { browserPayloadSchema, normalizeBrowserPayload, } from "./normalize-brow
|
|
|
7
7
|
import { normalizeMetrics, normalizeTraces, } from "./normalize-otlp.js";
|
|
8
8
|
import { decodeMetricsRequest, decodeTraceRequest } from "./otlp-proto.js";
|
|
9
9
|
import { decodeProfile } from "./profiles.js";
|
|
10
|
+
import { normalizeMemoryMetrics, normalizePlatformEvent, platformEventSchema } from "./memory.js";
|
|
10
11
|
import { validateSourceMap } from "./source-maps.js";
|
|
11
12
|
import { SinkForwarder } from "./sink.js";
|
|
12
13
|
export function createIngesterApp(config) {
|
|
@@ -156,13 +157,14 @@ export function createIngesterApp(config) {
|
|
|
156
157
|
const environment = req.header("x-autter-environment")?.trim() || "production";
|
|
157
158
|
const release = req.header("x-autter-release")?.trim() ?? "";
|
|
158
159
|
const traceId = req.header("x-autter-trace-id")?.trim() ?? "";
|
|
159
|
-
|
|
160
|
+
const instanceId = req.header("x-autter-instance-id")?.trim() ?? "";
|
|
161
|
+
if (!/^[a-zA-Z0-9._-]{1,200}$/.test(service) || environment.length > 100 || release.length > 200 || instanceId.length > 128 || (traceId && !/^[a-f0-9]{32}$/.test(traceId))) {
|
|
160
162
|
res.status(400).json({ error: "invalid profile metadata" });
|
|
161
163
|
return;
|
|
162
164
|
}
|
|
163
165
|
let samples;
|
|
164
166
|
try {
|
|
165
|
-
samples = decodeProfile(req.body, { service, environment, release, traceId });
|
|
167
|
+
samples = decodeProfile(req.body, { service, environment, release, traceId, instanceId });
|
|
166
168
|
if (!samples.length)
|
|
167
169
|
throw new Error("empty profile");
|
|
168
170
|
}
|
|
@@ -195,6 +197,29 @@ export function createIngesterApp(config) {
|
|
|
195
197
|
storageError(res, err);
|
|
196
198
|
}
|
|
197
199
|
});
|
|
200
|
+
/** ECS/Kubernetes event forwarders use the same server key as OTLP. */
|
|
201
|
+
app.post("/v1/platform-events", async (req, res) => {
|
|
202
|
+
const ctx = await authenticate(req, res, "otlp");
|
|
203
|
+
if (!ctx)
|
|
204
|
+
return;
|
|
205
|
+
const parsed = platformEventSchema.safeParse(req.body);
|
|
206
|
+
if (!parsed.success) {
|
|
207
|
+
res.status(400).json({ error: "invalid platform event" });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const event = normalizePlatformEvent(parsed.data);
|
|
211
|
+
if (!event) {
|
|
212
|
+
res.status(400).json({ error: "event timestamp outside retention window" });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await store.insertPlatformEvent(ctx, event);
|
|
217
|
+
res.status(202).json({ accepted: true });
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
storageError(res, err);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
198
223
|
app.post("/v1/traces", async (req, res) => {
|
|
199
224
|
const ctx = await authenticate(req, res, "otlp");
|
|
200
225
|
if (!ctx)
|
|
@@ -255,9 +280,11 @@ export function createIngesterApp(config) {
|
|
|
255
280
|
request = req.body;
|
|
256
281
|
}
|
|
257
282
|
const metricPoints = normalizeMetrics(request);
|
|
283
|
+
const memorySamples = normalizeMemoryMetrics(request);
|
|
258
284
|
try {
|
|
259
285
|
await store.insertLatencyHistograms(ctx, normalizeLatencyHistograms(request));
|
|
260
286
|
await store.insertMetricPoints(ctx, metricPoints);
|
|
287
|
+
await store.insertMemorySamples(ctx, memorySamples);
|
|
261
288
|
}
|
|
262
289
|
catch (err) {
|
|
263
290
|
storageError(res, err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autter/otlp-ingester",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
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",
|