@humanode-hyperlane/metrics 0.0.0-stage → 0.2.8

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.
Files changed (53) hide show
  1. package/LICENSE.md +193 -0
  2. package/dist/balance.d.ts +93 -0
  3. package/dist/balance.d.ts.map +1 -0
  4. package/dist/balance.js +235 -0
  5. package/dist/balance.js.map +1 -0
  6. package/dist/gauges.d.ts +41 -0
  7. package/dist/gauges.d.ts.map +1 -0
  8. package/dist/gauges.js +100 -0
  9. package/dist/gauges.js.map +1 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +13 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/index.test.d.ts +2 -0
  15. package/dist/index.test.d.ts.map +1 -0
  16. package/dist/index.test.js +45 -0
  17. package/dist/index.test.js.map +1 -0
  18. package/dist/pushgateway.d.ts +23 -0
  19. package/dist/pushgateway.d.ts.map +1 -0
  20. package/dist/pushgateway.js +54 -0
  21. package/dist/pushgateway.js.map +1 -0
  22. package/dist/server.d.ts +15 -0
  23. package/dist/server.d.ts.map +1 -0
  24. package/dist/server.js +63 -0
  25. package/dist/server.js.map +1 -0
  26. package/dist/server.test.d.ts +2 -0
  27. package/dist/server.test.d.ts.map +1 -0
  28. package/dist/server.test.js +23 -0
  29. package/dist/server.test.js.map +1 -0
  30. package/dist/types.d.ts +64 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +2 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/update.d.ts +51 -0
  35. package/dist/update.d.ts.map +1 -0
  36. package/dist/update.js +163 -0
  37. package/dist/update.js.map +1 -0
  38. package/dist/utils.d.ts +7 -0
  39. package/dist/utils.d.ts.map +1 -0
  40. package/dist/utils.js +9 -0
  41. package/dist/utils.js.map +1 -0
  42. package/package.json +52 -3
  43. package/src/balance.ts +380 -0
  44. package/src/gauges.ts +136 -0
  45. package/src/index.test.ts +52 -0
  46. package/src/index.ts +53 -0
  47. package/src/pushgateway.ts +66 -0
  48. package/src/server.test.ts +32 -0
  49. package/src/server.ts +76 -0
  50. package/src/types.ts +74 -0
  51. package/src/update.ts +239 -0
  52. package/src/utils.ts +10 -0
  53. package/README.md +0 -3
