@autter/otlp-ingester 1.3.3 → 1.4.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 +15 -0
- package/dist/context.d.ts +19 -0
- package/dist/context.js +92 -0
- package/dist/logs.d.ts +42 -0
- package/dist/logs.js +126 -0
- package/dist/memory.d.ts +2 -2
- package/dist/migrations.js +2 -0
- package/dist/normalize-browser.d.ts +9 -9
- package/dist/normalize-browser.js +3 -0
- package/dist/normalize-otlp.d.ts +2 -6
- package/dist/normalize-otlp.js +69 -7
- package/dist/otlp-proto.d.ts +2 -0
- package/dist/otlp-proto.js +21 -0
- package/dist/server.js +25 -1
- package/package.json +1 -1
package/dist/clickhouse.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { IngesterConfig } from "./config.js";
|
|
|
2
2
|
import { type LatencyHistogram } from "./latency.js";
|
|
3
3
|
import { type ProfileSample } from "./profiles.js";
|
|
4
4
|
import { type MemorySample, type PlatformEvent } from "./memory.js";
|
|
5
|
+
import { type RuntimeLogRecord } from "./logs.js";
|
|
5
6
|
import { type Migration } from "./migrations.js";
|
|
6
7
|
import type { IngestContext, RuntimeLlmCall, RuntimeMetricPoint, RuntimeOccurrence, RuntimeSpanRow } from "./types.js";
|
|
7
8
|
export declare class ClickHouseStore {
|
|
@@ -25,6 +26,7 @@ export declare class ClickHouseStore {
|
|
|
25
26
|
ping(): Promise<boolean>;
|
|
26
27
|
insertOccurrences(ctx: IngestContext, occurrences: RuntimeOccurrence[]): Promise<void>;
|
|
27
28
|
insertSpans(ctx: IngestContext, spans: RuntimeSpanRow[]): Promise<void>;
|
|
29
|
+
insertLogs(ctx: IngestContext, logs: RuntimeLogRecord[]): Promise<void>;
|
|
28
30
|
insertProfileSamples(ctx: IngestContext, samples: ProfileSample[]): Promise<void>;
|
|
29
31
|
insertMemorySamples(ctx: IngestContext, samples: MemorySample[]): Promise<void>;
|
|
30
32
|
insertPlatformEvent(ctx: IngestContext, event: PlatformEvent): Promise<void>;
|
package/dist/clickhouse.js
CHANGED
|
@@ -3,6 +3,7 @@ import { latencyTableDDL } from "./latency.js";
|
|
|
3
3
|
import { profileTableDDL } from "./profiles.js";
|
|
4
4
|
import { memoryTableDDL, platformEventTableDDL } from "./memory.js";
|
|
5
5
|
import { sourceMapTableDDL } from "./source-maps.js";
|
|
6
|
+
import { logTableDDL } from "./logs.js";
|
|
6
7
|
import { MIGRATIONS, migrationsTableDDL, } from "./migrations.js";
|
|
7
8
|
/**
|
|
8
9
|
* ClickHouse persistence. The schema is idempotent and applied lazily once
|
|
@@ -53,6 +54,7 @@ export class ClickHouseStore {
|
|
|
53
54
|
memoryTableDDL(db),
|
|
54
55
|
platformEventTableDDL(db),
|
|
55
56
|
sourceMapTableDDL(db),
|
|
57
|
+
logTableDDL(db),
|
|
56
58
|
`CREATE TABLE IF NOT EXISTS ${db}.runtime_error_occurrences (
|
|
57
59
|
org_id String,
|
|
58
60
|
repository_id String,
|
|
@@ -265,6 +267,19 @@ export class ClickHouseStore {
|
|
|
265
267
|
})),
|
|
266
268
|
});
|
|
267
269
|
}
|
|
270
|
+
async insertLogs(ctx, logs) {
|
|
271
|
+
if (!logs.length)
|
|
272
|
+
return;
|
|
273
|
+
if (!this.configured)
|
|
274
|
+
throw new Error("CLICKHOUSE_URL is not configured");
|
|
275
|
+
await this.ensureSchema();
|
|
276
|
+
await this.getClient().insert({ table: this.table("runtime_logs"), format: "JSONEachRow", clickhouse_settings: INSERT_SETTINGS,
|
|
277
|
+
values: logs.map((row) => ({ org_id: ctx.orgId, repository_id: ctx.repositoryId, event_id: row.id,
|
|
278
|
+
service: row.service, environment: row.environment, release: row.release, trace_id: row.traceId,
|
|
279
|
+
span_id: row.spanId, operation_id: row.operationId, operation: row.operation, event_type: row.type,
|
|
280
|
+
severity: row.severity, message: row.message, outcome: row.outcome, duration_ms: row.durationMs,
|
|
281
|
+
attributes: JSON.stringify(row.attributes), occurred_at: row.occurredAt.toISOString() })) });
|
|
282
|
+
}
|
|
268
283
|
async insertProfileSamples(ctx, samples) {
|
|
269
284
|
if (!samples.length)
|
|
270
285
|
return;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Bounded, defensive privacy boundary for custom telemetry from any OTLP SDK. */
|
|
2
|
+
export declare function sanitizeRuntimeContext(input: unknown): Record<string, unknown>;
|
|
3
|
+
export interface OtlpValue {
|
|
4
|
+
stringValue?: string;
|
|
5
|
+
intValue?: string | number;
|
|
6
|
+
doubleValue?: number;
|
|
7
|
+
boolValue?: boolean;
|
|
8
|
+
arrayValue?: {
|
|
9
|
+
values?: OtlpValue[];
|
|
10
|
+
};
|
|
11
|
+
kvlistValue?: {
|
|
12
|
+
values?: OtlpAttribute[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface OtlpAttribute {
|
|
16
|
+
key?: string;
|
|
17
|
+
value?: OtlpValue;
|
|
18
|
+
}
|
|
19
|
+
export declare function decodeOtlpAttributes(attributes: OtlpAttribute[] | undefined): Record<string, unknown>;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Bounded, defensive privacy boundary for custom telemetry from any OTLP SDK. */
|
|
2
|
+
export function sanitizeRuntimeContext(input) {
|
|
3
|
+
let budget = 512;
|
|
4
|
+
const seen = new WeakSet();
|
|
5
|
+
const scrub = (value) => value
|
|
6
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted]")
|
|
7
|
+
.replace(/\b(?:bearer\s+[A-Za-z0-9._~+\/=~-]{10,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|gh[pousr]_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|autter_(?:rt|pat)_[A-Za-z0-9_-]{10,}|(?:AKIA|ASIA)[A-Z0-9]{16})\b/gi, "[redacted]")
|
|
8
|
+
.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, "[redacted]")
|
|
9
|
+
.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi, "$1[redacted]@")
|
|
10
|
+
.replace(/(https?:\/\/[^\s?#]+)[?#][^\s]*/gi, "$1");
|
|
11
|
+
const visit = (value, key, depth) => {
|
|
12
|
+
if (--budget < 0 || depth > 6)
|
|
13
|
+
return "[truncated]";
|
|
14
|
+
if (/__proto__|constructor|prototype/i.test(key))
|
|
15
|
+
return undefined;
|
|
16
|
+
const usage = /(?:^|\.)(?:input|output|total|prompt|completion)_?tokens$/i.test(key) &&
|
|
17
|
+
typeof value === "number" &&
|
|
18
|
+
Number.isFinite(value) &&
|
|
19
|
+
value >= 0;
|
|
20
|
+
if (!usage &&
|
|
21
|
+
/password|passwd|secret|token|credential|authorization|cookie|email|phone|ssn|card[._-]?number|connection[._-]?string|api[._-]?key|private[._-]?key|request[._-]?body|response[._-]?body|headers|url[._-]?query/i.test(key))
|
|
22
|
+
return "[redacted]";
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
// Serialized custom context crosses the same privacy boundary as nested values.
|
|
25
|
+
if (/^\s*[\[{]/.test(value)) {
|
|
26
|
+
try {
|
|
27
|
+
return visit(JSON.parse(value), key, depth + 1);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* plain text */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const text = /(?:url|path|route|target)$/i.test(key)
|
|
34
|
+
? value.split(/[?#]/)[0]
|
|
35
|
+
: value;
|
|
36
|
+
return scrub(text).slice(0, /stack/i.test(key) ? 32000 : 2048);
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "number")
|
|
39
|
+
return Number.isFinite(value) ? value : undefined;
|
|
40
|
+
if (typeof value === "boolean" || value === null)
|
|
41
|
+
return value;
|
|
42
|
+
if (!value || typeof value !== "object")
|
|
43
|
+
return undefined;
|
|
44
|
+
if (seen.has(value))
|
|
45
|
+
return "[circular]";
|
|
46
|
+
seen.add(value);
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return value.slice(0, 64).map((v) => visit(v, key, depth + 1));
|
|
49
|
+
const result = {};
|
|
50
|
+
for (const [k, v] of Object.entries(value).slice(0, 128)) {
|
|
51
|
+
const safe = visit(v, k, depth + 1);
|
|
52
|
+
if (safe !== undefined)
|
|
53
|
+
result[k.slice(0, 200)] = safe;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
const result = visit(input, "", 0);
|
|
58
|
+
return result && typeof result === "object" && !Array.isArray(result)
|
|
59
|
+
? result
|
|
60
|
+
: {};
|
|
61
|
+
}
|
|
62
|
+
export function decodeOtlpAttributes(attributes) {
|
|
63
|
+
const decode = (v, depth = 0) => {
|
|
64
|
+
if (!v || depth > 6)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (v.stringValue !== undefined)
|
|
67
|
+
return v.stringValue;
|
|
68
|
+
if (v.intValue !== undefined)
|
|
69
|
+
return Number(v.intValue);
|
|
70
|
+
if (v.doubleValue !== undefined)
|
|
71
|
+
return v.doubleValue;
|
|
72
|
+
if (v.boolValue !== undefined)
|
|
73
|
+
return v.boolValue;
|
|
74
|
+
if (v.arrayValue)
|
|
75
|
+
return (v.arrayValue.values ?? [])
|
|
76
|
+
.slice(0, 64)
|
|
77
|
+
.map((value) => decode(value, depth + 1));
|
|
78
|
+
if (v.kvlistValue)
|
|
79
|
+
return Object.fromEntries((v.kvlistValue.values ?? [])
|
|
80
|
+
.slice(0, 128)
|
|
81
|
+
.filter((a) => a.key && !/^(?:__proto__|constructor|prototype)$/.test(a.key))
|
|
82
|
+
.map((a) => [a.key, decode(a.value, depth + 1)]));
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|
|
85
|
+
return sanitizeRuntimeContext(Object.fromEntries((attributes ?? [])
|
|
86
|
+
.slice()
|
|
87
|
+
.sort((a, b) => Number(/^autter\.(?:operation|event)\./.test(b.key ?? "")) -
|
|
88
|
+
Number(/^autter\.(?:operation|event)\./.test(a.key ?? "")))
|
|
89
|
+
.slice(0, 128)
|
|
90
|
+
.filter((a) => a.key && !/^(?:__proto__|constructor|prototype)$/.test(a.key))
|
|
91
|
+
.map((a) => [a.key, decode(a.value)])));
|
|
92
|
+
}
|
package/dist/logs.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type OtlpAttribute, type OtlpValue } from "./context.js";
|
|
2
|
+
export interface RuntimeLogRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
service: string;
|
|
5
|
+
environment: string;
|
|
6
|
+
release: string;
|
|
7
|
+
traceId: string;
|
|
8
|
+
spanId: string;
|
|
9
|
+
operationId: string;
|
|
10
|
+
operation: string;
|
|
11
|
+
type: "log" | "operation";
|
|
12
|
+
severity: string;
|
|
13
|
+
message: string;
|
|
14
|
+
outcome: string;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
attributes: Record<string, unknown>;
|
|
17
|
+
occurredAt: Date;
|
|
18
|
+
}
|
|
19
|
+
export interface OtlpLogsRequest {
|
|
20
|
+
resourceLogs?: Array<{
|
|
21
|
+
resource?: {
|
|
22
|
+
attributes?: OtlpAttribute[];
|
|
23
|
+
};
|
|
24
|
+
scopeLogs?: Array<{
|
|
25
|
+
scope?: {
|
|
26
|
+
name?: string;
|
|
27
|
+
};
|
|
28
|
+
logRecords?: Array<{
|
|
29
|
+
timeUnixNano?: string | number;
|
|
30
|
+
observedTimeUnixNano?: string | number;
|
|
31
|
+
severityNumber?: number;
|
|
32
|
+
severityText?: string;
|
|
33
|
+
traceId?: string;
|
|
34
|
+
spanId?: string;
|
|
35
|
+
body?: OtlpValue;
|
|
36
|
+
attributes?: OtlpAttribute[];
|
|
37
|
+
}>;
|
|
38
|
+
}>;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
export declare function normalizeLogs(request: OtlpLogsRequest): RuntimeLogRecord[];
|
|
42
|
+
export declare function logTableDDL(db: string): string;
|
package/dist/logs.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { decodeOtlpAttributes, sanitizeRuntimeContext, } from "./context.js";
|
|
3
|
+
import { normalizeRoute } from "./fingerprint.js";
|
|
4
|
+
export function normalizeLogs(request) {
|
|
5
|
+
if (!request || !Array.isArray(request.resourceLogs))
|
|
6
|
+
throw new Error("resourceLogs must be an array");
|
|
7
|
+
const rows = [];
|
|
8
|
+
if (request.resourceLogs.length > 128)
|
|
9
|
+
throw new Error("too many log resources");
|
|
10
|
+
for (const resource of request.resourceLogs) {
|
|
11
|
+
if (!resource ||
|
|
12
|
+
(resource.scopeLogs && !Array.isArray(resource.scopeLogs)) ||
|
|
13
|
+
(resource.scopeLogs?.length ?? 0) > 128)
|
|
14
|
+
throw new Error("invalid log scopes");
|
|
15
|
+
const info = decodeOtlpAttributes(resource.resource?.attributes);
|
|
16
|
+
for (const scope of resource.scopeLogs ?? []) {
|
|
17
|
+
if (!scope || (scope.logRecords && !Array.isArray(scope.logRecords)))
|
|
18
|
+
throw new Error("invalid log records");
|
|
19
|
+
for (const record of scope.logRecords ?? []) {
|
|
20
|
+
if (!record || typeof record !== "object")
|
|
21
|
+
throw new Error("invalid log record");
|
|
22
|
+
if (rows.length >= 2000)
|
|
23
|
+
throw new Error("too many log records");
|
|
24
|
+
const attrs = decodeOtlpAttributes(record.attributes);
|
|
25
|
+
let event = {};
|
|
26
|
+
const decodedBody = decodeOtlpAttributes([
|
|
27
|
+
{ key: "event", value: record.body },
|
|
28
|
+
]).event;
|
|
29
|
+
const body = typeof decodedBody === "string"
|
|
30
|
+
? decodedBody
|
|
31
|
+
: decodedBody == null
|
|
32
|
+
? ""
|
|
33
|
+
: JSON.stringify(decodedBody);
|
|
34
|
+
if (decodedBody &&
|
|
35
|
+
typeof decodedBody === "object" &&
|
|
36
|
+
!Array.isArray(decodedBody))
|
|
37
|
+
event = sanitizeRuntimeContext(decodedBody);
|
|
38
|
+
const metadata = Object.fromEntries(Object.entries(attrs).filter(([key]) => /^autter\.(?:operation|event)\./.test(key)));
|
|
39
|
+
const attributes = sanitizeRuntimeContext({
|
|
40
|
+
...metadata,
|
|
41
|
+
...event,
|
|
42
|
+
...attrs,
|
|
43
|
+
});
|
|
44
|
+
const nanos = record.timeUnixNano ?? record.observedTimeUnixNano;
|
|
45
|
+
const occurredAt = nanos === undefined
|
|
46
|
+
? new Date()
|
|
47
|
+
: new Date(Number(BigInt(String(nanos)) / 1000000n));
|
|
48
|
+
if (!Number.isFinite(occurredAt.getTime()))
|
|
49
|
+
throw new Error("invalid log timestamp");
|
|
50
|
+
const number = Number(record.severityNumber ?? 9);
|
|
51
|
+
const text = String(record.severityText ?? event.level ?? "").toLowerCase();
|
|
52
|
+
const severity = number >= 21 || text === "fatal"
|
|
53
|
+
? "fatal"
|
|
54
|
+
: number >= 17 || text === "error"
|
|
55
|
+
? "error"
|
|
56
|
+
: number >= 13 || /warn/.test(text)
|
|
57
|
+
? "warning"
|
|
58
|
+
: number < 9 || text === "debug"
|
|
59
|
+
? "debug"
|
|
60
|
+
: "info";
|
|
61
|
+
const errorMessage = event.error && typeof event.error === "object"
|
|
62
|
+
? event.error.message
|
|
63
|
+
: event.error;
|
|
64
|
+
const message = String(sanitizeRuntimeContext({
|
|
65
|
+
message: event.message ?? errorMessage ?? body,
|
|
66
|
+
}).message ?? "").slice(0, 4000);
|
|
67
|
+
const operationId = String(attributes["autter.operation.id"] ?? "").slice(0, 128);
|
|
68
|
+
const traceId = /^[a-f0-9]{32}$/i.test(record.traceId ?? "")
|
|
69
|
+
? record.traceId.toLowerCase().replace(/^0+$/, "")
|
|
70
|
+
: "";
|
|
71
|
+
const spanId = /^[a-f0-9]{16}$/i.test(record.spanId ?? "")
|
|
72
|
+
? record.spanId.toLowerCase().replace(/^0+$/, "")
|
|
73
|
+
: "";
|
|
74
|
+
if (typeof attributes.path === "string")
|
|
75
|
+
attributes.path = normalizeRoute(attributes.path.split(/[?#]/)[0]);
|
|
76
|
+
rows.push({
|
|
77
|
+
id: createHash("sha256")
|
|
78
|
+
.update(JSON.stringify([
|
|
79
|
+
info,
|
|
80
|
+
nanos,
|
|
81
|
+
traceId,
|
|
82
|
+
spanId,
|
|
83
|
+
severity,
|
|
84
|
+
attributes,
|
|
85
|
+
message,
|
|
86
|
+
]))
|
|
87
|
+
.digest("hex")
|
|
88
|
+
.slice(0, 32),
|
|
89
|
+
service: String(info["service.name"] ?? "unknown").slice(0, 200),
|
|
90
|
+
environment: String(info["deployment.environment.name"] ??
|
|
91
|
+
info["deployment.environment"] ??
|
|
92
|
+
"production").slice(0, 100),
|
|
93
|
+
release: String(info["service.version"] ?? "").slice(0, 200),
|
|
94
|
+
traceId,
|
|
95
|
+
spanId,
|
|
96
|
+
operationId,
|
|
97
|
+
operation: String(attributes["autter.operation.name"] ?? "").slice(0, 200),
|
|
98
|
+
type: attributes["autter.event.type"] === "operation"
|
|
99
|
+
? "operation"
|
|
100
|
+
: "log",
|
|
101
|
+
severity,
|
|
102
|
+
message,
|
|
103
|
+
outcome: String(attributes["autter.operation.outcome"] ?? "").slice(0, 30),
|
|
104
|
+
durationMs: Math.max(0, Number(attributes["autter.operation.duration_ms"]) || 0),
|
|
105
|
+
attributes,
|
|
106
|
+
occurredAt,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return rows;
|
|
112
|
+
}
|
|
113
|
+
export function logTableDDL(db) {
|
|
114
|
+
return `CREATE TABLE IF NOT EXISTS ${db}.runtime_logs (
|
|
115
|
+
org_id String, repository_id String, event_id String,
|
|
116
|
+
service LowCardinality(String), environment LowCardinality(String), release String DEFAULT '',
|
|
117
|
+
trace_id String DEFAULT '', span_id String DEFAULT '', operation_id String DEFAULT '',
|
|
118
|
+
operation String DEFAULT '', event_type LowCardinality(String) DEFAULT 'log',
|
|
119
|
+
severity LowCardinality(String) DEFAULT 'info', message String CODEC(ZSTD(1)),
|
|
120
|
+
outcome LowCardinality(String) DEFAULT '', duration_ms Float64 DEFAULT 0,
|
|
121
|
+
attributes String DEFAULT '{}' CODEC(ZSTD(1)), occurred_at DateTime64(3, 'UTC'),
|
|
122
|
+
ingested_at DateTime64(3, 'UTC') DEFAULT now64(3)
|
|
123
|
+
) ENGINE = ReplacingMergeTree PARTITION BY toDate(occurred_at)
|
|
124
|
+
ORDER BY (org_id, repository_id, occurred_at, event_id)
|
|
125
|
+
TTL toDateTime(occurred_at) + INTERVAL 14 DAY`;
|
|
126
|
+
}
|
package/dist/memory.d.ts
CHANGED
|
@@ -24,15 +24,15 @@ export declare const platformEventSchema: z.ZodObject<{
|
|
|
24
24
|
service: string;
|
|
25
25
|
environment: string;
|
|
26
26
|
release: string;
|
|
27
|
+
kind: "oom_kill" | "restart";
|
|
27
28
|
instanceId: string;
|
|
28
29
|
platform: "ecs" | "kubernetes" | "other";
|
|
29
|
-
kind: "oom_kill" | "restart";
|
|
30
30
|
occurredAt: string;
|
|
31
31
|
}, {
|
|
32
32
|
service: string;
|
|
33
|
+
kind: "oom_kill" | "restart";
|
|
33
34
|
instanceId: string;
|
|
34
35
|
platform: "ecs" | "kubernetes" | "other";
|
|
35
|
-
kind: "oom_kill" | "restart";
|
|
36
36
|
occurredAt: string;
|
|
37
37
|
environment?: string | undefined;
|
|
38
38
|
release?: string | undefined;
|
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 { logTableDDL } from "./logs.js";
|
|
38
39
|
import { memoryTableDDL, platformEventTableDDL } from "./memory.js";
|
|
39
40
|
export const MIGRATIONS = [
|
|
40
41
|
// 0001 intentionally reserved as the baseline marker: databases created
|
|
@@ -127,6 +128,7 @@ export const MIGRATIONS = [
|
|
|
127
128
|
{ id: "0010-memory-temporality", statements: [
|
|
128
129
|
`ALTER TABLE {db}.runtime_memory_samples ADD COLUMN IF NOT EXISTS temporality LowCardinality(String) DEFAULT 'gauge'`,
|
|
129
130
|
] },
|
|
131
|
+
{ id: "0011-runtime-logs", statements: [logTableDDL("{db}")] },
|
|
130
132
|
];
|
|
131
133
|
/** The tracking table itself — created by the runner before anything else. */
|
|
132
134
|
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,12 +38,12 @@ 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;
|
|
46
47
|
message?: string | undefined;
|
|
47
48
|
filename?: string | undefined;
|
|
48
49
|
id?: string | undefined;
|
|
@@ -52,7 +53,6 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
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,9 +82,10 @@ 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;
|
|
88
89
|
message?: string | undefined;
|
|
89
90
|
filename?: string | undefined;
|
|
90
91
|
id?: string | undefined;
|
|
@@ -94,7 +95,6 @@ export declare const browserPayloadSchema: z.ZodObject<{
|
|
|
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OtlpValue } from "./context.js";
|
|
1
2
|
import { type RuntimeLlmCall, type RuntimeMetricPoint, type RuntimeOccurrenceInput, type RuntimeSpanRow } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* OTLP/HTTP JSON → runtime signal. Structural types cover only the fields
|
|
@@ -10,12 +11,7 @@ import { type RuntimeLlmCall, type RuntimeMetricPoint, type RuntimeOccurrenceInp
|
|
|
10
11
|
*/
|
|
11
12
|
interface OtlpKeyValue {
|
|
12
13
|
key?: string;
|
|
13
|
-
value?:
|
|
14
|
-
stringValue?: string;
|
|
15
|
-
intValue?: string | number;
|
|
16
|
-
doubleValue?: number;
|
|
17
|
-
boolValue?: boolean;
|
|
18
|
-
};
|
|
14
|
+
value?: OtlpValue;
|
|
19
15
|
}
|
|
20
16
|
interface OtlpEvent {
|
|
21
17
|
name?: string;
|
package/dist/normalize-otlp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeOtlpAttributes, sanitizeRuntimeContext } from "./context.js";
|
|
1
2
|
import { normalizeRoute } from "./fingerprint.js";
|
|
2
3
|
import { extractLlmCall } from "./llm.js";
|
|
3
4
|
import { asSeverity, } from "./types.js";
|
|
@@ -96,6 +97,7 @@ function severityOf(eventAttrs, spanAttrs) {
|
|
|
96
97
|
const MAX_SPANS_PER_REQUEST = 5000;
|
|
97
98
|
export function normalizeTraces(request) {
|
|
98
99
|
const occurrences = [];
|
|
100
|
+
const occurrenceSpanIds = [];
|
|
99
101
|
const spans = [];
|
|
100
102
|
const llmCalls = [];
|
|
101
103
|
const rollups = new Map();
|
|
@@ -132,6 +134,7 @@ export function normalizeTraces(request) {
|
|
|
132
134
|
statusCode,
|
|
133
135
|
durationMs,
|
|
134
136
|
attributes: {
|
|
137
|
+
...decodeOtlpAttributes(span.attributes),
|
|
135
138
|
"http.request.method": (attrs.get("http.request.method") ?? attrs.get("http.method") ?? "").slice(0, 20),
|
|
136
139
|
},
|
|
137
140
|
startedAt,
|
|
@@ -143,6 +146,7 @@ export function normalizeTraces(request) {
|
|
|
143
146
|
for (const event of exceptionEvents) {
|
|
144
147
|
const eventAttrs = attrMap(event.attributes);
|
|
145
148
|
const handled = eventAttrs.get("autter.handled") === "true";
|
|
149
|
+
occurrenceSpanIds.push(span.spanId ?? "");
|
|
146
150
|
occurrences.push({
|
|
147
151
|
source: "server",
|
|
148
152
|
severity: severityOf(eventAttrs, attrs),
|
|
@@ -160,10 +164,8 @@ export function normalizeTraces(request) {
|
|
|
160
164
|
statusCode,
|
|
161
165
|
traceId: span.traceId ?? null,
|
|
162
166
|
sessionId: null,
|
|
163
|
-
attributes:
|
|
164
|
-
"autter.handled": true,
|
|
165
|
-
"autter.sampled": eventAttrs.get("autter.sampled") === "true",
|
|
166
|
-
} : null,
|
|
167
|
+
attributes: sanitizeRuntimeContext({ ...decodeOtlpAttributes(span.attributes), ...decodeOtlpAttributes(event.attributes),
|
|
168
|
+
...(handled ? { "autter.handled": true, "autter.sampled": eventAttrs.get("autter.sampled") === "true" } : {}) }),
|
|
167
169
|
occurredAt: nanosToDate(event.timeUnixNano ?? span.startTimeUnixNano),
|
|
168
170
|
});
|
|
169
171
|
}
|
|
@@ -175,17 +177,20 @@ 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,
|
|
181
184
|
errorType: "OutcomeFailure",
|
|
182
185
|
message: `${name}: ${(outcome.get("autter.outcome.message") ?? "failed").replace(EMAIL_VALUE_RE, "[redacted]").slice(0, 1000)}`,
|
|
183
|
-
stack: null, route, method: methodOf(attrs), statusCode,
|
|
184
|
-
traceId: span.traceId ?? null, sessionId: null,
|
|
186
|
+
stack: String(decodeOtlpAttributes(event.attributes)["autter.outcome.stack"] ?? "") || null, route, method: methodOf(attrs), statusCode,
|
|
187
|
+
traceId: span.traceId ?? null, sessionId: null,
|
|
188
|
+
attributes: sanitizeRuntimeContext({ ...decodeOtlpAttributes(span.attributes), ...decodeOtlpAttributes(event.attributes) }),
|
|
185
189
|
occurredAt: nanosToDate(event.timeUnixNano ?? span.startTimeUnixNano),
|
|
186
190
|
});
|
|
187
191
|
}
|
|
188
192
|
if (exceptionEvents.length === 0 && failedOutcomes.length === 0 && isError) {
|
|
193
|
+
occurrenceSpanIds.push(span.spanId ?? "");
|
|
189
194
|
occurrences.push({
|
|
190
195
|
source: "server",
|
|
191
196
|
severity: severityOf(new Map(), attrs),
|
|
@@ -202,7 +207,7 @@ export function normalizeTraces(request) {
|
|
|
202
207
|
statusCode,
|
|
203
208
|
traceId: span.traceId ?? null,
|
|
204
209
|
sessionId: null,
|
|
205
|
-
attributes:
|
|
210
|
+
attributes: decodeOtlpAttributes(span.attributes),
|
|
206
211
|
occurredAt: startedAt,
|
|
207
212
|
});
|
|
208
213
|
}
|
|
@@ -270,6 +275,7 @@ export function normalizeTraces(request) {
|
|
|
270
275
|
}
|
|
271
276
|
}
|
|
272
277
|
}
|
|
278
|
+
inheritHttpFromParent(occurrences, occurrenceSpanIds, spans);
|
|
273
279
|
return {
|
|
274
280
|
occurrences,
|
|
275
281
|
spans,
|
|
@@ -278,6 +284,62 @@ export function normalizeTraces(request) {
|
|
|
278
284
|
spanCount,
|
|
279
285
|
};
|
|
280
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Exception spans are often children named "Error" with no HTTP attributes.
|
|
289
|
+
* Copy method, route, status, and duration from the server span in the same
|
|
290
|
+
* trace so the issue can name the API that failed.
|
|
291
|
+
*/
|
|
292
|
+
function inheritHttpFromParent(occurrences, occurrenceSpanIds, spans) {
|
|
293
|
+
const byId = new Map();
|
|
294
|
+
for (const span of spans) {
|
|
295
|
+
if (span.spanId)
|
|
296
|
+
byId.set(span.spanId, span);
|
|
297
|
+
}
|
|
298
|
+
for (let i = 0; i < occurrences.length; i++) {
|
|
299
|
+
const occurrence = occurrences[i];
|
|
300
|
+
const spanId = occurrenceSpanIds[i];
|
|
301
|
+
if (!occurrence || !spanId)
|
|
302
|
+
continue;
|
|
303
|
+
const http = httpServerAncestor(byId, spanId);
|
|
304
|
+
if (!http)
|
|
305
|
+
continue;
|
|
306
|
+
if (!occurrence.route && http.route)
|
|
307
|
+
occurrence.route = http.route;
|
|
308
|
+
const method = methodFromSpan(http);
|
|
309
|
+
if (!occurrence.method && method)
|
|
310
|
+
occurrence.method = method;
|
|
311
|
+
if ((occurrence.statusCode == null || occurrence.statusCode === 0) &&
|
|
312
|
+
http.statusCode != null &&
|
|
313
|
+
http.statusCode > 0) {
|
|
314
|
+
occurrence.statusCode = http.statusCode;
|
|
315
|
+
}
|
|
316
|
+
if (http.durationMs > 0) {
|
|
317
|
+
const attributes = { ...(occurrence.attributes ?? {}) };
|
|
318
|
+
if (attributes["http.server.duration_ms"] == null) {
|
|
319
|
+
attributes["http.server.duration_ms"] = Math.round(http.durationMs);
|
|
320
|
+
occurrence.attributes = attributes;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function httpServerAncestor(byId, spanId) {
|
|
326
|
+
let current = byId.get(spanId) ?? null;
|
|
327
|
+
const seen = new Set();
|
|
328
|
+
while (current && current.spanId && !seen.has(current.spanId)) {
|
|
329
|
+
seen.add(current.spanId);
|
|
330
|
+
if (current.kind === "server")
|
|
331
|
+
return current;
|
|
332
|
+
const parentId = current.parentSpanId;
|
|
333
|
+
if (!parentId)
|
|
334
|
+
return null;
|
|
335
|
+
current = byId.get(parentId) ?? null;
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
function methodFromSpan(span) {
|
|
340
|
+
const raw = span.attributes?.["http.request.method"];
|
|
341
|
+
return typeof raw === "string" && raw.trim() ? raw.trim().toUpperCase().slice(0, 16) : null;
|
|
342
|
+
}
|
|
281
343
|
function minuteBucket(date) {
|
|
282
344
|
return new Date(Math.floor(date.getTime() / 60_000) * 60_000);
|
|
283
345
|
}
|
package/dist/otlp-proto.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { OtlpMetricsRequest, OtlpTraceRequest } from "./normalize-otlp.js";
|
|
2
|
+
import type { OtlpLogsRequest } from "./logs.js";
|
|
2
3
|
export declare function decodeTraceRequest(body: Buffer): OtlpTraceRequest;
|
|
3
4
|
export declare function decodeMetricsRequest(body: Buffer): OtlpMetricsRequest;
|
|
5
|
+
export declare function decodeLogsRequest(body: Buffer): OtlpLogsRequest;
|
package/dist/otlp-proto.js
CHANGED
|
@@ -21,10 +21,27 @@ message AnyValue {
|
|
|
21
21
|
bool bool_value = 2;
|
|
22
22
|
int64 int_value = 3;
|
|
23
23
|
double double_value = 4;
|
|
24
|
+
ArrayValue array_value = 5;
|
|
25
|
+
KeyValueList kvlist_value = 6;
|
|
24
26
|
}
|
|
25
27
|
}
|
|
28
|
+
message ArrayValue { repeated AnyValue values = 1; }
|
|
29
|
+
message KeyValueList { repeated KeyValue values = 1; }
|
|
26
30
|
message KeyValue { string key = 1; AnyValue value = 2; }
|
|
27
31
|
message Resource { repeated KeyValue attributes = 1; }
|
|
32
|
+
message ExportLogsServiceRequest { repeated ResourceLogs resource_logs = 1; }
|
|
33
|
+
message ResourceLogs { Resource resource = 1; repeated ScopeLogs scope_logs = 2; }
|
|
34
|
+
message ScopeLogs { repeated LogRecord log_records = 2; }
|
|
35
|
+
message LogRecord {
|
|
36
|
+
fixed64 time_unix_nano = 1;
|
|
37
|
+
int32 severity_number = 2;
|
|
38
|
+
string severity_text = 3;
|
|
39
|
+
AnyValue body = 5;
|
|
40
|
+
repeated KeyValue attributes = 6;
|
|
41
|
+
bytes trace_id = 9;
|
|
42
|
+
bytes span_id = 10;
|
|
43
|
+
fixed64 observed_time_unix_nano = 11;
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
message ExportTraceServiceRequest { repeated ResourceSpans resource_spans = 1; }
|
|
30
47
|
message ResourceSpans { Resource resource = 1; repeated ScopeSpans scope_spans = 2; }
|
|
@@ -80,6 +97,7 @@ message HistogramDataPoint {
|
|
|
80
97
|
const root = protobuf.parse(PROTO).root;
|
|
81
98
|
const TraceRequest = root.lookupType("otlp.ExportTraceServiceRequest");
|
|
82
99
|
const MetricsRequest = root.lookupType("otlp.ExportMetricsServiceRequest");
|
|
100
|
+
const LogsRequest = root.lookupType("otlp.ExportLogsServiceRequest");
|
|
83
101
|
const TO_OBJECT_OPTIONS = {
|
|
84
102
|
longs: String, // 64-bit ints → strings (matches OTLP/JSON)
|
|
85
103
|
defaults: false,
|
|
@@ -110,3 +128,6 @@ export function decodeMetricsRequest(body) {
|
|
|
110
128
|
const message = MetricsRequest.decode(body);
|
|
111
129
|
return hexifyIds(MetricsRequest.toObject(message, TO_OBJECT_OPTIONS));
|
|
112
130
|
}
|
|
131
|
+
export function decodeLogsRequest(body) {
|
|
132
|
+
return hexifyIds(LogsRequest.toObject(LogsRequest.decode(body), TO_OBJECT_OPTIONS));
|
|
133
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -5,7 +5,8 @@ import { normalizeLatencyHistograms } from "./latency.js";
|
|
|
5
5
|
import { deriveFields, fingerprintOccurrence, occurrenceIdFor, } from "./fingerprint.js";
|
|
6
6
|
import { browserPayloadSchema, normalizeBrowserPayload, } from "./normalize-browser.js";
|
|
7
7
|
import { normalizeMetrics, normalizeTraces, } from "./normalize-otlp.js";
|
|
8
|
-
import { decodeMetricsRequest, decodeTraceRequest } from "./otlp-proto.js";
|
|
8
|
+
import { decodeMetricsRequest, decodeTraceRequest, decodeLogsRequest } from "./otlp-proto.js";
|
|
9
|
+
import { normalizeLogs } from "./logs.js";
|
|
9
10
|
import { decodeProfile } from "./profiles.js";
|
|
10
11
|
import { normalizeMemoryMetrics, normalizePlatformEvent, platformEventSchema } from "./memory.js";
|
|
11
12
|
import { validateSourceMap } from "./source-maps.js";
|
|
@@ -262,6 +263,29 @@ export function createIngesterApp(config) {
|
|
|
262
263
|
sink?.enqueue(ctx, fingerprinted, metricPoints, llmCalls);
|
|
263
264
|
otlpSuccess(req, res);
|
|
264
265
|
});
|
|
266
|
+
app.post("/v1/logs", async (req, res) => {
|
|
267
|
+
const ctx = await authenticate(req, res, "otlp");
|
|
268
|
+
if (!ctx)
|
|
269
|
+
return;
|
|
270
|
+
let logs;
|
|
271
|
+
try {
|
|
272
|
+
logs = normalizeLogs(req.is("application/x-protobuf") ? decodeLogsRequest(req.body) : req.body);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
res.status(400).json({ error: "invalid OTLP logs payload" });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
await store.insertLogs(ctx, logs);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
storageError(res, err);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Logs are diagnostic evidence. Exceptions and failed outcomes use the trace sink,
|
|
286
|
+
// so one operation does not create duplicate issues through two export paths.
|
|
287
|
+
otlpSuccess(req, res);
|
|
288
|
+
});
|
|
265
289
|
app.post("/v1/metrics", async (req, res) => {
|
|
266
290
|
const ctx = await authenticate(req, res, "otlp");
|
|
267
291
|
if (!ctx)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autter/otlp-ingester",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|