openshard-shared 1.0.1
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 +11 -0
- package/package.json +15 -0
- package/src/index.js +57 -0
- package/src/payments.js +217 -0
- package/src/prometheus.js +169 -0
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @openshard/shared
|
|
2
|
+
|
|
3
|
+
> Shared utilities, types, constants, Prometheus metrics, and payment verification helpers for the OpenShard ecosystem.
|
|
4
|
+
|
|
5
|
+
This package provides common foundational code consumed across buyer proxies, seller nodes, registry servers, and CLI tools in the OpenShard monorepo.
|
|
6
|
+
|
|
7
|
+
## Modules
|
|
8
|
+
|
|
9
|
+
- `prometheus.js`: Shared Prometheus registry, metric formatting, and monitoring wrappers.
|
|
10
|
+
- `payments.js`: Cryptographic helpers, EIP-712 structured data signing, USDC formatting utilities, and x402 verification logic.
|
|
11
|
+
- `index.js`: Common constants, networking defaults, port mappings, and error formatting structures.
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openshard-shared",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Shared types, constants, and utilities for the anteseed network",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "echo 'shared has no build step'"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"viem": "^2.21.19"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [],
|
|
14
|
+
"license": "ISC"
|
|
15
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// ─── Shared constants & types for the anteseed network ───────────────────────
|
|
2
|
+
|
|
3
|
+
export const REGISTRY_PORT = 9000;
|
|
4
|
+
export const BUYER_PORT = 8377;
|
|
5
|
+
export const SELLER_PORT = 8378; // default; each seller can override via env
|
|
6
|
+
|
|
7
|
+
// ─── Provider capability advertised to the registry ──────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} ServicePricing
|
|
11
|
+
* @property {number} inputUsdPerMillion
|
|
12
|
+
* @property {number} cachedInputUsdPerMillion
|
|
13
|
+
* @property {number} outputUsdPerMillion
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} ServiceDefinition
|
|
18
|
+
* @property {string} id - buyer-facing model name, e.g. "claude-sonnet-4-6"
|
|
19
|
+
* @property {string} upstreamModel - actual model name sent to the upstream API
|
|
20
|
+
* @property {string[]} categories - ["chat","coding","math"]
|
|
21
|
+
* @property {ServicePricing} pricing
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} ProviderAdvertisement
|
|
26
|
+
* @property {string} peerId - hex public key / node identity
|
|
27
|
+
* @property {string} endpoint - http://host:port the buyer connects to
|
|
28
|
+
* @property {ServiceDefinition[]} services
|
|
29
|
+
* @property {number} registeredAt - unix ms
|
|
30
|
+
* @property {number} lastSeen - unix ms
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns true if the peer heartbeat is still considered "live"
|
|
37
|
+
* (seen within the last 60 seconds).
|
|
38
|
+
* @param {ProviderAdvertisement} peer
|
|
39
|
+
*/
|
|
40
|
+
export function isAlive(peer) {
|
|
41
|
+
return Date.now() - peer.lastSeen < 60_000;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a standard error body used across all Fastify nodes.
|
|
46
|
+
* @param {string} code - machine-readable error code
|
|
47
|
+
* @param {string} message - human-readable detail
|
|
48
|
+
*/
|
|
49
|
+
export function errorBody(code, message) {
|
|
50
|
+
return { error: { code, message } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Re-export payment utilities ─────────────────────────────────────────────
|
|
54
|
+
export * from './payments.js';
|
|
55
|
+
|
|
56
|
+
// ─── Re-export prometheus utilities ───────────────────────────────────────────
|
|
57
|
+
export * from './prometheus.js';
|
package/src/payments.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openshard_ggshared — payments.js
|
|
3
|
+
*
|
|
4
|
+
* EIP-712 signing utilities for ReserveAuth and SpendingAuth messages.
|
|
5
|
+
* Used by both the buyer proxy (signing) and the seller node (verification).
|
|
6
|
+
*
|
|
7
|
+
* Requires: viem (npm install viem)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createWalletClient,
|
|
12
|
+
createPublicClient,
|
|
13
|
+
http,
|
|
14
|
+
keccak256,
|
|
15
|
+
toBytes,
|
|
16
|
+
concat,
|
|
17
|
+
encodeAbiParameters,
|
|
18
|
+
parseAbiParameters,
|
|
19
|
+
recoverTypedDataAddress,
|
|
20
|
+
parseUnits,
|
|
21
|
+
formatUnits
|
|
22
|
+
} from 'viem';
|
|
23
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
24
|
+
import { base, baseSepolia } from 'viem/chains';
|
|
25
|
+
import { hardhat } from 'viem/chains';
|
|
26
|
+
|
|
27
|
+
// ─── USDC constants ───────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** USDC on Base mainnet */
|
|
30
|
+
export const USDC_BASE_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
31
|
+
/** USDC on Base Sepolia */
|
|
32
|
+
export const USDC_SEPOLIA_ADDRESS = '0x4eF7a082726B9E4fd136ED58B212F435b7b17594';
|
|
33
|
+
|
|
34
|
+
/** USDC decimals */
|
|
35
|
+
export const USDC_DECIMALS = 6;
|
|
36
|
+
|
|
37
|
+
/** Convert human USDC amount (e.g. "1.5") to USDC base units (BigInt) */
|
|
38
|
+
export const parseUsdc = (amount) => parseUnits(String(amount), USDC_DECIMALS);
|
|
39
|
+
/** Convert USDC base units back to human-readable string */
|
|
40
|
+
export const formatUsdc = (amount) => formatUnits(amount, USDC_DECIMALS);
|
|
41
|
+
|
|
42
|
+
// ─── EIP-712 domain builder ───────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Builds the EIP-712 domain object for OpenshardDeposits contract.
|
|
46
|
+
* @param {string} contractAddress - deployed OpenshardDeposits address
|
|
47
|
+
* @param {number} chainId
|
|
48
|
+
*/
|
|
49
|
+
export function buildDomain(contractAddress, chainId) {
|
|
50
|
+
return {
|
|
51
|
+
name: 'OpenShardPayments',
|
|
52
|
+
version: '1',
|
|
53
|
+
chainId,
|
|
54
|
+
verifyingContract: contractAddress
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Type definitions (viem format) ──────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export const RESERVE_AUTH_TYPES = {
|
|
61
|
+
ReserveAuth: [
|
|
62
|
+
{ name: 'channelId', type: 'bytes32' },
|
|
63
|
+
{ name: 'buyer', type: 'address' },
|
|
64
|
+
{ name: 'seller', type: 'address' },
|
|
65
|
+
{ name: 'maxAmount', type: 'uint256' },
|
|
66
|
+
{ name: 'deadline', type: 'uint256' }
|
|
67
|
+
]
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const SPENDING_AUTH_TYPES = {
|
|
71
|
+
SpendingAuth: [
|
|
72
|
+
{ name: 'channelId', type: 'bytes32' },
|
|
73
|
+
{ name: 'cumulativeAmount', type: 'uint256' },
|
|
74
|
+
{ name: 'metadataHash', type: 'bytes32' }
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ─── Channel ID generation ────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Deterministic channel ID derived from buyer + seller addresses and a nonce.
|
|
82
|
+
* @param {string} buyerAddress
|
|
83
|
+
* @param {string} sellerAddress
|
|
84
|
+
* @param {number} nonce - monotonically increasing per buyer-seller pair
|
|
85
|
+
* @returns {`0x${string}`} bytes32 hex
|
|
86
|
+
*/
|
|
87
|
+
export function buildChannelId(buyerAddress, sellerAddress, nonce) {
|
|
88
|
+
return keccak256(
|
|
89
|
+
encodeAbiParameters(
|
|
90
|
+
parseAbiParameters('address buyer, address seller, uint256 nonce'),
|
|
91
|
+
[buyerAddress, sellerAddress, BigInt(nonce)]
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Metadata hash ────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the metadata hash included in each SpendingAuth.
|
|
100
|
+
* Captures the token counts and request fingerprint.
|
|
101
|
+
*
|
|
102
|
+
* @param {{ inputTokens: number, outputTokens: number, requestId: string }} meta
|
|
103
|
+
* @returns {`0x${string}`}
|
|
104
|
+
*/
|
|
105
|
+
export function buildMetadataHash({ inputTokens, outputTokens, requestId }) {
|
|
106
|
+
return keccak256(
|
|
107
|
+
encodeAbiParameters(
|
|
108
|
+
parseAbiParameters('uint256 inputTokens, uint256 outputTokens, string requestId'),
|
|
109
|
+
[BigInt(inputTokens), BigInt(outputTokens), requestId]
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Signing (buyer side) ─────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Sign a ReserveAuth message with the buyer's private key.
|
|
118
|
+
* The seller will submit this signature on-chain to lock buyer funds.
|
|
119
|
+
*
|
|
120
|
+
* @param {{ privateKey: string, contractAddress: string, chainId: number }} opts
|
|
121
|
+
* @param {{ channelId: string, seller: string, maxAmount: bigint, deadline: bigint }} params
|
|
122
|
+
* @returns {Promise<string>} EIP-712 hex signature
|
|
123
|
+
*/
|
|
124
|
+
export async function signReserveAuth({ privateKey, contractAddress, chainId }, params) {
|
|
125
|
+
const account = privateKeyToAccount(privateKey);
|
|
126
|
+
const domain = buildDomain(contractAddress, chainId);
|
|
127
|
+
|
|
128
|
+
return account.signTypedData({
|
|
129
|
+
domain,
|
|
130
|
+
types: RESERVE_AUTH_TYPES,
|
|
131
|
+
primaryType: 'ReserveAuth',
|
|
132
|
+
message: {
|
|
133
|
+
channelId: params.channelId,
|
|
134
|
+
buyer: account.address,
|
|
135
|
+
seller: params.seller,
|
|
136
|
+
maxAmount: params.maxAmount,
|
|
137
|
+
deadline: params.deadline
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Sign a SpendingAuth message.
|
|
144
|
+
* Called by the buyer after each successful inference request.
|
|
145
|
+
*
|
|
146
|
+
* @param {{ privateKey: string, contractAddress: string, chainId: number }} opts
|
|
147
|
+
* @param {{ channelId: string, cumulativeAmount: bigint, metadataHash: string }} params
|
|
148
|
+
* @returns {Promise<string>} EIP-712 hex signature
|
|
149
|
+
*/
|
|
150
|
+
export async function signSpendingAuth({ privateKey, contractAddress, chainId }, params) {
|
|
151
|
+
const account = privateKeyToAccount(privateKey);
|
|
152
|
+
const domain = buildDomain(contractAddress, chainId);
|
|
153
|
+
|
|
154
|
+
return account.signTypedData({
|
|
155
|
+
domain,
|
|
156
|
+
types: SPENDING_AUTH_TYPES,
|
|
157
|
+
primaryType: 'SpendingAuth',
|
|
158
|
+
message: {
|
|
159
|
+
channelId: params.channelId,
|
|
160
|
+
cumulativeAmount: params.cumulativeAmount,
|
|
161
|
+
metadataHash: params.metadataHash
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Verification (seller side) ───────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Recover the signer address from a ReserveAuth signature.
|
|
170
|
+
* Seller uses this to verify the buyer's authorization before calling reserve().
|
|
171
|
+
*
|
|
172
|
+
* @param {{ contractAddress: string, chainId: number }} opts
|
|
173
|
+
* @param {{ channelId: string, buyer: string, seller: string, maxAmount: bigint, deadline: bigint }} params
|
|
174
|
+
* @param {string} signature
|
|
175
|
+
* @returns {Promise<string>} recovered signer address
|
|
176
|
+
*/
|
|
177
|
+
export async function recoverReserveAuthSigner({ contractAddress, chainId }, params, signature) {
|
|
178
|
+
return recoverTypedDataAddress({
|
|
179
|
+
domain: buildDomain(contractAddress, chainId),
|
|
180
|
+
types: RESERVE_AUTH_TYPES,
|
|
181
|
+
primaryType: 'ReserveAuth',
|
|
182
|
+
message: params,
|
|
183
|
+
signature
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Recover the signer address from a SpendingAuth signature.
|
|
189
|
+
*
|
|
190
|
+
* @param {{ contractAddress: string, chainId: number }} opts
|
|
191
|
+
* @param {{ channelId: string, cumulativeAmount: bigint, metadataHash: string }} params
|
|
192
|
+
* @param {string} signature
|
|
193
|
+
* @returns {Promise<string>} recovered signer address
|
|
194
|
+
*/
|
|
195
|
+
export async function recoverSpendingAuthSigner({ contractAddress, chainId }, params, signature) {
|
|
196
|
+
return recoverTypedDataAddress({
|
|
197
|
+
domain: buildDomain(contractAddress, chainId),
|
|
198
|
+
types: SPENDING_AUTH_TYPES,
|
|
199
|
+
primaryType: 'SpendingAuth',
|
|
200
|
+
message: params,
|
|
201
|
+
signature
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Chain helpers ────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export const CHAINS = {
|
|
208
|
+
'base': base,
|
|
209
|
+
'base-sepolia': baseSepolia,
|
|
210
|
+
'localhost': hardhat
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export function getChain(name) {
|
|
214
|
+
const chain = CHAINS[name];
|
|
215
|
+
if (!chain) throw new Error(`Unknown chain: ${name}. Supported: ${Object.keys(CHAINS).join(', ')}`);
|
|
216
|
+
return chain;
|
|
217
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openshard_ggshared — prometheus.js
|
|
3
|
+
*
|
|
4
|
+
* Minimal Prometheus text format builder.
|
|
5
|
+
* No external dependencies — just string building.
|
|
6
|
+
*
|
|
7
|
+
* Supports: Counter, Gauge, Histogram
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ─── Metric types ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
class Counter {
|
|
13
|
+
constructor(name, help, labelNames = []) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
this.help = help;
|
|
16
|
+
this.labelNames = labelNames;
|
|
17
|
+
this._values = new Map();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_key(labels) { return JSON.stringify(labels ?? {}); }
|
|
21
|
+
|
|
22
|
+
inc(labels = {}, value = 1) {
|
|
23
|
+
const k = this._key(labels);
|
|
24
|
+
this._values.set(k, (this._values.get(k) ?? 0) + value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
render() {
|
|
28
|
+
const lines = [
|
|
29
|
+
`# HELP ${this.name} ${this.help}`,
|
|
30
|
+
`# TYPE ${this.name} counter`
|
|
31
|
+
];
|
|
32
|
+
for (const [key, val] of this._values) {
|
|
33
|
+
const labels = JSON.parse(key);
|
|
34
|
+
lines.push(`${this.name}${renderLabels(labels)} ${val}`);
|
|
35
|
+
}
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class Gauge {
|
|
41
|
+
constructor(name, help, labelNames = []) {
|
|
42
|
+
this.name = name;
|
|
43
|
+
this.help = help;
|
|
44
|
+
this.labelNames = labelNames;
|
|
45
|
+
this._values = new Map();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_key(labels) { return JSON.stringify(labels ?? {}); }
|
|
49
|
+
|
|
50
|
+
set(labels = {}, value) {
|
|
51
|
+
this._values.set(this._key(labels), value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
inc(labels = {}, value = 1) {
|
|
55
|
+
const k = this._key(labels);
|
|
56
|
+
this._values.set(k, (this._values.get(k) ?? 0) + value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
dec(labels = {}, value = 1) {
|
|
60
|
+
const k = this._key(labels);
|
|
61
|
+
this._values.set(k, (this._values.get(k) ?? 0) - value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
render() {
|
|
65
|
+
const lines = [
|
|
66
|
+
`# HELP ${this.name} ${this.help}`,
|
|
67
|
+
`# TYPE ${this.name} gauge`
|
|
68
|
+
];
|
|
69
|
+
for (const [key, val] of this._values) {
|
|
70
|
+
const labels = JSON.parse(key);
|
|
71
|
+
lines.push(`${this.name}${renderLabels(labels)} ${val}`);
|
|
72
|
+
}
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class Histogram {
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} name
|
|
80
|
+
* @param {string} help
|
|
81
|
+
* @param {number[]} buckets - upper bounds in ascending order
|
|
82
|
+
*/
|
|
83
|
+
constructor(name, help, buckets = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]) {
|
|
84
|
+
this.name = name;
|
|
85
|
+
this.help = help;
|
|
86
|
+
this.buckets = [...buckets, Infinity];
|
|
87
|
+
this.labelNames = [];
|
|
88
|
+
this._data = new Map(); // labelKey → { counts[], sum, total }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_key(labels) { return JSON.stringify(labels ?? {}); }
|
|
92
|
+
|
|
93
|
+
observe(labels = {}, value) {
|
|
94
|
+
const k = this._key(labels);
|
|
95
|
+
let d = this._data.get(k);
|
|
96
|
+
if (!d) {
|
|
97
|
+
d = { counts: new Array(this.buckets.length).fill(0), sum: 0, total: 0 };
|
|
98
|
+
this._data.set(k, d);
|
|
99
|
+
}
|
|
100
|
+
for (let i = 0; i < this.buckets.length; i++) {
|
|
101
|
+
if (value <= this.buckets[i]) d.counts[i]++;
|
|
102
|
+
}
|
|
103
|
+
d.sum += value;
|
|
104
|
+
d.total += 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
render() {
|
|
108
|
+
const lines = [
|
|
109
|
+
`# HELP ${this.name} ${this.help}`,
|
|
110
|
+
`# TYPE ${this.name} histogram`
|
|
111
|
+
];
|
|
112
|
+
for (const [key, d] of this._data) {
|
|
113
|
+
const labels = JSON.parse(key);
|
|
114
|
+
const baseLabel = renderLabels(labels);
|
|
115
|
+
for (let i = 0; i < this.buckets.length; i++) {
|
|
116
|
+
const le = this.buckets[i] === Infinity ? '+Inf' : String(this.buckets[i]);
|
|
117
|
+
const bucketLabels = renderLabels({ ...labels, le });
|
|
118
|
+
lines.push(`${this.name}_bucket${bucketLabels} ${d.counts[i]}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push(`${this.name}_sum${baseLabel} ${d.sum}`);
|
|
121
|
+
lines.push(`${this.name}_count${baseLabel} ${d.total}`);
|
|
122
|
+
}
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
export class PrometheusRegistry {
|
|
130
|
+
constructor() {
|
|
131
|
+
this._metrics = [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
counter(name, help, labelNames = []) {
|
|
135
|
+
const m = new Counter(name, help, labelNames);
|
|
136
|
+
this._metrics.push(m);
|
|
137
|
+
return m;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
gauge(name, help, labelNames = []) {
|
|
141
|
+
const m = new Gauge(name, help, labelNames);
|
|
142
|
+
this._metrics.push(m);
|
|
143
|
+
return m;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
histogram(name, help, buckets, labelNames = []) {
|
|
147
|
+
const m = new Histogram(name, help, buckets);
|
|
148
|
+
m.labelNames = labelNames;
|
|
149
|
+
this._metrics.push(m);
|
|
150
|
+
return m;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Render all metrics in Prometheus text format.
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
format() {
|
|
158
|
+
return this._metrics.map(m => m.render()).join('\n\n') + '\n';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
function renderLabels(obj) {
|
|
165
|
+
const entries = Object.entries(obj);
|
|
166
|
+
if (entries.length === 0) return '';
|
|
167
|
+
const inner = entries.map(([k, v]) => `${k}="${String(v).replace(/"/g, '\\"')}"`).join(',');
|
|
168
|
+
return `{${inner}}`;
|
|
169
|
+
}
|