@grest-ts/metrics 0.0.5 → 0.0.7
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/LICENSE +21 -21
- package/README.md +45 -40
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/GGMetric.ts +124 -124
- package/src/GGMetricKey.ts +38 -38
- package/src/GGMetrics.ts +34 -34
- package/src/GGMetricsDefineStorage.ts +8 -8
- package/src/GGMetricsLoader.ts +21 -21
- package/src/GGMetricsStore.ts +26 -26
- package/src/exporters/GGJsonMetricsExporter.ts +176 -176
- package/src/exporters/GGMetricsExporter.ts +88 -88
- package/src/exporters/GGNestedMetricsExporter.ts +335 -335
- package/src/index-browser.ts +16 -16
- package/src/index-node.ts +21 -21
- package/src/keys/GGCounterKey.ts +29 -29
- package/src/keys/GGGaugeKey.ts +37 -37
- package/src/keys/GGHistogramKey.ts +37 -37
- package/src/keys/GGLazyGaugeKey.ts +36 -36
- package/src/metric/GGCounter.ts +19 -19
- package/src/metric/GGGauge.ts +38 -38
- package/src/metric/GGHistogram.ts +68 -68
- package/src/metric/GGLazyGauge.ts +31 -31
|
@@ -1,335 +1,335 @@
|
|
|
1
|
-
import {GGMetric} from "../GGMetric.js";
|
|
2
|
-
import {GGCounter} from "../metric/GGCounter.js";
|
|
3
|
-
import {GGGauge} from "../metric/GGGauge.js";
|
|
4
|
-
import {GGLazyGauge} from "../metric/GGLazyGauge.js";
|
|
5
|
-
import {GGHistogram, HistogramData} from "../metric/GGHistogram.js";
|
|
6
|
-
import {GGMetricKey} from "../GGMetricKey.js";
|
|
7
|
-
import {GGMetricsExporter, ExporterConfig} from "./GGMetricsExporter.js";
|
|
8
|
-
|
|
9
|
-
export type NestedValueConverter = (metric: GGMetric<any>, value: any, exporter: GGNestedMetricsExporter) => any;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Exports metrics in a nested, human-readable format.
|
|
13
|
-
* Groups metrics by their groupBy configuration and nests remaining labels.
|
|
14
|
-
*/
|
|
15
|
-
export class GGNestedMetricsExporter extends GGMetricsExporter<NestedMetricsOutput> {
|
|
16
|
-
|
|
17
|
-
// Static map for extensibility - register value converters for new metric types
|
|
18
|
-
private static converters = new Map<Function, NestedValueConverter>();
|
|
19
|
-
|
|
20
|
-
static {
|
|
21
|
-
GGNestedMetricsExporter.registerConverter(GGCounter, convertCounterValue);
|
|
22
|
-
GGNestedMetricsExporter.registerConverter(GGGauge, convertGaugeValue);
|
|
23
|
-
GGNestedMetricsExporter.registerConverter(GGLazyGauge, convertLazyGaugeValue);
|
|
24
|
-
GGNestedMetricsExporter.registerConverter(GGHistogram, convertHistogramValue);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Register a value converter for a custom metric type.
|
|
29
|
-
*/
|
|
30
|
-
static registerConverter(metricClass: Function, converter: NestedValueConverter): void {
|
|
31
|
-
GGNestedMetricsExporter.converters.set(metricClass, converter);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
constructor(config: ExporterConfig = {}) {
|
|
35
|
-
super(config);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
getMetrics(): NestedMetricsOutput {
|
|
39
|
-
const output: NestedMetricsOutput = {
|
|
40
|
-
timestamp: Date.now(),
|
|
41
|
-
groups: {}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// Collect all metric values with their parsed data
|
|
45
|
-
const allValues: MetricValueEntry[] = [];
|
|
46
|
-
|
|
47
|
-
for (const metric of this.getFilteredMetrics()) {
|
|
48
|
-
const entries = this.collectMetricValues(metric);
|
|
49
|
-
allValues.push(...entries);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Group by the groupBy key
|
|
53
|
-
const groupedByKey = new Map<string, MetricValueEntry[]>();
|
|
54
|
-
for (const entry of allValues) {
|
|
55
|
-
const existing = groupedByKey.get(entry.groupKey);
|
|
56
|
-
if (existing) {
|
|
57
|
-
existing.push(entry);
|
|
58
|
-
} else {
|
|
59
|
-
groupedByKey.set(entry.groupKey, [entry]);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Build the nested structure for each group
|
|
64
|
-
for (const [groupKey, entries] of groupedByKey) {
|
|
65
|
-
output.groups[groupKey] = this.buildGroupEntries(entries);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return output;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Parse a label key string into a labels object.
|
|
73
|
-
* Exposed for use by converters.
|
|
74
|
-
*/
|
|
75
|
-
parseLabels(labelKey: string): Record<string, string> {
|
|
76
|
-
if (!labelKey) return {};
|
|
77
|
-
const labels: Record<string, string> = {};
|
|
78
|
-
const parts = labelKey.split(',');
|
|
79
|
-
for (const part of parts) {
|
|
80
|
-
const [key, val] = part.split('=');
|
|
81
|
-
if (key && val !== undefined) {
|
|
82
|
-
labels[key] = val;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return labels;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private collectMetricValues(metric: GGMetric<any>): MetricValueEntry[] {
|
|
89
|
-
const entries: MetricValueEntry[] = [];
|
|
90
|
-
const key = metric.key as GGMetricKey<any>;
|
|
91
|
-
const groupByLabels = key.groupBy?.labels ?? [];
|
|
92
|
-
const metricName = this.getShortName(key);
|
|
93
|
-
|
|
94
|
-
for (const [labelKey, value] of metric.getValues()) {
|
|
95
|
-
const labels = this.parseLabels(labelKey);
|
|
96
|
-
|
|
97
|
-
// Compute groupBy key
|
|
98
|
-
const groupKey = this.computeGroupKey(key, labels);
|
|
99
|
-
|
|
100
|
-
// Compute remaining labels (not in groupBy)
|
|
101
|
-
const remainingLabels: Record<string, string> = {};
|
|
102
|
-
for (const [k, v] of Object.entries(labels)) {
|
|
103
|
-
if (!groupByLabels.includes(k)) {
|
|
104
|
-
remainingLabels[k] = v;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
entries.push({
|
|
109
|
-
groupKey,
|
|
110
|
-
metricName,
|
|
111
|
-
metricType: this.getMetricType(metric),
|
|
112
|
-
remainingLabels,
|
|
113
|
-
value: this.formatValue(metric, value)
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return entries;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private computeGroupKey(key: GGMetricKey<any>, labels: Record<string, string>): string {
|
|
121
|
-
const groupBy = key.groupBy;
|
|
122
|
-
if (!groupBy || groupBy.labels.length === 0) {
|
|
123
|
-
return key.root; // Use metric root as default group
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (groupBy.template) {
|
|
127
|
-
// Template string - replace {labelName} with values, missing values become empty string
|
|
128
|
-
return groupBy.template.replace(/\{(\w+)\}/g, (_, labelName) => {
|
|
129
|
-
return String(labels[labelName] ?? '');
|
|
130
|
-
});
|
|
131
|
-
} else {
|
|
132
|
-
// Default: join values with comma
|
|
133
|
-
return groupBy.labels.map(l => String(labels[l] ?? '')).join(',');
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private getShortName(key: GGMetricKey<any>): string {
|
|
138
|
-
// Remove the root prefix to get just the metric name
|
|
139
|
-
return key.name.replace(key.root, '');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private getMetricType(metric: GGMetric<any>): string {
|
|
143
|
-
return metric.constructor.name;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private formatValue(metric: GGMetric<any>, value: any): any {
|
|
147
|
-
const converter = GGNestedMetricsExporter.converters.get(metric.constructor);
|
|
148
|
-
if (!converter) {
|
|
149
|
-
// Fallback: return raw value
|
|
150
|
-
return value;
|
|
151
|
-
}
|
|
152
|
-
return converter(metric, value, this);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private buildGroupEntries(entries: MetricValueEntry[]): any[] {
|
|
156
|
-
// Find all unique "shared" label combinations
|
|
157
|
-
// Shared labels are those that exist in multiple metrics (like 'path')
|
|
158
|
-
// Non-shared labels are metric-specific (like 'result' only on counter)
|
|
159
|
-
|
|
160
|
-
// First, find which labels are common across all metrics in this group
|
|
161
|
-
const labelSets = new Map<string, Set<string>>();
|
|
162
|
-
for (const entry of entries) {
|
|
163
|
-
const labelNames = Object.keys(entry.remainingLabels);
|
|
164
|
-
const existing = labelSets.get(entry.metricName);
|
|
165
|
-
if (!existing) {
|
|
166
|
-
labelSets.set(entry.metricName, new Set(labelNames));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Find common labels (present in all metrics)
|
|
171
|
-
const allMetrics = [...labelSets.keys()];
|
|
172
|
-
let commonLabels: Set<string> = new Set();
|
|
173
|
-
if (allMetrics.length > 0) {
|
|
174
|
-
commonLabels = new Set(labelSets.get(allMetrics[0])!);
|
|
175
|
-
for (let i = 1; i < allMetrics.length; i++) {
|
|
176
|
-
const metricLabels = labelSets.get(allMetrics[i])!;
|
|
177
|
-
for (const label of commonLabels) {
|
|
178
|
-
if (!metricLabels.has(label)) {
|
|
179
|
-
commonLabels.delete(label);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Group entries by their common label values
|
|
186
|
-
const byCommonLabels = new Map<string, MetricValueEntry[]>();
|
|
187
|
-
for (const entry of entries) {
|
|
188
|
-
const commonKey = [...commonLabels]
|
|
189
|
-
.sort()
|
|
190
|
-
.map(l => `${l}=${entry.remainingLabels[l] ?? ''}`)
|
|
191
|
-
.join(',');
|
|
192
|
-
|
|
193
|
-
const existing = byCommonLabels.get(commonKey);
|
|
194
|
-
if (existing) {
|
|
195
|
-
existing.push(entry);
|
|
196
|
-
} else {
|
|
197
|
-
byCommonLabels.set(commonKey, [entry]);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Build output entries
|
|
202
|
-
const result: any[] = [];
|
|
203
|
-
for (const [_, groupEntries] of byCommonLabels) {
|
|
204
|
-
const outputEntry: Record<string, any> = {};
|
|
205
|
-
|
|
206
|
-
// Add common labels as direct properties
|
|
207
|
-
if (groupEntries.length > 0) {
|
|
208
|
-
for (const label of commonLabels) {
|
|
209
|
-
outputEntry[label] = groupEntries[0].remainingLabels[label];
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Group entries by metric name
|
|
214
|
-
const byMetric = new Map<string, MetricValueEntry[]>();
|
|
215
|
-
for (const entry of groupEntries) {
|
|
216
|
-
const existing = byMetric.get(entry.metricName);
|
|
217
|
-
if (existing) {
|
|
218
|
-
existing.push(entry);
|
|
219
|
-
} else {
|
|
220
|
-
byMetric.set(entry.metricName, [entry]);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Add each metric's values
|
|
225
|
-
for (const [metricName, metricEntries] of byMetric) {
|
|
226
|
-
// Get extra labels (not in commonLabels)
|
|
227
|
-
const extraLabels = Object.keys(metricEntries[0].remainingLabels)
|
|
228
|
-
.filter(l => !commonLabels.has(l));
|
|
229
|
-
|
|
230
|
-
if (extraLabels.length === 0) {
|
|
231
|
-
// No extra labels - value goes directly under metric name
|
|
232
|
-
outputEntry[metricName] = metricEntries[0].value;
|
|
233
|
-
} else {
|
|
234
|
-
// Has extra labels - nest by those label names
|
|
235
|
-
this.nestByLabels(outputEntry, metricName, metricEntries, extraLabels, 0);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
result.push(outputEntry);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return result;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private nestByLabels(
|
|
246
|
-
obj: Record<string, any>,
|
|
247
|
-
metricName: string,
|
|
248
|
-
entries: MetricValueEntry[],
|
|
249
|
-
extraLabels: string[],
|
|
250
|
-
labelIndex: number
|
|
251
|
-
): void {
|
|
252
|
-
const currentLabel = extraLabels[labelIndex];
|
|
253
|
-
const isLastLabel = labelIndex === extraLabels.length - 1;
|
|
254
|
-
|
|
255
|
-
// Group entries by current label value
|
|
256
|
-
const byLabelValue = new Map<string, MetricValueEntry[]>();
|
|
257
|
-
for (const entry of entries) {
|
|
258
|
-
const labelValue = entry.remainingLabels[currentLabel] ?? '';
|
|
259
|
-
const existing = byLabelValue.get(labelValue);
|
|
260
|
-
if (existing) {
|
|
261
|
-
existing.push(entry);
|
|
262
|
-
} else {
|
|
263
|
-
byLabelValue.set(labelValue, [entry]);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Create or get the label container
|
|
268
|
-
if (!obj[currentLabel]) {
|
|
269
|
-
obj[currentLabel] = {};
|
|
270
|
-
}
|
|
271
|
-
const labelContainer = obj[currentLabel];
|
|
272
|
-
|
|
273
|
-
// Add each label value
|
|
274
|
-
for (const [labelValue, valueEntries] of byLabelValue) {
|
|
275
|
-
if (isLastLabel) {
|
|
276
|
-
// Last label - add metric name and value
|
|
277
|
-
if (!labelContainer[labelValue]) {
|
|
278
|
-
labelContainer[labelValue] = {};
|
|
279
|
-
}
|
|
280
|
-
labelContainer[labelValue][metricName] = valueEntries[0].value;
|
|
281
|
-
} else {
|
|
282
|
-
// More labels to go - recurse
|
|
283
|
-
if (!labelContainer[labelValue]) {
|
|
284
|
-
labelContainer[labelValue] = {};
|
|
285
|
-
}
|
|
286
|
-
this.nestByLabels(labelContainer[labelValue], metricName, valueEntries, extraLabels, labelIndex + 1);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Built-in value converters
|
|
293
|
-
|
|
294
|
-
function convertCounterValue(_metric: GGCounter<any>, value: number, _exporter: GGNestedMetricsExporter): number {
|
|
295
|
-
return value;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function convertGaugeValue(_metric: GGGauge<any>, value: number, _exporter: GGNestedMetricsExporter): number {
|
|
299
|
-
return value;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function convertLazyGaugeValue(_metric: GGLazyGauge, value: number, _exporter: GGNestedMetricsExporter): number {
|
|
303
|
-
return value;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function convertHistogramValue(metric: GGHistogram<any>, value: HistogramData, _exporter: GGNestedMetricsExporter): any {
|
|
307
|
-
const buckets = metric.getBuckets();
|
|
308
|
-
const bucketObj: Record<string, number> = {};
|
|
309
|
-
for (let i = 0; i < buckets.length; i++) {
|
|
310
|
-
bucketObj[String(buckets[i])] = value.values[i] ?? 0;
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
count: value.count,
|
|
314
|
-
sum: value.sum,
|
|
315
|
-
avg: value.count > 0 ? value.sum / value.count : 0,
|
|
316
|
-
min: value.min === Infinity ? 0 : value.min,
|
|
317
|
-
max: value.max === -Infinity ? 0 : value.max,
|
|
318
|
-
buckets: bucketObj
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Types
|
|
323
|
-
|
|
324
|
-
interface MetricValueEntry {
|
|
325
|
-
groupKey: string;
|
|
326
|
-
metricName: string;
|
|
327
|
-
metricType: string;
|
|
328
|
-
remainingLabels: Record<string, string>;
|
|
329
|
-
value: any;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
export interface NestedMetricsOutput {
|
|
333
|
-
timestamp: number;
|
|
334
|
-
groups: Record<string, any[]>;
|
|
335
|
-
}
|
|
1
|
+
import {GGMetric} from "../GGMetric.js";
|
|
2
|
+
import {GGCounter} from "../metric/GGCounter.js";
|
|
3
|
+
import {GGGauge} from "../metric/GGGauge.js";
|
|
4
|
+
import {GGLazyGauge} from "../metric/GGLazyGauge.js";
|
|
5
|
+
import {GGHistogram, HistogramData} from "../metric/GGHistogram.js";
|
|
6
|
+
import {GGMetricKey} from "../GGMetricKey.js";
|
|
7
|
+
import {GGMetricsExporter, ExporterConfig} from "./GGMetricsExporter.js";
|
|
8
|
+
|
|
9
|
+
export type NestedValueConverter = (metric: GGMetric<any>, value: any, exporter: GGNestedMetricsExporter) => any;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Exports metrics in a nested, human-readable format.
|
|
13
|
+
* Groups metrics by their groupBy configuration and nests remaining labels.
|
|
14
|
+
*/
|
|
15
|
+
export class GGNestedMetricsExporter extends GGMetricsExporter<NestedMetricsOutput> {
|
|
16
|
+
|
|
17
|
+
// Static map for extensibility - register value converters for new metric types
|
|
18
|
+
private static converters = new Map<Function, NestedValueConverter>();
|
|
19
|
+
|
|
20
|
+
static {
|
|
21
|
+
GGNestedMetricsExporter.registerConverter(GGCounter, convertCounterValue);
|
|
22
|
+
GGNestedMetricsExporter.registerConverter(GGGauge, convertGaugeValue);
|
|
23
|
+
GGNestedMetricsExporter.registerConverter(GGLazyGauge, convertLazyGaugeValue);
|
|
24
|
+
GGNestedMetricsExporter.registerConverter(GGHistogram, convertHistogramValue);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register a value converter for a custom metric type.
|
|
29
|
+
*/
|
|
30
|
+
static registerConverter(metricClass: Function, converter: NestedValueConverter): void {
|
|
31
|
+
GGNestedMetricsExporter.converters.set(metricClass, converter);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
constructor(config: ExporterConfig = {}) {
|
|
35
|
+
super(config);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getMetrics(): NestedMetricsOutput {
|
|
39
|
+
const output: NestedMetricsOutput = {
|
|
40
|
+
timestamp: Date.now(),
|
|
41
|
+
groups: {}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Collect all metric values with their parsed data
|
|
45
|
+
const allValues: MetricValueEntry[] = [];
|
|
46
|
+
|
|
47
|
+
for (const metric of this.getFilteredMetrics()) {
|
|
48
|
+
const entries = this.collectMetricValues(metric);
|
|
49
|
+
allValues.push(...entries);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Group by the groupBy key
|
|
53
|
+
const groupedByKey = new Map<string, MetricValueEntry[]>();
|
|
54
|
+
for (const entry of allValues) {
|
|
55
|
+
const existing = groupedByKey.get(entry.groupKey);
|
|
56
|
+
if (existing) {
|
|
57
|
+
existing.push(entry);
|
|
58
|
+
} else {
|
|
59
|
+
groupedByKey.set(entry.groupKey, [entry]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build the nested structure for each group
|
|
64
|
+
for (const [groupKey, entries] of groupedByKey) {
|
|
65
|
+
output.groups[groupKey] = this.buildGroupEntries(entries);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse a label key string into a labels object.
|
|
73
|
+
* Exposed for use by converters.
|
|
74
|
+
*/
|
|
75
|
+
parseLabels(labelKey: string): Record<string, string> {
|
|
76
|
+
if (!labelKey) return {};
|
|
77
|
+
const labels: Record<string, string> = {};
|
|
78
|
+
const parts = labelKey.split(',');
|
|
79
|
+
for (const part of parts) {
|
|
80
|
+
const [key, val] = part.split('=');
|
|
81
|
+
if (key && val !== undefined) {
|
|
82
|
+
labels[key] = val;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return labels;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private collectMetricValues(metric: GGMetric<any>): MetricValueEntry[] {
|
|
89
|
+
const entries: MetricValueEntry[] = [];
|
|
90
|
+
const key = metric.key as GGMetricKey<any>;
|
|
91
|
+
const groupByLabels = key.groupBy?.labels ?? [];
|
|
92
|
+
const metricName = this.getShortName(key);
|
|
93
|
+
|
|
94
|
+
for (const [labelKey, value] of metric.getValues()) {
|
|
95
|
+
const labels = this.parseLabels(labelKey);
|
|
96
|
+
|
|
97
|
+
// Compute groupBy key
|
|
98
|
+
const groupKey = this.computeGroupKey(key, labels);
|
|
99
|
+
|
|
100
|
+
// Compute remaining labels (not in groupBy)
|
|
101
|
+
const remainingLabels: Record<string, string> = {};
|
|
102
|
+
for (const [k, v] of Object.entries(labels)) {
|
|
103
|
+
if (!groupByLabels.includes(k)) {
|
|
104
|
+
remainingLabels[k] = v;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
entries.push({
|
|
109
|
+
groupKey,
|
|
110
|
+
metricName,
|
|
111
|
+
metricType: this.getMetricType(metric),
|
|
112
|
+
remainingLabels,
|
|
113
|
+
value: this.formatValue(metric, value)
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return entries;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private computeGroupKey(key: GGMetricKey<any>, labels: Record<string, string>): string {
|
|
121
|
+
const groupBy = key.groupBy;
|
|
122
|
+
if (!groupBy || groupBy.labels.length === 0) {
|
|
123
|
+
return key.root; // Use metric root as default group
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (groupBy.template) {
|
|
127
|
+
// Template string - replace {labelName} with values, missing values become empty string
|
|
128
|
+
return groupBy.template.replace(/\{(\w+)\}/g, (_, labelName) => {
|
|
129
|
+
return String(labels[labelName] ?? '');
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
// Default: join values with comma
|
|
133
|
+
return groupBy.labels.map(l => String(labels[l] ?? '')).join(',');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private getShortName(key: GGMetricKey<any>): string {
|
|
138
|
+
// Remove the root prefix to get just the metric name
|
|
139
|
+
return key.name.replace(key.root, '');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private getMetricType(metric: GGMetric<any>): string {
|
|
143
|
+
return metric.constructor.name;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private formatValue(metric: GGMetric<any>, value: any): any {
|
|
147
|
+
const converter = GGNestedMetricsExporter.converters.get(metric.constructor);
|
|
148
|
+
if (!converter) {
|
|
149
|
+
// Fallback: return raw value
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
return converter(metric, value, this);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private buildGroupEntries(entries: MetricValueEntry[]): any[] {
|
|
156
|
+
// Find all unique "shared" label combinations
|
|
157
|
+
// Shared labels are those that exist in multiple metrics (like 'path')
|
|
158
|
+
// Non-shared labels are metric-specific (like 'result' only on counter)
|
|
159
|
+
|
|
160
|
+
// First, find which labels are common across all metrics in this group
|
|
161
|
+
const labelSets = new Map<string, Set<string>>();
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const labelNames = Object.keys(entry.remainingLabels);
|
|
164
|
+
const existing = labelSets.get(entry.metricName);
|
|
165
|
+
if (!existing) {
|
|
166
|
+
labelSets.set(entry.metricName, new Set(labelNames));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Find common labels (present in all metrics)
|
|
171
|
+
const allMetrics = [...labelSets.keys()];
|
|
172
|
+
let commonLabels: Set<string> = new Set();
|
|
173
|
+
if (allMetrics.length > 0) {
|
|
174
|
+
commonLabels = new Set(labelSets.get(allMetrics[0])!);
|
|
175
|
+
for (let i = 1; i < allMetrics.length; i++) {
|
|
176
|
+
const metricLabels = labelSets.get(allMetrics[i])!;
|
|
177
|
+
for (const label of commonLabels) {
|
|
178
|
+
if (!metricLabels.has(label)) {
|
|
179
|
+
commonLabels.delete(label);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Group entries by their common label values
|
|
186
|
+
const byCommonLabels = new Map<string, MetricValueEntry[]>();
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const commonKey = [...commonLabels]
|
|
189
|
+
.sort()
|
|
190
|
+
.map(l => `${l}=${entry.remainingLabels[l] ?? ''}`)
|
|
191
|
+
.join(',');
|
|
192
|
+
|
|
193
|
+
const existing = byCommonLabels.get(commonKey);
|
|
194
|
+
if (existing) {
|
|
195
|
+
existing.push(entry);
|
|
196
|
+
} else {
|
|
197
|
+
byCommonLabels.set(commonKey, [entry]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Build output entries
|
|
202
|
+
const result: any[] = [];
|
|
203
|
+
for (const [_, groupEntries] of byCommonLabels) {
|
|
204
|
+
const outputEntry: Record<string, any> = {};
|
|
205
|
+
|
|
206
|
+
// Add common labels as direct properties
|
|
207
|
+
if (groupEntries.length > 0) {
|
|
208
|
+
for (const label of commonLabels) {
|
|
209
|
+
outputEntry[label] = groupEntries[0].remainingLabels[label];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Group entries by metric name
|
|
214
|
+
const byMetric = new Map<string, MetricValueEntry[]>();
|
|
215
|
+
for (const entry of groupEntries) {
|
|
216
|
+
const existing = byMetric.get(entry.metricName);
|
|
217
|
+
if (existing) {
|
|
218
|
+
existing.push(entry);
|
|
219
|
+
} else {
|
|
220
|
+
byMetric.set(entry.metricName, [entry]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Add each metric's values
|
|
225
|
+
for (const [metricName, metricEntries] of byMetric) {
|
|
226
|
+
// Get extra labels (not in commonLabels)
|
|
227
|
+
const extraLabels = Object.keys(metricEntries[0].remainingLabels)
|
|
228
|
+
.filter(l => !commonLabels.has(l));
|
|
229
|
+
|
|
230
|
+
if (extraLabels.length === 0) {
|
|
231
|
+
// No extra labels - value goes directly under metric name
|
|
232
|
+
outputEntry[metricName] = metricEntries[0].value;
|
|
233
|
+
} else {
|
|
234
|
+
// Has extra labels - nest by those label names
|
|
235
|
+
this.nestByLabels(outputEntry, metricName, metricEntries, extraLabels, 0);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
result.push(outputEntry);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private nestByLabels(
|
|
246
|
+
obj: Record<string, any>,
|
|
247
|
+
metricName: string,
|
|
248
|
+
entries: MetricValueEntry[],
|
|
249
|
+
extraLabels: string[],
|
|
250
|
+
labelIndex: number
|
|
251
|
+
): void {
|
|
252
|
+
const currentLabel = extraLabels[labelIndex];
|
|
253
|
+
const isLastLabel = labelIndex === extraLabels.length - 1;
|
|
254
|
+
|
|
255
|
+
// Group entries by current label value
|
|
256
|
+
const byLabelValue = new Map<string, MetricValueEntry[]>();
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
const labelValue = entry.remainingLabels[currentLabel] ?? '';
|
|
259
|
+
const existing = byLabelValue.get(labelValue);
|
|
260
|
+
if (existing) {
|
|
261
|
+
existing.push(entry);
|
|
262
|
+
} else {
|
|
263
|
+
byLabelValue.set(labelValue, [entry]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Create or get the label container
|
|
268
|
+
if (!obj[currentLabel]) {
|
|
269
|
+
obj[currentLabel] = {};
|
|
270
|
+
}
|
|
271
|
+
const labelContainer = obj[currentLabel];
|
|
272
|
+
|
|
273
|
+
// Add each label value
|
|
274
|
+
for (const [labelValue, valueEntries] of byLabelValue) {
|
|
275
|
+
if (isLastLabel) {
|
|
276
|
+
// Last label - add metric name and value
|
|
277
|
+
if (!labelContainer[labelValue]) {
|
|
278
|
+
labelContainer[labelValue] = {};
|
|
279
|
+
}
|
|
280
|
+
labelContainer[labelValue][metricName] = valueEntries[0].value;
|
|
281
|
+
} else {
|
|
282
|
+
// More labels to go - recurse
|
|
283
|
+
if (!labelContainer[labelValue]) {
|
|
284
|
+
labelContainer[labelValue] = {};
|
|
285
|
+
}
|
|
286
|
+
this.nestByLabels(labelContainer[labelValue], metricName, valueEntries, extraLabels, labelIndex + 1);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Built-in value converters
|
|
293
|
+
|
|
294
|
+
function convertCounterValue(_metric: GGCounter<any>, value: number, _exporter: GGNestedMetricsExporter): number {
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function convertGaugeValue(_metric: GGGauge<any>, value: number, _exporter: GGNestedMetricsExporter): number {
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function convertLazyGaugeValue(_metric: GGLazyGauge, value: number, _exporter: GGNestedMetricsExporter): number {
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function convertHistogramValue(metric: GGHistogram<any>, value: HistogramData, _exporter: GGNestedMetricsExporter): any {
|
|
307
|
+
const buckets = metric.getBuckets();
|
|
308
|
+
const bucketObj: Record<string, number> = {};
|
|
309
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
310
|
+
bucketObj[String(buckets[i])] = value.values[i] ?? 0;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
count: value.count,
|
|
314
|
+
sum: value.sum,
|
|
315
|
+
avg: value.count > 0 ? value.sum / value.count : 0,
|
|
316
|
+
min: value.min === Infinity ? 0 : value.min,
|
|
317
|
+
max: value.max === -Infinity ? 0 : value.max,
|
|
318
|
+
buckets: bucketObj
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Types
|
|
323
|
+
|
|
324
|
+
interface MetricValueEntry {
|
|
325
|
+
groupKey: string;
|
|
326
|
+
metricName: string;
|
|
327
|
+
metricType: string;
|
|
328
|
+
remainingLabels: Record<string, string>;
|
|
329
|
+
value: any;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export interface NestedMetricsOutput {
|
|
333
|
+
timestamp: number;
|
|
334
|
+
groups: Record<string, any[]>;
|
|
335
|
+
}
|