@grest-ts/metrics 0.0.5
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 -0
- package/README.md +40 -0
- package/dist/src/GGMetric.d.ts +60 -0
- package/dist/src/GGMetric.d.ts.map +1 -0
- package/dist/src/GGMetric.js +64 -0
- package/dist/src/GGMetric.js.map +1 -0
- package/dist/src/GGMetricKey.d.ts +13 -0
- package/dist/src/GGMetricKey.d.ts.map +1 -0
- package/dist/src/GGMetricKey.js +29 -0
- package/dist/src/GGMetricKey.js.map +1 -0
- package/dist/src/GGMetrics.d.ts +10 -0
- package/dist/src/GGMetrics.d.ts.map +1 -0
- package/dist/src/GGMetrics.js +29 -0
- package/dist/src/GGMetrics.js.map +1 -0
- package/dist/src/GGMetricsDefineStorage.d.ts +4 -0
- package/dist/src/GGMetricsDefineStorage.d.ts.map +1 -0
- package/dist/src/GGMetricsDefineStorage.js +7 -0
- package/dist/src/GGMetricsDefineStorage.js.map +1 -0
- package/dist/src/GGMetricsLoader.d.ts +8 -0
- package/dist/src/GGMetricsLoader.d.ts.map +1 -0
- package/dist/src/GGMetricsLoader.js +17 -0
- package/dist/src/GGMetricsLoader.js.map +1 -0
- package/dist/src/GGMetricsStore.d.ts +9 -0
- package/dist/src/GGMetricsStore.d.ts.map +1 -0
- package/dist/src/GGMetricsStore.js +20 -0
- package/dist/src/GGMetricsStore.js.map +1 -0
- package/dist/src/exporters/GGJsonMetricsExporter.d.ts +41 -0
- package/dist/src/exporters/GGJsonMetricsExporter.d.ts.map +1 -0
- package/dist/src/exporters/GGJsonMetricsExporter.js +129 -0
- package/dist/src/exporters/GGJsonMetricsExporter.js.map +1 -0
- package/dist/src/exporters/GGMetricsExporter.d.ts +41 -0
- package/dist/src/exporters/GGMetricsExporter.d.ts.map +1 -0
- package/dist/src/exporters/GGMetricsExporter.js +70 -0
- package/dist/src/exporters/GGMetricsExporter.js.map +1 -0
- package/dist/src/exporters/GGNestedMetricsExporter.d.ts +33 -0
- package/dist/src/exporters/GGNestedMetricsExporter.d.ts.map +1 -0
- package/dist/src/exporters/GGNestedMetricsExporter.js +275 -0
- package/dist/src/exporters/GGNestedMetricsExporter.js.map +1 -0
- package/dist/src/index-browser.d.ts +17 -0
- package/dist/src/index-browser.d.ts.map +1 -0
- package/dist/src/index-browser.js +17 -0
- package/dist/src/index-browser.js.map +1 -0
- package/dist/src/index-node.d.ts +17 -0
- package/dist/src/index-node.d.ts.map +1 -0
- package/dist/src/index-node.js +20 -0
- package/dist/src/index-node.js.map +1 -0
- package/dist/src/keys/GGCounterKey.d.ts +11 -0
- package/dist/src/keys/GGCounterKey.d.ts.map +1 -0
- package/dist/src/keys/GGCounterKey.js +21 -0
- package/dist/src/keys/GGCounterKey.js.map +1 -0
- package/dist/src/keys/GGGaugeKey.d.ts +13 -0
- package/dist/src/keys/GGGaugeKey.d.ts.map +1 -0
- package/dist/src/keys/GGGaugeKey.js +27 -0
- package/dist/src/keys/GGGaugeKey.js.map +1 -0
- package/dist/src/keys/GGHistogramKey.d.ts +13 -0
- package/dist/src/keys/GGHistogramKey.d.ts.map +1 -0
- package/dist/src/keys/GGHistogramKey.js +27 -0
- package/dist/src/keys/GGHistogramKey.js.map +1 -0
- package/dist/src/keys/GGLazyGaugeKey.d.ts +21 -0
- package/dist/src/keys/GGLazyGaugeKey.d.ts.map +1 -0
- package/dist/src/keys/GGLazyGaugeKey.js +25 -0
- package/dist/src/keys/GGLazyGaugeKey.js.map +1 -0
- package/dist/src/metric/GGCounter.d.ts +7 -0
- package/dist/src/metric/GGCounter.d.ts.map +1 -0
- package/dist/src/metric/GGCounter.js +14 -0
- package/dist/src/metric/GGCounter.js.map +1 -0
- package/dist/src/metric/GGGauge.d.ts +9 -0
- package/dist/src/metric/GGGauge.d.ts.map +1 -0
- package/dist/src/metric/GGGauge.js +31 -0
- package/dist/src/metric/GGGauge.js.map +1 -0
- package/dist/src/metric/GGHistogram.d.ts +27 -0
- package/dist/src/metric/GGHistogram.d.ts.map +1 -0
- package/dist/src/metric/GGHistogram.js +41 -0
- package/dist/src/metric/GGHistogram.js.map +1 -0
- package/dist/src/metric/GGLazyGauge.d.ts +20 -0
- package/dist/src/metric/GGLazyGauge.d.ts.map +1 -0
- package/dist/src/metric/GGLazyGauge.js +27 -0
- package/dist/src/metric/GGLazyGauge.js.map +1 -0
- package/dist/src/tsconfig.json +16 -0
- package/dist/testkit/GGMetricsCommands.d.ts +18 -0
- package/dist/testkit/GGMetricsCommands.d.ts.map +1 -0
- package/dist/testkit/GGMetricsCommands.js +77 -0
- package/dist/testkit/GGMetricsCommands.js.map +1 -0
- package/dist/testkit/GGMetricsInterceptor.d.ts +34 -0
- package/dist/testkit/GGMetricsInterceptor.d.ts.map +1 -0
- package/dist/testkit/GGMetricsInterceptor.js +202 -0
- package/dist/testkit/GGMetricsInterceptor.js.map +1 -0
- package/dist/testkit/GGMetricsWith.d.ts +22 -0
- package/dist/testkit/GGMetricsWith.d.ts.map +1 -0
- package/dist/testkit/GGMetricsWith.js +61 -0
- package/dist/testkit/GGMetricsWith.js.map +1 -0
- package/dist/testkit/GGTestMetricsExporter.d.ts +40 -0
- package/dist/testkit/GGTestMetricsExporter.d.ts.map +1 -0
- package/dist/testkit/GGTestMetricsExporter.js +119 -0
- package/dist/testkit/GGTestMetricsExporter.js.map +1 -0
- package/dist/testkit/GGTestSelectorMetrics.d.ts +15 -0
- package/dist/testkit/GGTestSelectorMetrics.d.ts.map +1 -0
- package/dist/testkit/GGTestSelectorMetrics.js +16 -0
- package/dist/testkit/GGTestSelectorMetrics.js.map +1 -0
- package/dist/testkit/index-testkit.d.ts +6 -0
- package/dist/testkit/index-testkit.d.ts.map +1 -0
- package/dist/testkit/index-testkit.js +6 -0
- package/dist/testkit/index-testkit.js.map +1 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/GGMetric.ts +124 -0
- package/src/GGMetricKey.ts +38 -0
- package/src/GGMetrics.ts +34 -0
- package/src/GGMetricsDefineStorage.ts +8 -0
- package/src/GGMetricsLoader.ts +21 -0
- package/src/GGMetricsStore.ts +26 -0
- package/src/exporters/GGJsonMetricsExporter.ts +176 -0
- package/src/exporters/GGMetricsExporter.ts +88 -0
- package/src/exporters/GGNestedMetricsExporter.ts +335 -0
- package/src/index-browser.ts +16 -0
- package/src/index-node.ts +21 -0
- package/src/keys/GGCounterKey.ts +29 -0
- package/src/keys/GGGaugeKey.ts +37 -0
- package/src/keys/GGHistogramKey.ts +37 -0
- package/src/keys/GGLazyGaugeKey.ts +36 -0
- package/src/metric/GGCounter.ts +19 -0
- package/src/metric/GGGauge.ts +38 -0
- package/src/metric/GGHistogram.ts +68 -0
- package/src/metric/GGLazyGauge.ts +31 -0
- package/src/tsconfig.json +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@grest-ts/metrics",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Metrics library for Grest Framework",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/src/index-node.d.ts",
|
|
10
|
+
"import": "./dist/src/index-node.js"
|
|
11
|
+
},
|
|
12
|
+
"./testkit": {
|
|
13
|
+
"types": "./dist/testkit/index-testkit.d.ts",
|
|
14
|
+
"import": "./dist/testkit/index-testkit.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"typecheck": "tsc --noEmit -p src"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/grest-ts/grest-ts.git",
|
|
30
|
+
"directory": "packages/metrics"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/grest-ts/grest-ts/tree/master/packages/metrics",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/grest-ts/grest-ts/issues"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"typescript",
|
|
38
|
+
"framework",
|
|
39
|
+
"contract",
|
|
40
|
+
"api",
|
|
41
|
+
"microservices",
|
|
42
|
+
"testing",
|
|
43
|
+
"metrics",
|
|
44
|
+
"monitoring",
|
|
45
|
+
"observability"
|
|
46
|
+
],
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@grest-ts/common": "0.0.5",
|
|
52
|
+
"@grest-ts/locator": "0.0.5"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@grest-ts/testkit": "0.0.5",
|
|
56
|
+
"@grest-ts/x-packager": "0.0.5"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/GGMetric.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {GGMetricKey} from "./GGMetricKey";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for grouping metrics in nested exporters.
|
|
5
|
+
* - labels: Which labels to use for grouping (must be subset of labelNames)
|
|
6
|
+
* - template: Optional template string for the group key. Use {labelName} for placeholders.
|
|
7
|
+
* Missing values become empty strings. Defaults to values.join(',')
|
|
8
|
+
* Example: "{api}.{method}" produces "MyApi.myMethod"
|
|
9
|
+
*/
|
|
10
|
+
export interface GroupByConfig<TLabels extends GGMetricLabels = {}> {
|
|
11
|
+
labels: readonly (keyof TLabels & string)[];
|
|
12
|
+
template?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GGMetricOptionsBase {
|
|
16
|
+
help: string;
|
|
17
|
+
maxLabelCombinations?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GGMetricOptionsWithLabels<TLabels extends GGMetricLabels> {
|
|
21
|
+
help: string;
|
|
22
|
+
maxLabelCombinations?: number;
|
|
23
|
+
labelNames: readonly (keyof TLabels & string)[];
|
|
24
|
+
groupBy?: GroupByConfig<TLabels>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for metrics. When TLabels has keys, labelNames is required and must match those keys.
|
|
29
|
+
* Uses {} as "no labels" because keyof {} = never, while keyof Record<string, never> = string.
|
|
30
|
+
*/
|
|
31
|
+
export type GGMetricOptions<TLabels extends GGMetricLabels = {}> =
|
|
32
|
+
keyof TLabels extends never
|
|
33
|
+
? GGMetricOptionsBase
|
|
34
|
+
: GGMetricOptionsWithLabels<TLabels>;
|
|
35
|
+
|
|
36
|
+
export abstract class GGMetric<
|
|
37
|
+
TLabels extends GGMetricLabels = {},
|
|
38
|
+
TValue = unknown,
|
|
39
|
+
TKey extends GGMetricKey<TLabels> = GGMetricKey<TLabels>
|
|
40
|
+
> {
|
|
41
|
+
|
|
42
|
+
public readonly key: TKey;
|
|
43
|
+
private readonly values = new Map<string, TValue>();
|
|
44
|
+
private readonly compiledGetKey: (labels: TLabels) => tMetricKey;
|
|
45
|
+
|
|
46
|
+
public constructor(key: TKey) {
|
|
47
|
+
this.key = key;
|
|
48
|
+
this.compiledGetKey = this.compileGetKey();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private compileGetKey(): (labels: TLabels) => tMetricKey {
|
|
52
|
+
const labelNames = this.key.labelNames;
|
|
53
|
+
if (labelNames.length === 0) {
|
|
54
|
+
return () => '' as tMetricKey;
|
|
55
|
+
}
|
|
56
|
+
// Build the function body: "name1=" + (l?.name1 ?? "") + ",name2=" + (l?.name2 ?? "")
|
|
57
|
+
let body = 'return ';
|
|
58
|
+
for (let i = 0; i < labelNames.length; i++) {
|
|
59
|
+
const name = labelNames[i];
|
|
60
|
+
if (i > 0) body += '+","+';
|
|
61
|
+
body += `"${name}="+(l?.${name}??"")`;
|
|
62
|
+
}
|
|
63
|
+
return new Function('l', body) as (labels: TLabels) => tMetricKey;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public get name(): string {
|
|
67
|
+
return this.key.name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public reset(): void {
|
|
71
|
+
this.values.clear();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected abstract getDefaultValue(): TValue;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Can't throw.
|
|
78
|
+
* Uses AOT-compiled function for fast key generation.
|
|
79
|
+
*/
|
|
80
|
+
protected getKey(labels: TLabels): tMetricKey {
|
|
81
|
+
return this.compiledGetKey(labels);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Can't throw.
|
|
86
|
+
*/
|
|
87
|
+
protected getByKey(key: tMetricKey): TValue | undefined {
|
|
88
|
+
const value = this.values.get(key);
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
if (this.values.size >= this.key.maxLabelCombinations) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const defaultValue = this.getDefaultValue();
|
|
94
|
+
this.setByKey(key, defaultValue);
|
|
95
|
+
return defaultValue;
|
|
96
|
+
} else {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected setByKey(key: tMetricKey, value: TValue): void {
|
|
102
|
+
this.values.set(key, value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public getValue(...args: LabelsArgs<TLabels>): TValue {
|
|
106
|
+
return this.getByKey(this.getKey(args[0] as TLabels));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public getValues(): Map<string, TValue> {
|
|
110
|
+
return this.values;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type tMetricKey = string & { tMetricKey: never };
|
|
115
|
+
|
|
116
|
+
export type GGMetricLabels = Record<string, string | number | boolean>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Helper type for labels parameter. Makes labels required when TLabels has keys.
|
|
120
|
+
* - When TLabels = {}, no argument needed
|
|
121
|
+
* - When TLabels has keys, labels argument is required
|
|
122
|
+
*/
|
|
123
|
+
export type LabelsArgs<TLabels extends GGMetricLabels> =
|
|
124
|
+
keyof TLabels extends never ? [] : [labels: TLabels];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {GGMetrics} from "./GGMetrics.js";
|
|
2
|
+
import {GG_METRICS} from "./GGMetricsLoader.js";
|
|
3
|
+
import {GGMetric, GGMetricLabels, GGMetricOptions, GroupByConfig} from "./GGMetric.js";
|
|
4
|
+
|
|
5
|
+
export abstract class GGMetricKey<
|
|
6
|
+
TLabels extends GGMetricLabels = {},
|
|
7
|
+
TMetric extends GGMetric<TLabels, any, any> = GGMetric<TLabels, any, any>
|
|
8
|
+
> {
|
|
9
|
+
|
|
10
|
+
public readonly root: string;
|
|
11
|
+
public readonly name: string;
|
|
12
|
+
public readonly help: string;
|
|
13
|
+
public readonly labelNames: readonly string[];
|
|
14
|
+
public readonly maxLabelCombinations?: number;
|
|
15
|
+
public readonly groupBy?: GroupByConfig<TLabels>;
|
|
16
|
+
|
|
17
|
+
protected constructor(name: string, options: GGMetricOptions<TLabels>) {
|
|
18
|
+
this.root = GGMetrics.getDefinitionContext();
|
|
19
|
+
if (!this.root) {
|
|
20
|
+
throw new Error("Metric key must be created inside GGMetrics.define()");
|
|
21
|
+
}
|
|
22
|
+
this.name = this.root + name;
|
|
23
|
+
this.help = options.help;
|
|
24
|
+
this.labelNames = ('labelNames' in options ? options.labelNames : []) as readonly (keyof TLabels & string)[];
|
|
25
|
+
this.maxLabelCombinations = options.maxLabelCombinations;
|
|
26
|
+
this.groupBy = ('groupBy' in options ? options.groupBy : undefined) as GroupByConfig<TLabels> | undefined;
|
|
27
|
+
Object.freeze(this.labelNames);
|
|
28
|
+
if (this.groupBy) {
|
|
29
|
+
Object.freeze(this.groupBy.labels);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected getMetric(): TMetric {
|
|
34
|
+
return GG_METRICS.get().get(this) as TMetric;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public abstract create(): TMetric;
|
|
38
|
+
}
|
package/src/GGMetrics.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {GGMetricKey} from "./GGMetricKey";
|
|
2
|
+
import {METRICS_DEFINE_CONTEXT} from "./GGMetricsDefineStorage";
|
|
3
|
+
|
|
4
|
+
export type GGMetricsDefinition<T> = T & { __isGGMetricsDefinition: never };
|
|
5
|
+
|
|
6
|
+
export class GGMetrics {
|
|
7
|
+
|
|
8
|
+
public static define<T>(name: string, define: () => T): GGMetricsDefinition<T> {
|
|
9
|
+
return METRICS_DEFINE_CONTEXT.run(name, define) as GGMetricsDefinition<T>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public static getDefinitionContext(): string {
|
|
13
|
+
return METRICS_DEFINE_CONTEXT.getStore()!
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static toJSON(metrics: GGMetricsDefinition<any>): Record<string, unknown> {
|
|
17
|
+
const result: Record<string, unknown> = {};
|
|
18
|
+
this.collectKeys(metrics, result);
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private static collectKeys(obj: unknown, result: Record<string, unknown>): void {
|
|
23
|
+
if (obj instanceof GGMetricKey) {
|
|
24
|
+
result[obj.name] = {
|
|
25
|
+
type: obj.constructor.name,
|
|
26
|
+
help: obj.help
|
|
27
|
+
};
|
|
28
|
+
} else if (obj && typeof obj === 'object') {
|
|
29
|
+
for (const value of Object.values(obj)) {
|
|
30
|
+
this.collectKeys(value, result);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {BrowserAsyncStorage, type IAsyncStorage} from "@grest-ts/common";
|
|
2
|
+
|
|
3
|
+
// Browser-safe by default. Node.js entry replaces with real AsyncLocalStorage.
|
|
4
|
+
export let METRICS_DEFINE_CONTEXT: IAsyncStorage<string> = new BrowserAsyncStorage();
|
|
5
|
+
|
|
6
|
+
export function _initMetricsStorage(storage: IAsyncStorage<string>): void {
|
|
7
|
+
METRICS_DEFINE_CONTEXT = storage;
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {GGMetricsStore} from "./GGMetricsStore";
|
|
2
|
+
import {GGLocator, GGLocatorKey, GGLocatorServiceType} from "@grest-ts/locator";
|
|
3
|
+
|
|
4
|
+
export const GG_METRICS = new GGLocatorKey<GGMetricsStore>("GGMetrics");
|
|
5
|
+
|
|
6
|
+
export class GGMetricsLoader {
|
|
7
|
+
|
|
8
|
+
private readonly store: GGMetricsStore;
|
|
9
|
+
|
|
10
|
+
constructor(store?: GGMetricsStore) {
|
|
11
|
+
this.store = store ?? new GGMetricsStore();
|
|
12
|
+
|
|
13
|
+
GGLocator.getScope().setWithLifecycle(GG_METRICS, this.store, {
|
|
14
|
+
type: GGLocatorServiceType.CONFIG + 1,
|
|
15
|
+
start: async () => {
|
|
16
|
+
},
|
|
17
|
+
teardown: async () => {
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {GGMetric, GGMetricLabels} from "./GGMetric.js";
|
|
2
|
+
import {GGMetricKey} from "./GGMetricKey.js";
|
|
3
|
+
|
|
4
|
+
export class GGMetricsStore {
|
|
5
|
+
|
|
6
|
+
private readonly metrics = new Map<string, GGMetric<any>>();
|
|
7
|
+
|
|
8
|
+
public get<TLabel extends GGMetricLabels>(key: GGMetricKey<TLabel>): GGMetric<TLabel> {
|
|
9
|
+
let metric = this.metrics.get(key.name);
|
|
10
|
+
if (!metric) {
|
|
11
|
+
metric = key.create();
|
|
12
|
+
this.metrics.set(key.name, metric);
|
|
13
|
+
}
|
|
14
|
+
return metric;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public getAllMetrics(): IterableIterator<GGMetric<any>> {
|
|
18
|
+
return this.metrics.values();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public reset(): void {
|
|
22
|
+
for (const metric of this.metrics.values()) {
|
|
23
|
+
metric.reset();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
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 {GGMetricsExporter, ExporterConfig} from "./GGMetricsExporter.js";
|
|
7
|
+
|
|
8
|
+
export type JsonMetricConverter = (metric: GGMetric<any>, exporter: GGJsonMetricsExporter) => JsonMetric;
|
|
9
|
+
|
|
10
|
+
export class GGJsonMetricsExporter extends GGMetricsExporter<JsonMetricsOutput> {
|
|
11
|
+
|
|
12
|
+
// Static map for extensibility - register converters for new metric types
|
|
13
|
+
private static converters = new Map<Function, JsonMetricConverter>();
|
|
14
|
+
|
|
15
|
+
static {
|
|
16
|
+
GGJsonMetricsExporter.registerConverter(GGCounter, convertCounter);
|
|
17
|
+
GGJsonMetricsExporter.registerConverter(GGGauge, convertGauge);
|
|
18
|
+
GGJsonMetricsExporter.registerConverter(GGLazyGauge, convertLazyGauge);
|
|
19
|
+
GGJsonMetricsExporter.registerConverter(GGHistogram, convertHistogram);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a converter for a custom metric type.
|
|
24
|
+
*/
|
|
25
|
+
static registerConverter(metricClass: Function, converter: JsonMetricConverter): void {
|
|
26
|
+
GGJsonMetricsExporter.converters.set(metricClass, converter);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
constructor(config: ExporterConfig = {}) {
|
|
30
|
+
super(config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getMetrics(): JsonMetricsOutput {
|
|
34
|
+
const output: JsonMetricsOutput = {
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
metrics: {}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (const metric of this.getFilteredMetrics()) {
|
|
40
|
+
output.metrics[metric.name] = this.convertMetric(metric);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private convertMetric(metric: GGMetric<any>): JsonMetric {
|
|
47
|
+
const converter = GGJsonMetricsExporter.converters.get(metric.constructor);
|
|
48
|
+
if (!converter) {
|
|
49
|
+
throw new Error(`No converter registered for metric type: ${metric.constructor.name}`);
|
|
50
|
+
}
|
|
51
|
+
return converter(metric, this);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse a label key string into a labels object.
|
|
56
|
+
* Exposed for use by converters.
|
|
57
|
+
*/
|
|
58
|
+
parseLabels(labelKey: string): Record<string, string> {
|
|
59
|
+
if (!labelKey) return {};
|
|
60
|
+
const labels: Record<string, string> = {};
|
|
61
|
+
const parts = labelKey.split(',');
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
const [key, val] = part.split('=');
|
|
64
|
+
if (key && val !== undefined) {
|
|
65
|
+
labels[key] = val;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return labels;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Built-in converters
|
|
73
|
+
|
|
74
|
+
function convertCounter(metric: GGCounter<any>, exporter: GGJsonMetricsExporter): JsonMetric {
|
|
75
|
+
const values: JsonMetricValue[] = [];
|
|
76
|
+
for (const [labelKey, value] of metric.getValues()) {
|
|
77
|
+
values.push({
|
|
78
|
+
labels: exporter.parseLabels(labelKey),
|
|
79
|
+
value
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
name: metric.name,
|
|
84
|
+
type: 'counter',
|
|
85
|
+
help: metric.key.help,
|
|
86
|
+
values
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function convertGauge(metric: GGGauge<any>, exporter: GGJsonMetricsExporter): JsonMetric {
|
|
91
|
+
const values: JsonMetricValue[] = [];
|
|
92
|
+
for (const [labelKey, value] of metric.getValues()) {
|
|
93
|
+
values.push({
|
|
94
|
+
labels: exporter.parseLabels(labelKey),
|
|
95
|
+
value
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
name: metric.name,
|
|
100
|
+
type: 'gauge',
|
|
101
|
+
help: metric.key.help,
|
|
102
|
+
values
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function convertLazyGauge(metric: GGLazyGauge, exporter: GGJsonMetricsExporter): JsonMetric {
|
|
107
|
+
// Lazy gauge computes value on read - no labels
|
|
108
|
+
return {
|
|
109
|
+
name: metric.name,
|
|
110
|
+
type: 'gauge',
|
|
111
|
+
help: metric.key.help,
|
|
112
|
+
values: [{
|
|
113
|
+
labels: {},
|
|
114
|
+
value: metric.getValue()
|
|
115
|
+
}]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function convertHistogram(metric: GGHistogram<any>, exporter: GGJsonMetricsExporter): JsonMetric {
|
|
120
|
+
const values: JsonMetricValue[] = [];
|
|
121
|
+
const buckets = metric.getBuckets();
|
|
122
|
+
|
|
123
|
+
for (const [labelKey, data] of metric.getValues() as Map<string, HistogramData>) {
|
|
124
|
+
const bucketObj: Record<string, number> = {};
|
|
125
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
126
|
+
bucketObj[String(buckets[i])] = data.values[i] ?? 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
values.push({
|
|
130
|
+
labels: exporter.parseLabels(labelKey),
|
|
131
|
+
value: {
|
|
132
|
+
count: data.count,
|
|
133
|
+
sum: data.sum,
|
|
134
|
+
avg: data.count > 0 ? data.sum / data.count : 0,
|
|
135
|
+
min: data.min === Infinity ? 0 : data.min,
|
|
136
|
+
max: data.max === -Infinity ? 0 : data.max,
|
|
137
|
+
buckets: bucketObj
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
name: metric.name,
|
|
144
|
+
type: 'histogram',
|
|
145
|
+
help: metric.key.help,
|
|
146
|
+
values
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Types
|
|
151
|
+
|
|
152
|
+
export interface JsonMetricValue {
|
|
153
|
+
labels: Record<string, string>;
|
|
154
|
+
value: number | JsonHistogramValue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface JsonHistogramValue {
|
|
158
|
+
count: number;
|
|
159
|
+
sum: number;
|
|
160
|
+
avg: number;
|
|
161
|
+
min: number;
|
|
162
|
+
max: number;
|
|
163
|
+
buckets: Record<string, number>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface JsonMetric {
|
|
167
|
+
name: string;
|
|
168
|
+
type: string;
|
|
169
|
+
help: string;
|
|
170
|
+
values: JsonMetricValue[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface JsonMetricsOutput {
|
|
174
|
+
timestamp: number;
|
|
175
|
+
metrics: Record<string, JsonMetric>;
|
|
176
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {GGMetricsStore} from "../GGMetricsStore.js";
|
|
2
|
+
import {GG_METRICS} from "../GGMetricsLoader.js";
|
|
3
|
+
import {GGMetric} from "../GGMetric.js";
|
|
4
|
+
import {GGMetricKey} from "../GGMetricKey.js";
|
|
5
|
+
|
|
6
|
+
export interface ExporterConfig {
|
|
7
|
+
store?: GGMetricsStore;
|
|
8
|
+
include?: unknown[];
|
|
9
|
+
exclude?: unknown[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Abstract base class for metrics exporters.
|
|
14
|
+
* Handles config parsing, metric filtering, and key discovery.
|
|
15
|
+
*/
|
|
16
|
+
export abstract class GGMetricsExporter<TOutput> {
|
|
17
|
+
protected readonly store: GGMetricsStore;
|
|
18
|
+
private readonly includeKeys?: Set<GGMetricKey<any>>;
|
|
19
|
+
private readonly excludeKeys?: Set<GGMetricKey<any>>;
|
|
20
|
+
|
|
21
|
+
constructor(config: ExporterConfig = {}) {
|
|
22
|
+
this.store = config.store ?? GG_METRICS.get();
|
|
23
|
+
this.includeKeys = config.include ? this.discoverKeys(config.include) : undefined;
|
|
24
|
+
this.excludeKeys = config.exclude ? this.discoverKeys(config.exclude) : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Discover all GGMetricKey instances from an array of objects.
|
|
29
|
+
* Objects can be individual keys, or nested structures containing keys.
|
|
30
|
+
*/
|
|
31
|
+
private discoverKeys(objects: unknown[]): Set<GGMetricKey<any>> {
|
|
32
|
+
const keys = new Set<GGMetricKey<any>>();
|
|
33
|
+
for (const obj of objects) {
|
|
34
|
+
this.discoverKeysRecursive(obj, keys);
|
|
35
|
+
}
|
|
36
|
+
return keys;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private discoverKeysRecursive(obj: unknown, keys: Set<GGMetricKey<any>>): void {
|
|
40
|
+
if (obj instanceof GGMetricKey) {
|
|
41
|
+
keys.add(obj);
|
|
42
|
+
} else if (obj && typeof obj === 'object') {
|
|
43
|
+
for (const value of Object.values(obj)) {
|
|
44
|
+
this.discoverKeysRecursive(value, keys);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if a metric should be included in the export.
|
|
51
|
+
* Exclude takes precedence over include.
|
|
52
|
+
*/
|
|
53
|
+
protected shouldIncludeMetric(metric: GGMetric<any>): boolean {
|
|
54
|
+
// Exclude takes precedence
|
|
55
|
+
if (this.excludeKeys?.has(metric.key)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// If include is set, only include those
|
|
59
|
+
if (this.includeKeys) {
|
|
60
|
+
return this.includeKeys.has(metric.key);
|
|
61
|
+
}
|
|
62
|
+
// Default: include all
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get all metrics that pass the include/exclude filters.
|
|
68
|
+
*/
|
|
69
|
+
protected* getFilteredMetrics(): Iterable<GGMetric<any>> {
|
|
70
|
+
for (const metric of this.store.getAllMetrics()) {
|
|
71
|
+
if (this.shouldIncludeMetric(metric)) {
|
|
72
|
+
yield metric;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Export metrics to the output format.
|
|
79
|
+
*/
|
|
80
|
+
abstract getMetrics(): TOutput;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Export metrics as a JSON string.
|
|
84
|
+
*/
|
|
85
|
+
getMetricsString(): string {
|
|
86
|
+
return JSON.stringify(this.getMetrics(), null, 2);
|
|
87
|
+
}
|
|
88
|
+
}
|