@autter/otlp-ingester 1.3.2 → 1.3.3

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 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/JSON `ExportMetricsServiceRequest` | HTTP-server duration histograms → usage rollups |
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
 
@@ -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;
@@ -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({
@@ -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
+ instanceId: string;
28
+ platform: "ecs" | "kubernetes" | "other";
29
+ kind: "oom_kill" | "restart";
30
+ occurredAt: string;
31
+ }, {
32
+ service: string;
33
+ instanceId: string;
34
+ platform: "ecs" | "kubernetes" | "other";
35
+ kind: "oom_kill" | "restart";
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
+ }
@@ -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) {
@@ -43,9 +43,9 @@ export declare const browserPayloadSchema: z.ZodObject<{
43
43
  type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "timing";
44
44
  timestamp: string;
45
45
  durationMs?: number | undefined;
46
+ message?: string | undefined;
46
47
  filename?: string | undefined;
47
48
  id?: string | undefined;
48
- message?: string | undefined;
49
49
  severity?: "fatal" | "error" | "warning" | "info" | undefined;
50
50
  name?: string | undefined;
51
51
  stack?: string | undefined;
@@ -85,9 +85,9 @@ export declare const browserPayloadSchema: z.ZodObject<{
85
85
  type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event" | "outcome" | "request_failure" | "timing";
86
86
  timestamp: string;
87
87
  durationMs?: number | undefined;
88
+ message?: string | undefined;
88
89
  filename?: string | undefined;
89
90
  id?: string | undefined;
90
- message?: string | undefined;
91
91
  severity?: "fatal" | "error" | "warning" | "info" | undefined;
92
92
  name?: string | undefined;
93
93
  stack?: string | undefined;
@@ -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
  }>;
@@ -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 Sum { repeated NumberDataPoint data_points = 1; }
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;
@@ -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 type = profile.sampleType?.[0];
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 fnId = locations.get(id)?.line?.[0]?.functionId;
48
- const name = fnId ? strings[Number(functions.get(fnId)?.name ?? 0)] : undefined;
49
- return name ? [name.slice(0, 200)] : [];
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?.[0] ?? 0);
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
- if (!/^[a-zA-Z0-9._-]{1,200}$/.test(service) || environment.length > 100 || release.length > 200 || (traceId && !/^[a-f0-9]{32}$/.test(traceId))) {
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.2",
3
+ "version": "1.3.3",
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",