From db7328b154e545a384c2254c657693618d02447a Mon Sep 17 00:00:00 2001 From: 0xghost42 Date: Tue, 19 May 2026 12:05:20 +0530 Subject: [PATCH 1/2] feat(price-pusher): emit `namespace` label on all metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sample Grafana dashboard (`grafana-dashboard.sample.json`) filters every Prometheus query by `namespace=$chain`: pyth_price_feeds_total{namespace="$chain"} pyth_price_last_published_time{namespace="$chain"} pyth_price_update_attempts_total{namespace="$chain", ...} ... But `src/metrics.ts` only set `app="price_pusher"` as the default label and never emitted a `namespace` label on any counter/gauge, and `prometheus.sample.yml` did not add one via relabeling. With the dashboard out of the box that filter matched zero series, so every panel for a fresh deployment came up empty — flagged by Devin during review of #3692 and acknowledged there as pre-existing dashboard behavior worth fixing in a separate PR. This is that PR. Wiring: - new CLI option `--metrics-namespace ` in `src/options.ts`, defaulted per chain command (`evm`, `sui`, `aptos`, `solana`) so that single-chain deployments work without configuration - `PricePusherMetrics` constructor now takes a `namespace: string` parameter and sets it via `registry.setDefaultLabels`, so every existing metric series gains a `namespace` label without per-metric `labelNames` plumbing - each chain command (`evm/sui/aptos/solana/command.ts`) reads the new arg and passes through to the constructor Operators running multiple deployments of the same chain (e.g. several EVM networks against one Grafana instance) can now set `--metrics-namespace bsc-mainnet` / `--metrics-namespace polygon-mainnet` to disambiguate. Single-deployment setups keep working unchanged. Bumps `@pythnetwork/price-pusher` to `10.5.0` (additive, no breaking changes — old CLIs still work). Note: #3689 and #3703 also bump to `10.5.0`, so whichever of the three lands second/third will need a trivial rebase to `10.6.0` / `10.7.0`. Refs #3692. --- apps/price_pusher/package.json | 2 +- apps/price_pusher/src/aptos/command.ts | 7 ++++++- apps/price_pusher/src/evm/command.ts | 7 ++++++- apps/price_pusher/src/metrics.ts | 8 +++++--- apps/price_pusher/src/options.ts | 12 ++++++++++++ apps/price_pusher/src/solana/command.ts | 7 ++++++- apps/price_pusher/src/sui/command.ts | 7 ++++++- 7 files changed, 42 insertions(+), 8 deletions(-) diff --git a/apps/price_pusher/package.json b/apps/price_pusher/package.json index 79abf56c0e..20e69aff37 100644 --- a/apps/price_pusher/package.json +++ b/apps/price_pusher/package.json @@ -219,5 +219,5 @@ }, "type": "module", "types": "./dist/index.d.ts", - "version": "10.4.0" + "version": "10.5.0" } diff --git a/apps/price_pusher/src/aptos/command.ts b/apps/price_pusher/src/aptos/command.ts index ab6928b873..46b7580415 100644 --- a/apps/price_pusher/src/aptos/command.ts +++ b/apps/price_pusher/src/aptos/command.ts @@ -51,6 +51,7 @@ export default { ...options.controllerLogLevel, ...options.enableMetrics, ...options.metricsPort, + ...options.metricsNamespace, }, command: "aptos", describe: "run price pusher for aptos", @@ -70,6 +71,7 @@ export default { controllerLogLevel, enableMetrics, metricsPort, + metricsNamespace, } = argv; const logger = pino({ level: logLevel }); @@ -82,7 +84,10 @@ export default { // Initialize metrics if enabled let metrics: PricePusherMetrics | undefined; if (enableMetrics) { - metrics = new PricePusherMetrics(logger.child({ module: "Metrics" })); + metrics = new PricePusherMetrics( + logger.child({ module: "Metrics" }), + metricsNamespace ?? "aptos", + ); metrics.start(metricsPort); logger.info(`Metrics server started on port ${metricsPort}`); } diff --git a/apps/price_pusher/src/evm/command.ts b/apps/price_pusher/src/evm/command.ts index 281c9d9ab0..925d5257e7 100644 --- a/apps/price_pusher/src/evm/command.ts +++ b/apps/price_pusher/src/evm/command.ts @@ -93,6 +93,7 @@ export default { ...options.controllerLogLevel, ...options.enableMetrics, ...options.metricsPort, + ...options.metricsNamespace, }, command: "evm", describe: "run price pusher for evm", @@ -118,6 +119,7 @@ export default { controllerLogLevel, enableMetrics, metricsPort, + metricsNamespace, } = argv; const logger = pino({ @@ -150,7 +152,10 @@ export default { // Initialize metrics if enabled let metrics: PricePusherMetrics | undefined; if (enableMetrics) { - metrics = new PricePusherMetrics(logger.child({ module: "Metrics" })); + metrics = new PricePusherMetrics( + logger.child({ module: "Metrics" }), + metricsNamespace ?? "evm", + ); metrics.start(metricsPort); logger.info(`Metrics server started on port ${metricsPort}`); } diff --git a/apps/price_pusher/src/metrics.ts b/apps/price_pusher/src/metrics.ts index fb129f79e6..4f5e5d7d8e 100644 --- a/apps/price_pusher/src/metrics.ts +++ b/apps/price_pusher/src/metrics.ts @@ -23,13 +23,15 @@ export class PricePusherMetrics { // Wallet metrics public walletBalance: Gauge; - constructor(logger: Logger) { + constructor(logger: Logger, namespace: string) { this.logger = logger; this.registry = new Registry(); this.server = express(); - // Register the default metrics (memory, CPU, etc.) - this.registry.setDefaultLabels({ app: "price_pusher" }); + // Register the default metrics (memory, CPU, etc.). `namespace` is set as + // a default label so the sample Grafana dashboard's `namespace=$chain` + // filter actually matches emitted series. + this.registry.setDefaultLabels({ app: "price_pusher", namespace }); // Create metrics this.lastPublishedTime = new Gauge({ diff --git a/apps/price_pusher/src/options.ts b/apps/price_pusher/src/options.ts index 294dbd8eca..88d381270f 100644 --- a/apps/price_pusher/src/options.ts +++ b/apps/price_pusher/src/options.ts @@ -103,3 +103,15 @@ export const metricsPort = { type: "number", } as Options, }; + +export const metricsNamespace = { + "metrics-namespace": { + description: + "Value emitted as the `namespace` label on every Prometheus metric. " + + "The sample Grafana dashboard filters by `namespace=$chain`, so set this " + + "to your deployment's chain/network name (e.g. `bsc-mainnet`, `sui-testnet`). " + + "Defaults to the chain command name (e.g. `evm`, `sui`, `aptos`, `solana`).", + type: "string", + required: false, + } as Options, +}; diff --git a/apps/price_pusher/src/solana/command.ts b/apps/price_pusher/src/solana/command.ts index 8f3b13163c..b8a98ede89 100644 --- a/apps/price_pusher/src/solana/command.ts +++ b/apps/price_pusher/src/solana/command.ts @@ -124,6 +124,7 @@ export default { ...options.controllerLogLevel, ...options.enableMetrics, ...options.metricsPort, + ...options.metricsNamespace, }, command: "solana", describe: "run price pusher for solana", @@ -153,6 +154,7 @@ export default { metricsPort, pythReceiverProgramId, wormholeProgramId, + metricsNamespace, } = argv; const logger = pino({ level: logLevel }); @@ -166,7 +168,10 @@ export default { // Initialize metrics if enabled let metrics: PricePusherMetrics | undefined; if (enableMetrics) { - metrics = new PricePusherMetrics(logger.child({ module: "Metrics" })); + metrics = new PricePusherMetrics( + logger.child({ module: "Metrics" }), + metricsNamespace ?? "solana", + ); metrics.start(metricsPort); logger.info(`Metrics server started on port ${metricsPort}`); } diff --git a/apps/price_pusher/src/sui/command.ts b/apps/price_pusher/src/sui/command.ts index 270a1b936e..3db58d17da 100644 --- a/apps/price_pusher/src/sui/command.ts +++ b/apps/price_pusher/src/sui/command.ts @@ -79,6 +79,7 @@ export default { ...options.controllerLogLevel, ...options.enableMetrics, ...options.metricsPort, + ...options.metricsNamespace, }, command: "sui", describe: @@ -105,6 +106,7 @@ export default { controllerLogLevel, enableMetrics, metricsPort, + metricsNamespace, } = argv; const logger = pino({ level: logLevel }); @@ -141,7 +143,10 @@ export default { // Initialize metrics if enabled let metrics: PricePusherMetrics | undefined; if (enableMetrics) { - metrics = new PricePusherMetrics(logger.child({ module: "Metrics" })); + metrics = new PricePusherMetrics( + logger.child({ module: "Metrics" }), + metricsNamespace ?? "sui", + ); metrics.start(metricsPort); logger.info(`Metrics server started on port ${metricsPort}`); } From 0ad092f4996b86bd5489e5598761af50b9c25006 Mon Sep 17 00:00:00 2001 From: 0xghost42 Date: Wed, 20 May 2026 12:54:44 +0530 Subject: [PATCH 2/2] fix(price-pusher): rename metric label `namespace` -> `chain` Devin re-review on this PR pointed out that `namespace` is the label Kubernetes Prometheus service discovery auto-applies to scraped pods (it carries the pod's k8s namespace). With the default `honor_labels: false` on the scrape config, my label would be renamed to `exported_namespace` and the dashboard's `namespace=\$chain` filter would silently break in any in-cluster deployment. With `honor_labels: true` the operator wins but loses k8s namespace context. Renamed everywhere to `chain`, which is what the dashboard's template variable was already called and avoids the collision entirely. Renames applied: - CLI option `--metrics-namespace` -> `--metrics-chain` (option name + variable identifier + help text in `apps/price_pusher/src/options.ts`) - Constructor arg `PricePusherMetrics(logger, namespace)` -> `PricePusherMetrics(logger, chain)` (+ comment noting why the obvious name was avoided) - Default-label key in `registry.setDefaultLabels` from `namespace` to `chain` - Per-chain callsites in `apps/price_pusher/src/{evm,sui,aptos,solana}/command.ts` swap `options.metricsNamespace` / `metricsNamespace` to the new name - `apps/price_pusher/grafana-dashboard.sample.json` panel queries (`namespace=\"$chain\"` -> `chain=\"$chain\"`, Loki `{namespace=~...}` -> `{chain=~...}`, `legendFormat: {{namespace}}` -> `{{chain}}`) and the template variable's source label (`label: 'namespace'` -> `label: 'chain'` on the Loki query that populates `$chain`) Same semantics, less ambiguous label. Refs #3692. --- .../grafana-dashboard.sample.json | 34 +++++++++---------- apps/price_pusher/src/aptos/command.ts | 6 ++-- apps/price_pusher/src/evm/command.ts | 6 ++-- apps/price_pusher/src/metrics.ts | 14 +++++--- apps/price_pusher/src/options.ts | 12 ++++--- apps/price_pusher/src/solana/command.ts | 6 ++-- apps/price_pusher/src/sui/command.ts | 6 ++-- 7 files changed, 45 insertions(+), 39 deletions(-) diff --git a/apps/price_pusher/grafana-dashboard.sample.json b/apps/price_pusher/grafana-dashboard.sample.json index b4780fa196..0a6a0b0bf7 100644 --- a/apps/price_pusher/grafana-dashboard.sample.json +++ b/apps/price_pusher/grafana-dashboard.sample.json @@ -90,9 +90,9 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "pyth_price_feeds_total{namespace=\"$chain\"}", + "expr": "pyth_price_feeds_total{chain=\"$chain\"}", "instant": true, - "legendFormat": "{{namespace}}", + "legendFormat": "{{chain}}", "range": false, "refId": "A" } @@ -169,9 +169,9 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "count(pyth_price_last_published_time{namespace=\"$chain\"})", + "expr": "count(pyth_price_last_published_time{chain=\"$chain\"})", "instant": true, - "legendFormat": "{{namespace}}", + "legendFormat": "{{chain}}", "range": false, "refId": "A" } @@ -252,7 +252,7 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "time() - pyth_price_last_published_time{namespace=\"$chain\"}", + "expr": "time() - pyth_price_last_published_time{chain=\"$chain\"}", "instant": true, "legendFormat": "{{alias}}", "range": false, @@ -383,7 +383,7 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "pyth_price_last_published_time{namespace=\"$chain\"}", + "expr": "pyth_price_last_published_time{chain=\"$chain\"}", "format": "table", "instant": true, "legendFormat": "__auto", @@ -396,7 +396,7 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "time() - pyth_price_last_published_time{namespace=\"$chain\"}", + "expr": "time() - pyth_price_last_published_time{chain=\"$chain\"}", "format": "table", "instant": true, "legendFormat": "__auto", @@ -409,7 +409,7 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "pyth_price_update_attempts_total{status=\"success\", namespace=\"$chain\"}", + "expr": "pyth_price_update_attempts_total{status=\"success\", chain=\"$chain\"}", "format": "table", "instant": true, "legendFormat": "__auto", @@ -606,7 +606,7 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "increase(pyth_price_update_attempts_total{status=\"success\", namespace=\"$chain\"}[$__range])", + "expr": "increase(pyth_price_update_attempts_total{status=\"success\", chain=\"$chain\"}[$__range])", "legendFormat": "{{alias}} - Updates", "range": true, "refId": "A" @@ -732,7 +732,7 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "sum by (trigger) (increase(pyth_price_update_attempts_total{namespace=\"$chain\"}[$__range]))", + "expr": "sum by (trigger) (increase(pyth_price_update_attempts_total{chain=\"$chain\"}[$__range]))", "instant": false, "legendFormat": "{{trigger}}", "range": true, @@ -819,7 +819,7 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "pyth_wallet_balance{namespace=\"$chain\"}", + "expr": "pyth_wallet_balance{chain=\"$chain\"}", "instant": true, "legendFormat": "{{wallet_address}}", "range": false, @@ -915,7 +915,7 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "pyth_wallet_balance{namespace=\"$chain\"}", + "expr": "pyth_wallet_balance{chain=\"$chain\"}", "legendFormat": "{{wallet_address}} ({{network}})", "range": true, "refId": "A" @@ -1024,7 +1024,7 @@ "uid": "edryyydtht14wa" }, "editorMode": "code", - "expr": "increase(pyth_price_update_attempts_total{status=\"error\", namespace=\"$chain\"}[$__range])", + "expr": "increase(pyth_price_update_attempts_total{status=\"error\", chain=\"$chain\"}[$__range])", "legendFormat": "{{alias}} - Errors", "range": true, "refId": "A" @@ -1077,7 +1077,7 @@ "uid": "ads9ouz3jh4hsa" }, "editorMode": "code", - "expr": "{namespace=~\"$chain\"} | logfmt | json | msg =~ `.*(Price update successful|Transaction confirmed|Successfully updated price).*` | line_format `Tx Hash: {{.hash}}`", + "expr": "{chain=~\"$chain\"} | logfmt | json | msg =~ `.*(Price update successful|Transaction confirmed|Successfully updated price).*` | line_format `Tx Hash: {{.hash}}`", "queryType": "range", "refId": "A" } @@ -1127,7 +1127,7 @@ "uid": "ads9ouz3jh4hsa" }, "editorMode": "code", - "expr": "{namespace=~\"$chain\"} | logfmt", + "expr": "{chain=~\"$chain\"} | logfmt", "queryType": "range", "refId": "A" } @@ -1164,7 +1164,7 @@ "uid": "ads9ouz3jh4hsa" }, "editorMode": "builder", - "expr": "{namespace=~\"$chain\"} | logfmt | detected_level = `error`", + "expr": "{chain=~\"$chain\"} | logfmt | detected_level = `error`", "queryType": "range", "refId": "A" } @@ -1202,7 +1202,7 @@ "name": "chain", "options": [], "query": { - "label": "namespace", + "label": "chain", "refId": "LokiVariableQueryEditor-VariableQuery", "stream": "", "type": 1 diff --git a/apps/price_pusher/src/aptos/command.ts b/apps/price_pusher/src/aptos/command.ts index 46b7580415..dfbde2e271 100644 --- a/apps/price_pusher/src/aptos/command.ts +++ b/apps/price_pusher/src/aptos/command.ts @@ -51,7 +51,7 @@ export default { ...options.controllerLogLevel, ...options.enableMetrics, ...options.metricsPort, - ...options.metricsNamespace, + ...options.metricsChain, }, command: "aptos", describe: "run price pusher for aptos", @@ -71,7 +71,7 @@ export default { controllerLogLevel, enableMetrics, metricsPort, - metricsNamespace, + metricsChain, } = argv; const logger = pino({ level: logLevel }); @@ -86,7 +86,7 @@ export default { if (enableMetrics) { metrics = new PricePusherMetrics( logger.child({ module: "Metrics" }), - metricsNamespace ?? "aptos", + metricsChain ?? "aptos", ); metrics.start(metricsPort); logger.info(`Metrics server started on port ${metricsPort}`); diff --git a/apps/price_pusher/src/evm/command.ts b/apps/price_pusher/src/evm/command.ts index 925d5257e7..f7fb521f9e 100644 --- a/apps/price_pusher/src/evm/command.ts +++ b/apps/price_pusher/src/evm/command.ts @@ -93,7 +93,7 @@ export default { ...options.controllerLogLevel, ...options.enableMetrics, ...options.metricsPort, - ...options.metricsNamespace, + ...options.metricsChain, }, command: "evm", describe: "run price pusher for evm", @@ -119,7 +119,7 @@ export default { controllerLogLevel, enableMetrics, metricsPort, - metricsNamespace, + metricsChain, } = argv; const logger = pino({ @@ -154,7 +154,7 @@ export default { if (enableMetrics) { metrics = new PricePusherMetrics( logger.child({ module: "Metrics" }), - metricsNamespace ?? "evm", + metricsChain ?? "evm", ); metrics.start(metricsPort); logger.info(`Metrics server started on port ${metricsPort}`); diff --git a/apps/price_pusher/src/metrics.ts b/apps/price_pusher/src/metrics.ts index 4f5e5d7d8e..edebb723ae 100644 --- a/apps/price_pusher/src/metrics.ts +++ b/apps/price_pusher/src/metrics.ts @@ -23,15 +23,19 @@ export class PricePusherMetrics { // Wallet metrics public walletBalance: Gauge; - constructor(logger: Logger, namespace: string) { + constructor(logger: Logger, chain: string) { this.logger = logger; this.registry = new Registry(); this.server = express(); - // Register the default metrics (memory, CPU, etc.). `namespace` is set as - // a default label so the sample Grafana dashboard's `namespace=$chain` - // filter actually matches emitted series. - this.registry.setDefaultLabels({ app: "price_pusher", namespace }); + // Register the default metrics (memory, CPU, etc.). `chain` is set as a + // default label so the sample Grafana dashboard's `chain=$chain` filter + // actually matches emitted series. Deliberately not named `namespace` — + // Kubernetes Prometheus service discovery auto-applies a `namespace` + // label representing the pod's k8s namespace, and with the default + // `honor_labels: false` our label would be renamed to + // `exported_namespace` and the dashboard filter would silently break. + this.registry.setDefaultLabels({ app: "price_pusher", chain }); // Create metrics this.lastPublishedTime = new Gauge({ diff --git a/apps/price_pusher/src/options.ts b/apps/price_pusher/src/options.ts index 88d381270f..8f6239b8c2 100644 --- a/apps/price_pusher/src/options.ts +++ b/apps/price_pusher/src/options.ts @@ -104,13 +104,15 @@ export const metricsPort = { } as Options, }; -export const metricsNamespace = { - "metrics-namespace": { +export const metricsChain = { + "metrics-chain": { description: - "Value emitted as the `namespace` label on every Prometheus metric. " + - "The sample Grafana dashboard filters by `namespace=$chain`, so set this " + + "Value emitted as the `chain` label on every Prometheus metric. " + + "The sample Grafana dashboard filters by `chain=$chain`, so set this " + "to your deployment's chain/network name (e.g. `bsc-mainnet`, `sui-testnet`). " + - "Defaults to the chain command name (e.g. `evm`, `sui`, `aptos`, `solana`).", + "Defaults to the chain command name (e.g. `evm`, `sui`, `aptos`, `solana`). " + + "Named `chain` rather than `namespace` to avoid colliding with the " + + "Kubernetes Prometheus service-discovery `namespace` label.", type: "string", required: false, } as Options, diff --git a/apps/price_pusher/src/solana/command.ts b/apps/price_pusher/src/solana/command.ts index b8a98ede89..bc3ce4e0b7 100644 --- a/apps/price_pusher/src/solana/command.ts +++ b/apps/price_pusher/src/solana/command.ts @@ -124,7 +124,7 @@ export default { ...options.controllerLogLevel, ...options.enableMetrics, ...options.metricsPort, - ...options.metricsNamespace, + ...options.metricsChain, }, command: "solana", describe: "run price pusher for solana", @@ -154,7 +154,7 @@ export default { metricsPort, pythReceiverProgramId, wormholeProgramId, - metricsNamespace, + metricsChain, } = argv; const logger = pino({ level: logLevel }); @@ -170,7 +170,7 @@ export default { if (enableMetrics) { metrics = new PricePusherMetrics( logger.child({ module: "Metrics" }), - metricsNamespace ?? "solana", + metricsChain ?? "solana", ); metrics.start(metricsPort); logger.info(`Metrics server started on port ${metricsPort}`); diff --git a/apps/price_pusher/src/sui/command.ts b/apps/price_pusher/src/sui/command.ts index 3db58d17da..3482a9714b 100644 --- a/apps/price_pusher/src/sui/command.ts +++ b/apps/price_pusher/src/sui/command.ts @@ -79,7 +79,7 @@ export default { ...options.controllerLogLevel, ...options.enableMetrics, ...options.metricsPort, - ...options.metricsNamespace, + ...options.metricsChain, }, command: "sui", describe: @@ -106,7 +106,7 @@ export default { controllerLogLevel, enableMetrics, metricsPort, - metricsNamespace, + metricsChain, } = argv; const logger = pino({ level: logLevel }); @@ -145,7 +145,7 @@ export default { if (enableMetrics) { metrics = new PricePusherMetrics( logger.child({ module: "Metrics" }), - metricsNamespace ?? "sui", + metricsChain ?? "sui", ); metrics.start(metricsPort); logger.info(`Metrics server started on port ${metricsPort}`);