otelturbine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +453 -0
- package/dist/index.js +25966 -0
- package/index.ts +27 -0
- package/package.json +59 -0
- package/src/adapters/bun.ts +62 -0
- package/src/adapters/elysia.ts +35 -0
- package/src/compress/snappy.ts +9 -0
- package/src/core/Compat.ts +92 -0
- package/src/core/OtelTurbine.ts +121 -0
- package/src/core/Pipeline.ts +169 -0
- package/src/core/RemoteWriteConfig.ts +8 -0
- package/src/proto/writeRequest.ts +133 -0
- package/src/tests/bunHandler.test.ts +134 -0
- package/src/tests/compat.test.ts +120 -0
- package/src/tests/otlpToTimeSeries.test.ts +184 -0
- package/src/tests/pipeline.test.ts +140 -0
- package/src/tests/proto.test.ts +102 -0
- package/src/tests/schemaEngine.test.ts +222 -0
- package/src/tests/varint.test.ts +53 -0
- package/src/transform/SchemaEngine.ts +174 -0
- package/src/transform/otlpToTimeSeries.ts +156 -0
- package/src/types/otlp.ts +83 -0
- package/src/types/prometheus.ts +23 -0
- package/src/types/schema.ts +36 -0
- package/src/util/varint.ts +80 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts OTLP JSON metrics payload to Prometheus TimeSeries[].
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - gauge → 1 TimeSeries per data point
|
|
6
|
+
* - sum → 1 TimeSeries per data point
|
|
7
|
+
* - histogram → _bucket (cumulative) × (bounds+1) + _count + _sum per data point
|
|
8
|
+
*
|
|
9
|
+
* Resource attributes are merged into every series (dp attributes win on conflict).
|
|
10
|
+
* Labels are sorted alphabetically (Prometheus requirement).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { OtlpMetricsPayload, OtlpKeyValue, OtlpNumberDataPoint, OtlpHistogramDataPoint } from '../types/otlp.ts';
|
|
14
|
+
import type { TimeSeries, Label } from '../types/prometheus.ts';
|
|
15
|
+
|
|
16
|
+
/** Convert OTLP key-value attributes to a plain string map. */
|
|
17
|
+
function attrsToMap(attrs: OtlpKeyValue[] | undefined): Record<string, string> {
|
|
18
|
+
const map: Record<string, string> = {};
|
|
19
|
+
if (!attrs) return map;
|
|
20
|
+
for (const kv of attrs) {
|
|
21
|
+
const v = kv.value;
|
|
22
|
+
if (v.stringValue !== undefined) {
|
|
23
|
+
map[kv.key] = v.stringValue;
|
|
24
|
+
} else if (v.intValue !== undefined) {
|
|
25
|
+
map[kv.key] = String(v.intValue);
|
|
26
|
+
} else if (v.doubleValue !== undefined) {
|
|
27
|
+
map[kv.key] = String(v.doubleValue);
|
|
28
|
+
} else if (v.boolValue !== undefined) {
|
|
29
|
+
map[kv.key] = String(v.boolValue);
|
|
30
|
+
} else {
|
|
31
|
+
map[kv.key] = '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return map;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Convert timeUnixNano string to milliseconds as BigInt. */
|
|
38
|
+
function nanoToMs(timeUnixNano: string | undefined): bigint {
|
|
39
|
+
if (!timeUnixNano) return BigInt(Date.now());
|
|
40
|
+
return BigInt(timeUnixNano) / 1_000_000n;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build sorted Label[] from a merged attribute map + metric name. */
|
|
44
|
+
function buildLabels(name: string, merged: Record<string, string>): Label[] {
|
|
45
|
+
const labels: Label[] = [{ name: '__name__', value: name }];
|
|
46
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
47
|
+
labels.push({ name: k, value: v });
|
|
48
|
+
}
|
|
49
|
+
// Sort alphabetically by name (__name__ sorts before most names — fine)
|
|
50
|
+
labels.sort((a, b) => a.name.localeCompare(b.name));
|
|
51
|
+
return labels;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Merge resource attrs and data point attrs; dp wins on conflict. */
|
|
55
|
+
function mergeAttrs(
|
|
56
|
+
resourceAttrs: Record<string, string>,
|
|
57
|
+
dpAttrs: OtlpKeyValue[] | undefined
|
|
58
|
+
): Record<string, string> {
|
|
59
|
+
const dp = attrsToMap(dpAttrs);
|
|
60
|
+
return { ...resourceAttrs, ...dp };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function processNumberDataPoint(
|
|
64
|
+
name: string,
|
|
65
|
+
dp: OtlpNumberDataPoint,
|
|
66
|
+
resourceAttrs: Record<string, string>
|
|
67
|
+
): TimeSeries {
|
|
68
|
+
const merged = mergeAttrs(resourceAttrs, dp.attributes);
|
|
69
|
+
const labels = buildLabels(name, merged);
|
|
70
|
+
const timestamp = nanoToMs(dp.timeUnixNano);
|
|
71
|
+
let value: number;
|
|
72
|
+
if (dp.asDouble !== undefined) {
|
|
73
|
+
value = dp.asDouble;
|
|
74
|
+
} else if (dp.asInt !== undefined) {
|
|
75
|
+
value = Number(dp.asInt);
|
|
76
|
+
} else {
|
|
77
|
+
value = 0;
|
|
78
|
+
}
|
|
79
|
+
return { labels, samples: [{ value, timestamp }] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function processHistogramDataPoint(
|
|
83
|
+
name: string,
|
|
84
|
+
dp: OtlpHistogramDataPoint,
|
|
85
|
+
resourceAttrs: Record<string, string>
|
|
86
|
+
): TimeSeries[] {
|
|
87
|
+
const merged = mergeAttrs(resourceAttrs, dp.attributes);
|
|
88
|
+
const timestamp = nanoToMs(dp.timeUnixNano);
|
|
89
|
+
const series: TimeSeries[] = [];
|
|
90
|
+
|
|
91
|
+
// Bucket series: cumulative counts
|
|
92
|
+
const bounds = dp.explicitBounds ?? [];
|
|
93
|
+
const bucketCounts = dp.bucketCounts ?? [];
|
|
94
|
+
|
|
95
|
+
let cumulative = 0n;
|
|
96
|
+
for (let i = 0; i <= bounds.length; i++) {
|
|
97
|
+
const le = i < bounds.length ? String(bounds[i]) : '+Inf';
|
|
98
|
+
const count = bucketCounts[i] !== undefined ? BigInt(bucketCounts[i]!) : 0n;
|
|
99
|
+
cumulative += count;
|
|
100
|
+
const bucketLabels = buildLabels(`${name}_bucket`, { ...merged, le });
|
|
101
|
+
series.push({
|
|
102
|
+
labels: bucketLabels,
|
|
103
|
+
samples: [{ value: Number(cumulative), timestamp }],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// _count series
|
|
108
|
+
const countVal = dp.count !== undefined ? Number(dp.count) : 0;
|
|
109
|
+
series.push({
|
|
110
|
+
labels: buildLabels(`${name}_count`, merged),
|
|
111
|
+
samples: [{ value: countVal, timestamp }],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// _sum series
|
|
115
|
+
const sumVal = dp.sum !== undefined ? dp.sum : 0;
|
|
116
|
+
series.push({
|
|
117
|
+
labels: buildLabels(`${name}_sum`, merged),
|
|
118
|
+
samples: [{ value: sumVal, timestamp }],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return series;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Convert an OTLP metrics payload to an array of Prometheus TimeSeries.
|
|
126
|
+
*/
|
|
127
|
+
export function otlpToTimeSeries(payload: OtlpMetricsPayload): TimeSeries[] {
|
|
128
|
+
const result: TimeSeries[] = [];
|
|
129
|
+
|
|
130
|
+
for (const rm of payload.resourceMetrics) {
|
|
131
|
+
const resourceAttrs = attrsToMap(rm.resource?.attributes);
|
|
132
|
+
|
|
133
|
+
for (const sm of rm.scopeMetrics) {
|
|
134
|
+
for (const metric of sm.metrics) {
|
|
135
|
+
const name = metric.name;
|
|
136
|
+
|
|
137
|
+
if (metric.gauge) {
|
|
138
|
+
for (const dp of metric.gauge.dataPoints) {
|
|
139
|
+
result.push(processNumberDataPoint(name, dp, resourceAttrs));
|
|
140
|
+
}
|
|
141
|
+
} else if (metric.sum) {
|
|
142
|
+
for (const dp of metric.sum.dataPoints) {
|
|
143
|
+
result.push(processNumberDataPoint(name, dp, resourceAttrs));
|
|
144
|
+
}
|
|
145
|
+
} else if (metric.histogram) {
|
|
146
|
+
for (const dp of metric.histogram.dataPoints) {
|
|
147
|
+
result.push(...processHistogramDataPoint(name, dp, resourceAttrs));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Other metric types (ExponentialHistogram, Summary) not yet supported
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTLP JSON payload types (minimal subset used for metrics ingestion).
|
|
3
|
+
* Based on the OTLP specification for metrics export.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface OtlpKeyValue {
|
|
7
|
+
key: string;
|
|
8
|
+
value: OtlpAnyValue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OtlpAnyValue {
|
|
12
|
+
stringValue?: string;
|
|
13
|
+
intValue?: string | number;
|
|
14
|
+
doubleValue?: number;
|
|
15
|
+
boolValue?: boolean;
|
|
16
|
+
arrayValue?: { values: OtlpAnyValue[] };
|
|
17
|
+
kvlistValue?: { values: OtlpKeyValue[] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OtlpResource {
|
|
21
|
+
attributes?: OtlpKeyValue[];
|
|
22
|
+
droppedAttributesCount?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface OtlpNumberDataPoint {
|
|
26
|
+
attributes?: OtlpKeyValue[];
|
|
27
|
+
startTimeUnixNano?: string;
|
|
28
|
+
timeUnixNano?: string;
|
|
29
|
+
asDouble?: number;
|
|
30
|
+
asInt?: string | number;
|
|
31
|
+
exemplars?: unknown[];
|
|
32
|
+
flags?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OtlpHistogramDataPoint {
|
|
36
|
+
attributes?: OtlpKeyValue[];
|
|
37
|
+
startTimeUnixNano?: string;
|
|
38
|
+
timeUnixNano?: string;
|
|
39
|
+
count?: string | number;
|
|
40
|
+
sum?: number;
|
|
41
|
+
bucketCounts?: (string | number)[];
|
|
42
|
+
explicitBounds?: number[];
|
|
43
|
+
exemplars?: unknown[];
|
|
44
|
+
flags?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OtlpGauge {
|
|
48
|
+
dataPoints: OtlpNumberDataPoint[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface OtlpSum {
|
|
52
|
+
dataPoints: OtlpNumberDataPoint[];
|
|
53
|
+
aggregationTemporality?: number;
|
|
54
|
+
isMonotonic?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface OtlpHistogram {
|
|
58
|
+
dataPoints: OtlpHistogramDataPoint[];
|
|
59
|
+
aggregationTemporality?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface OtlpMetric {
|
|
63
|
+
name: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
unit?: string;
|
|
66
|
+
gauge?: OtlpGauge;
|
|
67
|
+
sum?: OtlpSum;
|
|
68
|
+
histogram?: OtlpHistogram;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface OtlpScopeMetrics {
|
|
72
|
+
scope?: { name?: string; version?: string };
|
|
73
|
+
metrics: OtlpMetric[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface OtlpResourceMetrics {
|
|
77
|
+
resource?: OtlpResource;
|
|
78
|
+
scopeMetrics: OtlpScopeMetrics[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface OtlpMetricsPayload {
|
|
82
|
+
resourceMetrics: OtlpResourceMetrics[];
|
|
83
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal Prometheus remote-write data structures.
|
|
3
|
+
* These map directly to the protobuf WriteRequest schema.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Label {
|
|
7
|
+
name: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Sample {
|
|
12
|
+
value: number;
|
|
13
|
+
timestamp: bigint; // milliseconds since epoch
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TimeSeries {
|
|
17
|
+
labels: Label[]; // must be sorted alphabetically by name
|
|
18
|
+
samples: Sample[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WriteRequest {
|
|
22
|
+
timeseries: TimeSeries[];
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing MetricSchema and internal CompiledSchema types.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** A label value pattern: RegExp for pattern match, string for exact match. */
|
|
6
|
+
export type LabelPattern = RegExp | string;
|
|
7
|
+
|
|
8
|
+
export interface MetricSchema {
|
|
9
|
+
name: string | RegExp;
|
|
10
|
+
labels?: { [labelName: string]: LabelPattern };
|
|
11
|
+
inject?: Record<string, string>;
|
|
12
|
+
maxLabels?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fast matcher that avoids regex overhead for common patterns.
|
|
17
|
+
* Compiled once at build() time.
|
|
18
|
+
*/
|
|
19
|
+
export type FastMatcher =
|
|
20
|
+
| { type: 'any' } // /.*/ — always true, zero cost
|
|
21
|
+
| { type: 'exact'; value: string } // /^foo$/ or string — equality check
|
|
22
|
+
| { type: 'regex'; re: RegExp } // general fallback
|
|
23
|
+
|
|
24
|
+
export interface CompiledSchema {
|
|
25
|
+
namePattern: RegExp;
|
|
26
|
+
/** Explicit label matchers (excludes "*"). */
|
|
27
|
+
labelMatchers: Map<string, FastMatcher>;
|
|
28
|
+
/** Wildcard matcher for unlisted labels. undefined = drop all unlisted. */
|
|
29
|
+
wildcardMatcher: FastMatcher | undefined;
|
|
30
|
+
inject: Record<string, string>;
|
|
31
|
+
/** Pre-computed entries array — avoids Object.entries() per series. */
|
|
32
|
+
injectEntries: Array<[string, string]>;
|
|
33
|
+
maxLabels: number | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type DefaultAction = 'pass' | 'drop';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LEB128 varint encoding for protobuf wire format.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Write a varint for a non-negative JS number (no BigInt overhead). */
|
|
6
|
+
export function writeIntVarint(buf: Uint8Array, offset: number, value: number): number {
|
|
7
|
+
let i = offset;
|
|
8
|
+
while (value > 0x7f) {
|
|
9
|
+
buf[i++] = (value & 0x7f) | 0x80;
|
|
10
|
+
value >>>= 7;
|
|
11
|
+
}
|
|
12
|
+
buf[i++] = value;
|
|
13
|
+
return i - offset;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Byte length of a non-negative JS number varint. */
|
|
17
|
+
export function intVarintSize(n: number): number {
|
|
18
|
+
if (n < 0x80) return 1;
|
|
19
|
+
if (n < 0x4000) return 2;
|
|
20
|
+
if (n < 0x200000) return 3;
|
|
21
|
+
if (n < 0x10000000) return 4;
|
|
22
|
+
return 5; // up to ~4 GB, sufficient for proto field sizes
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* LEB128 varint encoding for protobuf wire format.
|
|
27
|
+
* Encodes a BigInt as a variable-length integer into a Uint8Array.
|
|
28
|
+
*/
|
|
29
|
+
export function encodeVarint(value: bigint): Uint8Array {
|
|
30
|
+
if (value < 0n) {
|
|
31
|
+
// For negative numbers, encode as 64-bit two's complement (10 bytes)
|
|
32
|
+
value = BigInt.asUintN(64, value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const bytes: number[] = [];
|
|
36
|
+
do {
|
|
37
|
+
let byte = Number(value & 0x7fn);
|
|
38
|
+
value >>= 7n;
|
|
39
|
+
if (value !== 0n) {
|
|
40
|
+
byte |= 0x80;
|
|
41
|
+
}
|
|
42
|
+
bytes.push(byte);
|
|
43
|
+
} while (value !== 0n);
|
|
44
|
+
|
|
45
|
+
return new Uint8Array(bytes);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Write a varint into a pre-allocated buffer at the given offset.
|
|
50
|
+
* Returns the number of bytes written.
|
|
51
|
+
*/
|
|
52
|
+
export function writeVarint(buf: Uint8Array, offset: number, value: bigint): number {
|
|
53
|
+
if (value < 0n) {
|
|
54
|
+
value = BigInt.asUintN(64, value);
|
|
55
|
+
}
|
|
56
|
+
let i = offset;
|
|
57
|
+
do {
|
|
58
|
+
let byte = Number(value & 0x7fn);
|
|
59
|
+
value >>= 7n;
|
|
60
|
+
if (value !== 0n) {
|
|
61
|
+
byte |= 0x80;
|
|
62
|
+
}
|
|
63
|
+
buf[i++] = byte;
|
|
64
|
+
} while (value !== 0n);
|
|
65
|
+
return i - offset;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Calculate the byte length of a varint encoding without allocating.
|
|
70
|
+
*/
|
|
71
|
+
export function varintSize(value: bigint): number {
|
|
72
|
+
if (value < 0n) value = BigInt.asUintN(64, value);
|
|
73
|
+
if (value === 0n) return 1;
|
|
74
|
+
let size = 0;
|
|
75
|
+
while (value > 0n) {
|
|
76
|
+
size++;
|
|
77
|
+
value >>= 7n;
|
|
78
|
+
}
|
|
79
|
+
return size;
|
|
80
|
+
}
|