package/src/gauges.ts ADDED
@@ -0,0 +1,136 @@
1
+ import { Gauge, type Registry } from 'prom-client';
2
+
3
+ import type {
4
+ WarpRouteMetricLabels,
5
+ WarpRouteValueAtRiskMetricLabels,
6
+ } from './types.js';
7
+
8
+ /**
9
+ * Label names for warp route metrics.
10
+ */
11
+ export const warpRouteMetricLabels: (keyof WarpRouteMetricLabels)[] = [
12
+ 'chain_name',
13
+ 'token_address',
14
+ 'token_name',
15
+ 'wallet_address',
16
+ 'token_standard',
17
+ 'warp_route_id',
18
+ 'related_chain_names',
19
+ ];
20
+
21
+ /**
22
+ * Label names for value at risk metrics.
23
+ */
24
+ export const warpRouteValueAtRiskMetricLabels: (keyof WarpRouteValueAtRiskMetricLabels)[] =
25
+ [
26
+ 'chain_name',
27
+ 'collateral_chain_name',
28
+ 'token_address',
29
+ 'token_name',
30
+ 'collateral_token_standard',
31
+ 'warp_route_id',
32
+ ];
33
+
34
+ /**
35
+ * Wallet balance metric label names.
36
+ */
37
+ export const walletBalanceMetricLabels = [
38
+ 'chain',
39
+ 'wallet_address',
40
+ 'wallet_name',
41
+ 'token_address',
42
+ 'token_symbol',
43
+ 'token_name',
44
+ ] as const;
45
+
46
+ /**
47
+ * xERC20 limits metric label names.
48
+ */
49
+ export const xERC20LimitsMetricLabels = [
50
+ 'chain_name',
51
+ 'limit_type',
52
+ 'token_name',
53
+ 'bridge_address',
54
+ 'token_address',
55
+ 'bridge_label',
56
+ ] as const;
57
+
58
+ /**
59
+ * Collection of warp route Prometheus gauges.
60
+ */
61
+ export interface WarpMetricsGauges {
62
+ warpRouteTokenBalance: Gauge<string>;
63
+ warpRouteCollateralValue: Gauge<string>;
64
+ warpRouteValueAtRisk: Gauge<string>;
65
+ walletBalanceGauge: Gauge<string>;
66
+ xERC20LimitsGauge: Gauge<string>;
67
+ }
68
+
69
+ /**
70
+ * Creates a new wallet balance gauge with optional additional labels.
71
+ */
72
+ export function createWalletBalanceGauge(
73
+ registry: Registry,
74
+ additionalLabels: string[] = [],
75
+ ): Gauge<string> {
76
+ return new Gauge({
77
+ // Mirror the rust/main/ethers-prometheus `wallet_balance` gauge metric.
78
+ name: 'hyperlane_wallet_balance',
79
+ help: 'Current balance of a wallet for a token',
80
+ registers: [registry],
81
+ labelNames: [...walletBalanceMetricLabels, ...additionalLabels],
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Creates all warp route metric gauges and registers them with the provided registry.
87
+ *
88
+ * @param registry - The Prometheus registry to register the gauges with
89
+ * @param walletBalanceAdditionalLabels - Optional additional labels for the wallet balance gauge
90
+ * @returns Collection of warp metric gauges
91
+ */
92
+ export function createWarpMetricsGauges(
93
+ registry: Registry,
94
+ walletBalanceAdditionalLabels: string[] = [],
95
+ ): WarpMetricsGauges {
96
+ const warpRouteTokenBalance = new Gauge({
97
+ name: 'hyperlane_warp_route_token_balance',
98
+ help: 'HypERC20 token balance of a Warp Route',
99
+ registers: [registry],
100
+ labelNames: warpRouteMetricLabels,
101
+ });
102
+
103
+ const warpRouteCollateralValue = new Gauge({
104
+ name: 'hyperlane_warp_route_collateral_value',
105
+ help: 'Total value of collateral held in a HypERC20Collateral or HypNative contract of a Warp Route',
106
+ registers: [registry],
107
+ labelNames: warpRouteMetricLabels,
108
+ });
109
+
110
+ const warpRouteValueAtRisk = new Gauge({
111
+ name: 'hyperlane_warp_route_value_at_risk',
112
+ help: 'Value at risk on chain for a given Warp Route',
113
+ registers: [registry],
114
+ labelNames: warpRouteValueAtRiskMetricLabels,
115
+ });
116
+
117
+ const walletBalanceGauge = createWalletBalanceGauge(
118
+ registry,
119
+ walletBalanceAdditionalLabels,
120
+ );
121
+
122
+ const xERC20LimitsGauge = new Gauge({
123
+ name: 'hyperlane_xerc20_limits',
124
+ help: 'Current minting and burning limits of xERC20 tokens',
125
+ registers: [registry],
126
+ labelNames: xERC20LimitsMetricLabels,
127
+ });
128
+
129
+ return {
130
+ warpRouteTokenBalance,
131
+ warpRouteCollateralValue,
132
+ warpRouteValueAtRisk,
133
+ walletBalanceGauge,
134
+ xERC20LimitsGauge,
135
+ };
136
+ }
@@ -0,0 +1,52 @@
1
+ import { expect } from 'chai';
2
+ import { Registry } from 'prom-client';
3
+
4
+ import { createWarpMetricsGauges } from './gauges.js';
5
+
6
+ describe('Warp Metrics', () => {
7
+ describe('createWarpMetricsGauges', () => {
8
+ it('should create all required gauges', () => {
9
+ const registry = new Registry();
10
+ const gauges = createWarpMetricsGauges(registry);
11
+
12
+ expect(gauges).to.have.property('warpRouteTokenBalance');
13
+ expect(gauges).to.have.property('warpRouteCollateralValue');
14
+ expect(gauges).to.have.property('warpRouteValueAtRisk');
15
+ expect(gauges).to.have.property('walletBalanceGauge');
16
+ expect(gauges).to.have.property('xERC20LimitsGauge');
17
+ });
18
+
19
+ it('should register gauges with the provided registry', async () => {
20
+ const registry = new Registry();
21
+ createWarpMetricsGauges(registry);
22
+
23
+ const metrics = await registry.metrics();
24
+ expect(metrics).to.include('hyperlane_warp_route_token_balance');
25
+ expect(metrics).to.include('hyperlane_warp_route_collateral_value');
26
+ expect(metrics).to.include('hyperlane_warp_route_value_at_risk');
27
+ expect(metrics).to.include('hyperlane_wallet_balance');
28
+ expect(metrics).to.include('hyperlane_xerc20_limits');
29
+ });
30
+
31
+ it('should support additional wallet balance labels', async () => {
32
+ const registry = new Registry();
33
+ const gauges = createWarpMetricsGauges(registry, ['warp_route_id']);
34
+
35
+ // Set a value to verify the label is accepted
36
+ gauges.walletBalanceGauge
37
+ .labels({
38
+ chain: 'ethereum',
39
+ wallet_address: '0x123',
40
+ wallet_name: 'test',
41
+ token_address: 'native',
42
+ token_symbol: 'ETH',
43
+ token_name: 'Ether',
44
+ warp_route_id: 'test-route',
45
+ })
46
+ .set(1.5);
47
+
48
+ const metrics = await registry.metrics();
49
+ expect(metrics).to.include('warp_route_id="test-route"');
50
+ });
51
+ });
52
+ });
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ // Types
2
+ export {
3
+ type BaseWarpRouteMetricLabels,
4
+ type NativeWalletBalance,
5
+ type SupportedTokenStandards,
6
+ type WarpRouteBalance,
7
+ type WarpRouteMetricLabels,
8
+ type WarpRouteValueAtRiskMetricLabels,
9
+ type XERC20Info,
10
+ type XERC20Limit,
11
+ } from './types.js';
12
+
13
+ // Gauges and factory
14
+ export {
15
+ createWalletBalanceGauge,
16
+ createWarpMetricsGauges,
17
+ type WarpMetricsGauges,
18
+ walletBalanceMetricLabels,
19
+ warpRouteMetricLabels,
20
+ warpRouteValueAtRiskMetricLabels,
21
+ xERC20LimitsMetricLabels,
22
+ } from './gauges.js';
23
+
24
+ // Metric update functions
25
+ export {
26
+ updateManagedLockboxBalanceMetrics,
27
+ updateNativeWalletBalanceMetrics,
28
+ updateTokenBalanceMetrics,
29
+ updateXERC20LimitsMetrics,
30
+ } from './update.js';
31
+
32
+ // Balance utilities
33
+ export {
34
+ getExtraLockboxBalance,
35
+ getExtraLockboxInfo,
36
+ getManagedLockBox,
37
+ getManagedLockBoxCollateralInfo,
38
+ getSealevelAtaPayerBalance,
39
+ getTokenBridgedBalance,
40
+ getXERC20Info,
41
+ getXERC20Limit,
42
+ MANAGED_LOCKBOX_MINIMAL_ABI,
43
+ type TokenPriceGetter,
44
+ } from './balance.js';
45
+
46
+ // Utilities
47
+ export { formatBigInt } from './utils.js';
48
+
49
+ // Server
50
+ export { startMetricsServer } from './server.js';
51
+
52
+ // Push Gateway
53
+ export { getPushGateway, submitMetrics } from './pushgateway.js';
@@ -0,0 +1,66 @@
1
+ import type { Logger } from 'pino';
2
+ import { Pushgateway, type Registry } from 'prom-client';
3
+ import { format } from 'util';
4
+
5
+ import { rootLogger } from '@humanode-hyperlane/utils';
6
+
7
+ /**
8
+ * Gets the push gateway if PROMETHEUS_PUSH_GATEWAY environment variable is set.
9
+ *
10
+ * @param register - The Prometheus registry
11
+ * @param logger - Optional logger instance
12
+ * @returns The Pushgateway instance or null if not configured
13
+ */
14
+ export function getPushGateway(
15
+ register: Registry,
16
+ logger?: Logger,
17
+ ): Pushgateway | null {
18
+ const log = logger ?? rootLogger.child({ module: 'metrics' });
19
+ const gatewayAddr = process.env['PROMETHEUS_PUSH_GATEWAY'];
20
+ if (gatewayAddr) {
21
+ return new Pushgateway(gatewayAddr, [], register);
22
+ } else {
23
+ log.warn(
24
+ 'Prometheus push gateway address was not defined; not publishing metrics.',
25
+ );
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Submits metrics to a Prometheus push gateway.
32
+ *
33
+ * @param register - The Prometheus registry to submit
34
+ * @param jobName - The job name for the metrics
35
+ * @param options - Optional configuration
36
+ * @param options.overwriteAllMetrics - If true, overwrites all metrics instead of adding
37
+ * @param logger - Optional logger instance
38
+ */
39
+ export async function submitMetrics(
40
+ register: Registry,
41
+ jobName: string,
42
+ options?: { overwriteAllMetrics?: boolean },
43
+ logger?: Logger,
44
+ ): Promise<void> {
45
+ const log = logger ?? rootLogger.child({ module: 'metrics' });
46
+ const gateway = getPushGateway(register, log);
47
+ if (!gateway) return;
48
+
49
+ let resp;
50
+ try {
51
+ if (options?.overwriteAllMetrics) {
52
+ resp = (await gateway.push({ jobName })).resp;
53
+ } else {
54
+ resp = (await gateway.pushAdd({ jobName })).resp;
55
+ }
56
+ } catch (e) {
57
+ log.error('Error when pushing metrics', { error: format(e) });
58
+ return;
59
+ }
60
+
61
+ const statusCode =
62
+ typeof resp == 'object' && resp != null && 'statusCode' in resp
63
+ ? (resp as any).statusCode
64
+ : 'unknown';
65
+ log.info('Prometheus metrics pushed to PushGateway', { statusCode });
66
+ }
@@ -0,0 +1,32 @@
1
+ import { expect } from 'chai';
2
+ import { Registry } from 'prom-client';
3
+
4
+ import { startMetricsServer } from './server.js';
5
+
6
+ describe('startMetricsServer', () => {
7
+ const originalPort = process.env['PROMETHEUS_PORT'];
8
+
9
+ afterEach(() => {
10
+ if (originalPort === undefined) {
11
+ delete process.env['PROMETHEUS_PORT'];
12
+ } else {
13
+ process.env['PROMETHEUS_PORT'] = originalPort;
14
+ }
15
+ });
16
+
17
+ it('throws for non-numeric PROMETHEUS_PORT values', () => {
18
+ process.env['PROMETHEUS_PORT'] = '';
19
+
20
+ expect(() => startMetricsServer(new Registry())).to.throw(
21
+ /PROMETHEUS_PORT must contain only digits/i,
22
+ );
23
+ });
24
+
25
+ it('throws for out-of-range PROMETHEUS_PORT values', () => {
26
+ process.env['PROMETHEUS_PORT'] = '70000';
27
+
28
+ expect(() => startMetricsServer(new Registry())).to.throw(
29
+ /PROMETHEUS_PORT must be between 1 and 65535/i,
30
+ );
31
+ });
32
+ });
package/src/server.ts ADDED
@@ -0,0 +1,76 @@
1
+ import http from 'http';
2
+ import type { Logger } from 'pino';
3
+ import type { Registry } from 'prom-client';
4
+
5
+ /**
6
+ * Start a simple HTTP server to host metrics. This takes the registry and dumps the text
7
+ * string to people who request `GET /metrics`.
8
+ *
9
+ * PROMETHEUS_PORT env var is used to determine what port to host on, defaults to 9090.
10
+ *
11
+ * @param register - The Prometheus registry to serve metrics from
12
+ * @param logger - Optional logger instance for error logging
13
+ * @returns The HTTP server instance
14
+ */
15
+ export function startMetricsServer(
16
+ register: Registry,
17
+ logger?: Logger,
18
+ ): http.Server {
19
+ const configuredPort = process.env['PROMETHEUS_PORT'];
20
+ const portString = configuredPort ?? '9090';
21
+
22
+ if (!/^\d+$/.test(portString)) {
23
+ const message =
24
+ 'PROMETHEUS_PORT must contain only digits and be between 1 and 65535';
25
+ logger?.error({ port: configuredPort }, message);
26
+ throw new Error(message);
27
+ }
28
+
29
+ const port = Number(portString);
30
+
31
+ if (port < 1 || port > 65535) {
32
+ const message = 'PROMETHEUS_PORT must be between 1 and 65535';
33
+ logger?.error({ port }, message);
34
+ throw new Error(message);
35
+ }
36
+
37
+ const server = http
38
+ .createServer((req, res) => {
39
+ if (req.url !== '/metrics') {
40
+ res.writeHead(404, 'Invalid url').end();
41
+ return;
42
+ }
43
+ if (req.method !== 'GET') {
44
+ res.writeHead(405, 'Invalid method').end();
45
+ return;
46
+ }
47
+
48
+ register
49
+ .metrics()
50
+ .then((metricsStr) => {
51
+ res
52
+ .writeHead(200, { 'Content-Type': register.contentType })
53
+ .end(metricsStr);
54
+ })
55
+ .catch((err) => {
56
+ if (logger) {
57
+ logger.error(err, 'Failed to collect metrics');
58
+ }
59
+ res
60
+ .writeHead(500, { 'Content-Type': 'text/plain' })
61
+ .end('Internal Server Error');
62
+ });
63
+ })
64
+ .on('error', (err: NodeJS.ErrnoException) => {
65
+ if (err.code === 'EADDRINUSE') {
66
+ logger?.error(
67
+ `Metrics server port ${port} already in use. Set PROMETHEUS_PORT to use a different port.`,
68
+ );
69
+ } else {
70
+ logger?.error(err, 'Metrics server error');
71
+ }
72
+ })
73
+ .listen(port);
74
+
75
+ return server;
76
+ }
package/src/types.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { ChainName, TokenStandard } from '@humanode-hyperlane/sdk';
2
+ import type { Address } from '@humanode-hyperlane/utils';
3
+
4
+ /**
5
+ * xERC20 minting and burning limits for a token.
6
+ */
7
+ export interface XERC20Limit {
8
+ mint: number;
9
+ burn: number;
10
+ mintMax: number;
11
+ burnMax: number;
12
+ }
13
+
14
+ /**
15
+ * xERC20 information including limits and the xERC20 token address.
16
+ */
17
+ export interface XERC20Info {
18
+ limits: XERC20Limit;
19
+ xERC20Address: Address;
20
+ }
21
+
22
+ /**
23
+ * Balance information for a warp route token.
24
+ */
25
+ export interface WarpRouteBalance {
26
+ balance: number;
27
+ valueUSD?: number;
28
+ tokenAddress: Address;
29
+ }
30
+
31
+ /**
32
+ * Native wallet balance information.
33
+ */
34
+ export interface NativeWalletBalance {
35
+ chain: ChainName;
36
+ walletAddress: Address;
37
+ walletName: string;
38
+ balance: number;
39
+ }
40
+
41
+ /**
42
+ * Token standards supported by warp metrics, including pseudo-standards.
43
+ */
44
+ export type SupportedTokenStandards =
45
+ | TokenStandard
46
+ | 'EvmManagedLockbox'
47
+ | 'xERC20';
48
+
49
+ /**
50
+ * Base metric labels common to all warp route metrics.
51
+ */
52
+ export interface BaseWarpRouteMetricLabels {
53
+ chain_name: ChainName;
54
+ token_address: string;
55
+ token_name: string;
56
+ warp_route_id: string;
57
+ }
58
+
59
+ /**
60
+ * Full warp route metric labels including wallet and token standard info.
61
+ */
62
+ export interface WarpRouteMetricLabels extends BaseWarpRouteMetricLabels {
63
+ wallet_address: string;
64
+ token_standard: SupportedTokenStandards;
65
+ related_chain_names: string;
66
+ }
67
+
68
+ /**
69
+ * Value at risk metric labels.
70
+ */
71
+ export interface WarpRouteValueAtRiskMetricLabels extends BaseWarpRouteMetricLabels {
72
+ collateral_chain_name: ChainName;
73
+ collateral_token_standard: SupportedTokenStandards;
74
+ }