@grest-ts/metrics 0.0.6 → 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.
@@ -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
+ }