@autter/otlp-ingester 1.2.0 → 1.3.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/dist/clickhouse.d.ts +2 -0
- package/dist/clickhouse.js +19 -0
- package/dist/latency.d.ts +17 -0
- package/dist/latency.js +68 -0
- package/dist/migrations.js +2 -0
- package/dist/normalize-browser.d.ts +8 -8
- package/dist/normalize-otlp.d.ts +3 -0
- package/dist/normalize-otlp.js +4 -2
- package/dist/otlp-proto.js +3 -0
- package/dist/server.js +2 -0
- package/package.json +1 -1
package/dist/clickhouse.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IngesterConfig } from "./config.js";
|
|
2
|
+
import { type LatencyHistogram } from "./latency.js";
|
|
2
3
|
import { type Migration } from "./migrations.js";
|
|
3
4
|
import type { IngestContext, RuntimeLlmCall, RuntimeMetricPoint, RuntimeOccurrence, RuntimeSpanRow } from "./types.js";
|
|
4
5
|
export declare class ClickHouseStore {
|
|
@@ -24,5 +25,6 @@ export declare class ClickHouseStore {
|
|
|
24
25
|
insertSpans(ctx: IngestContext, spans: RuntimeSpanRow[]): Promise<void>;
|
|
25
26
|
insertLlmCalls(ctx: IngestContext, calls: RuntimeLlmCall[]): Promise<void>;
|
|
26
27
|
insertMetricPoints(ctx: IngestContext, points: RuntimeMetricPoint[]): Promise<void>;
|
|
28
|
+
insertLatencyHistograms(ctx: IngestContext, points: LatencyHistogram[]): Promise<void>;
|
|
27
29
|
close(): Promise<void>;
|
|
28
30
|
}
|
package/dist/clickhouse.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createClient, } from "@clickhouse/client";
|
|
2
|
+
import { latencyTableDDL } from "./latency.js";
|
|
2
3
|
import { MIGRATIONS, migrationsTableDDL, } from "./migrations.js";
|
|
3
4
|
/**
|
|
4
5
|
* ClickHouse persistence. The schema is idempotent and applied lazily once
|
|
@@ -44,6 +45,7 @@ export class ClickHouseStore {
|
|
|
44
45
|
const { occurrenceTtlDays, spanTtlDays, metricsTtlDays, llmCallTtlDays } = this.config;
|
|
45
46
|
return [
|
|
46
47
|
`CREATE DATABASE IF NOT EXISTS ${db}`,
|
|
48
|
+
latencyTableDDL(db, metricsTtlDays),
|
|
47
49
|
`CREATE TABLE IF NOT EXISTS ${db}.runtime_error_occurrences (
|
|
48
50
|
org_id String,
|
|
49
51
|
repository_id String,
|
|
@@ -312,6 +314,23 @@ export class ClickHouseStore {
|
|
|
312
314
|
})),
|
|
313
315
|
});
|
|
314
316
|
}
|
|
317
|
+
async insertLatencyHistograms(ctx, points) {
|
|
318
|
+
if (points.length === 0 || !this.configured)
|
|
319
|
+
return;
|
|
320
|
+
await this.ensureSchema();
|
|
321
|
+
await this.getClient().insert({
|
|
322
|
+
table: this.table("runtime_latency_histograms"),
|
|
323
|
+
format: "JSONEachRow",
|
|
324
|
+
clickhouse_settings: INSERT_SETTINGS,
|
|
325
|
+
values: points.map((point) => ({
|
|
326
|
+
org_id: ctx.orgId, repository_id: ctx.repositoryId, point_id: point.pointId,
|
|
327
|
+
service: point.service, environment: point.environment, release: point.release,
|
|
328
|
+
method: point.method, route: point.route, bucket_at: point.bucketAt.toISOString(),
|
|
329
|
+
request_count: point.requestCount, error_count: point.errorCount,
|
|
330
|
+
duration_sum_ms: point.durationSumMs, bounds_ms: point.boundsMs, counts: point.counts,
|
|
331
|
+
})),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
315
334
|
async close() {
|
|
316
335
|
if (!this.client)
|
|
317
336
|
return;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OtlpMetricsRequest } from "./normalize-otlp.js";
|
|
2
|
+
export interface LatencyHistogram {
|
|
3
|
+
pointId: string;
|
|
4
|
+
service: string;
|
|
5
|
+
environment: string;
|
|
6
|
+
release: string;
|
|
7
|
+
method: string;
|
|
8
|
+
route: string;
|
|
9
|
+
bucketAt: Date;
|
|
10
|
+
requestCount: number;
|
|
11
|
+
errorCount: number;
|
|
12
|
+
durationSumMs: number;
|
|
13
|
+
boundsMs: number[];
|
|
14
|
+
counts: number[];
|
|
15
|
+
}
|
|
16
|
+
export declare function latencyTableDDL(database: string, ttlDays?: number): string;
|
|
17
|
+
export declare function normalizeLatencyHistograms(request: OtlpMetricsRequest): LatencyHistogram[];
|
package/dist/latency.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { normalizeRoute } from "./fingerprint.js";
|
|
3
|
+
export function latencyTableDDL(database, ttlDays = 30) {
|
|
4
|
+
return `CREATE TABLE IF NOT EXISTS ${database}.runtime_latency_histograms (
|
|
5
|
+
org_id String, repository_id String, point_id String,
|
|
6
|
+
service LowCardinality(String), environment LowCardinality(String),
|
|
7
|
+
release String, method LowCardinality(String), route String,
|
|
8
|
+
bucket_at DateTime('UTC'), request_count UInt64, error_count UInt64,
|
|
9
|
+
duration_sum_ms Float64, bounds_ms Array(Float64), counts Array(UInt64)
|
|
10
|
+
) ENGINE = ReplacingMergeTree
|
|
11
|
+
PARTITION BY toDate(bucket_at)
|
|
12
|
+
ORDER BY (org_id, repository_id, service, environment, bucket_at, point_id)
|
|
13
|
+
TTL bucket_at + INTERVAL ${Math.max(1, Math.round(ttlDays))} DAY`;
|
|
14
|
+
}
|
|
15
|
+
export function normalizeLatencyHistograms(request) {
|
|
16
|
+
const points = [];
|
|
17
|
+
for (const resource of request.resourceMetrics ?? []) {
|
|
18
|
+
const hasStableDuration = resource.scopeMetrics?.some((scope) => scope.metrics?.some((metric) => metric.name === "http.server.request.duration"));
|
|
19
|
+
const resourceAttributes = Object.fromEntries((resource.resource?.attributes ?? [])
|
|
20
|
+
.map((attribute) => [attribute.key ?? "", attribute.value?.stringValue ?? ""]));
|
|
21
|
+
for (const scope of resource.scopeMetrics ?? []) {
|
|
22
|
+
for (const metric of scope.metrics ?? []) {
|
|
23
|
+
if (hasStableDuration && metric.name === "http.server.duration")
|
|
24
|
+
continue;
|
|
25
|
+
const multiplier = metric.name === "http.server.request.duration" ? 1000
|
|
26
|
+
: metric.name === "http.server.duration" ? 1 : null;
|
|
27
|
+
if (multiplier === null || ![2, "2", "AGGREGATION_TEMPORALITY_DELTA"].includes(metric.histogram?.aggregationTemporality ?? ""))
|
|
28
|
+
continue;
|
|
29
|
+
for (const point of metric.histogram?.dataPoints ?? []) {
|
|
30
|
+
const bounds = point.explicitBounds ?? [];
|
|
31
|
+
const counts = (point.bucketCounts ?? []).map(Number);
|
|
32
|
+
const count = Number(point.count);
|
|
33
|
+
const endMs = Number(point.timeUnixNano) / 1e6;
|
|
34
|
+
const startMs = Number(point.startTimeUnixNano) / 1e6;
|
|
35
|
+
if (!Number.isSafeInteger(count) || count <= 0 || !Number.isFinite(endMs)
|
|
36
|
+
|| !Number.isFinite(startMs) || endMs <= startMs || endMs - startMs > 120_000
|
|
37
|
+
|| !Number.isFinite(point.sum) || Number(point.sum) < 0
|
|
38
|
+
|| bounds.length === 0 || bounds.length > 256 || counts.length !== bounds.length + 1
|
|
39
|
+
|| bounds.some((bound, index) => !Number.isFinite(bound) || bound < 0 || (index > 0 && bound <= bounds[index - 1]))
|
|
40
|
+
|| counts.some((value) => !Number.isSafeInteger(value) || value < 0)
|
|
41
|
+
|| counts.reduce((total, value) => total + value, 0) !== count)
|
|
42
|
+
continue;
|
|
43
|
+
const attrs = Object.fromEntries((point.attributes ?? []).map((attribute) => [attribute.key ?? "",
|
|
44
|
+
String(attribute.value?.stringValue ?? attribute.value?.intValue ?? "")]));
|
|
45
|
+
const route = normalizeRoute(attrs["http.route"] ?? "");
|
|
46
|
+
const method = attrs["http.request.method"] ?? attrs["http.method"] ?? "";
|
|
47
|
+
if (!route || !method)
|
|
48
|
+
continue;
|
|
49
|
+
const status = Number(attrs["http.response.status_code"] ?? attrs["http.status_code"]);
|
|
50
|
+
points.push({
|
|
51
|
+
pointId: createHash("sha256").update(JSON.stringify([metric.name,
|
|
52
|
+
Object.entries(resourceAttributes).sort(), Object.entries(attrs).sort(),
|
|
53
|
+
String(point.startTimeUnixNano), String(point.timeUnixNano)])).digest("hex"),
|
|
54
|
+
service: resourceAttributes["service.name"] || "unknown",
|
|
55
|
+
environment: resourceAttributes["deployment.environment.name"] || resourceAttributes["deployment.environment"] || "production",
|
|
56
|
+
release: resourceAttributes["service.version"] || "",
|
|
57
|
+
method: method.slice(0, 20), route,
|
|
58
|
+
bucketAt: new Date(Math.floor(endMs / 60_000) * 60_000),
|
|
59
|
+
requestCount: count, errorCount: status >= 500 ? count : 0,
|
|
60
|
+
durationSumMs: Number(point.sum) * multiplier,
|
|
61
|
+
boundsMs: bounds.map((bound) => bound * multiplier), counts,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return points;
|
|
68
|
+
}
|
package/dist/migrations.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* ],
|
|
33
33
|
* },
|
|
34
34
|
*/
|
|
35
|
+
import { latencyTableDDL } from "./latency.js";
|
|
35
36
|
export const MIGRATIONS = [
|
|
36
37
|
// 0001 intentionally reserved as the baseline marker: databases created
|
|
37
38
|
// before the migration runner existed record it as applied without
|
|
@@ -113,6 +114,7 @@ export const MIGRATIONS = [
|
|
|
113
114
|
TTL toDateTime(started_at) + INTERVAL 90 DAY`,
|
|
114
115
|
],
|
|
115
116
|
},
|
|
117
|
+
{ id: "0005-latency-histograms", statements: [latencyTableDDL("{db}")] },
|
|
116
118
|
];
|
|
117
119
|
/** The tracking table itself — created by the runner before anything else. */
|
|
118
120
|
export function migrationsTableDDL(db) {
|
|
@@ -24,7 +24,7 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
24
24
|
route: z.ZodOptional<z.ZodString>;
|
|
25
25
|
context: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
26
26
|
}, "strip", z.ZodTypeAny, {
|
|
27
|
-
type: "
|
|
27
|
+
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event";
|
|
28
28
|
message: string;
|
|
29
29
|
timestamp: string;
|
|
30
30
|
id?: string | undefined;
|
|
@@ -38,7 +38,7 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
38
38
|
route?: string | undefined;
|
|
39
39
|
context?: Record<string, unknown> | undefined;
|
|
40
40
|
}, {
|
|
41
|
-
type: "
|
|
41
|
+
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event";
|
|
42
42
|
timestamp: string;
|
|
43
43
|
id?: string | undefined;
|
|
44
44
|
message?: string | undefined;
|
|
@@ -53,11 +53,11 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
53
53
|
context?: Record<string, unknown> | undefined;
|
|
54
54
|
}>, "many">;
|
|
55
55
|
}, "strip", z.ZodTypeAny, {
|
|
56
|
-
version: 1;
|
|
57
56
|
service: string;
|
|
58
57
|
environment: string;
|
|
58
|
+
version: 1;
|
|
59
59
|
events: {
|
|
60
|
-
type: "
|
|
60
|
+
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event";
|
|
61
61
|
message: string;
|
|
62
62
|
timestamp: string;
|
|
63
63
|
id?: string | undefined;
|
|
@@ -71,14 +71,14 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
71
71
|
route?: string | undefined;
|
|
72
72
|
context?: Record<string, unknown> | undefined;
|
|
73
73
|
}[];
|
|
74
|
-
sessionId?: string | undefined;
|
|
75
74
|
release?: string | undefined;
|
|
75
|
+
sessionId?: string | undefined;
|
|
76
76
|
}, {
|
|
77
|
-
version: 1;
|
|
78
77
|
service: string;
|
|
79
78
|
environment: string;
|
|
79
|
+
version: 1;
|
|
80
80
|
events: {
|
|
81
|
-
type: "
|
|
81
|
+
type: "exception" | "message" | "unhandled_rejection" | "session_start" | "track_event";
|
|
82
82
|
timestamp: string;
|
|
83
83
|
id?: string | undefined;
|
|
84
84
|
message?: string | undefined;
|
|
@@ -92,8 +92,8 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
92
92
|
route?: string | undefined;
|
|
93
93
|
context?: Record<string, unknown> | undefined;
|
|
94
94
|
}[];
|
|
95
|
-
sessionId?: string | undefined;
|
|
96
95
|
release?: string | undefined;
|
|
96
|
+
sessionId?: string | undefined;
|
|
97
97
|
}>;
|
|
98
98
|
export type BrowserPayload = z.infer<typeof browserPayloadSchema>;
|
|
99
99
|
export interface NormalizedBrowser {
|
package/dist/normalize-otlp.d.ts
CHANGED
|
@@ -50,7 +50,10 @@ export interface OtlpTraceRequest {
|
|
|
50
50
|
}
|
|
51
51
|
interface OtlpDataPoint {
|
|
52
52
|
attributes?: OtlpKeyValue[];
|
|
53
|
+
startTimeUnixNano?: string | number;
|
|
53
54
|
timeUnixNano?: string | number;
|
|
55
|
+
explicitBounds?: number[];
|
|
56
|
+
bucketCounts?: Array<string | number>;
|
|
54
57
|
count?: string | number;
|
|
55
58
|
sum?: number;
|
|
56
59
|
asInt?: string | number;
|
package/dist/normalize-otlp.js
CHANGED
|
@@ -124,10 +124,12 @@ export function normalizeTraces(request) {
|
|
|
124
124
|
name: span.name ?? "unnamed",
|
|
125
125
|
kind,
|
|
126
126
|
status: isError ? "error" : "ok",
|
|
127
|
-
route,
|
|
127
|
+
route: route ? normalizeRoute(route) : null,
|
|
128
128
|
statusCode,
|
|
129
129
|
durationMs,
|
|
130
|
-
attributes:
|
|
130
|
+
attributes: {
|
|
131
|
+
"http.request.method": (attrs.get("http.request.method") ?? attrs.get("http.method") ?? "").slice(0, 20),
|
|
132
|
+
},
|
|
131
133
|
startedAt,
|
|
132
134
|
});
|
|
133
135
|
// Error occurrences: one per exception event; if the span is
|
package/dist/otlp-proto.js
CHANGED
|
@@ -66,9 +66,12 @@ message NumberDataPoint {
|
|
|
66
66
|
}
|
|
67
67
|
message Histogram { repeated HistogramDataPoint data_points = 1; int32 aggregation_temporality = 2; }
|
|
68
68
|
message HistogramDataPoint {
|
|
69
|
+
fixed64 start_time_unix_nano = 2;
|
|
69
70
|
fixed64 time_unix_nano = 3;
|
|
70
71
|
fixed64 count = 4;
|
|
71
72
|
optional double sum = 5;
|
|
73
|
+
repeated fixed64 bucket_counts = 6;
|
|
74
|
+
repeated double explicit_bounds = 7;
|
|
72
75
|
repeated KeyValue attributes = 9;
|
|
73
76
|
}
|
|
74
77
|
`;
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { KeyResolver, RateLimiter } from "./auth.js";
|
|
3
3
|
import { ClickHouseStore } from "./clickhouse.js";
|
|
4
|
+
import { normalizeLatencyHistograms } from "./latency.js";
|
|
4
5
|
import { deriveFields, fingerprintOccurrence, occurrenceIdFor, } from "./fingerprint.js";
|
|
5
6
|
import { browserPayloadSchema, normalizeBrowserPayload, } from "./normalize-browser.js";
|
|
6
7
|
import { normalizeMetrics, normalizeTraces, } from "./normalize-otlp.js";
|
|
@@ -200,6 +201,7 @@ export function createIngesterApp(config) {
|
|
|
200
201
|
}
|
|
201
202
|
const metricPoints = normalizeMetrics(request);
|
|
202
203
|
try {
|
|
204
|
+
await store.insertLatencyHistograms(ctx, normalizeLatencyHistograms(request));
|
|
203
205
|
await store.insertMetricPoints(ctx, metricPoints);
|
|
204
206
|
}
|
|
205
207
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autter/otlp-ingester",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|