Răsfoiți Sursa

add prom metrics

Daniel Chew 8 luni în urmă
părinte
comite
528aae2528

+ 88 - 0
apps/price_pusher/README.md

@@ -259,3 +259,91 @@ pushed twice and you won't pay additional costs most of the time.** However, the
 conditions in the RPCs because they are often behind a load balancer which can sometimes cause rejected
 transactions to land on-chain. You can reduce the chances of additional cost overhead by reducing the
 pushing frequency.
+
+## Prometheus Metrics
+
+The price_pusher now supports Prometheus metrics to monitor the health and performance of the price update service. Metrics are exposed via an HTTP endpoint that can be scraped by Prometheus.
+
+### Available Metrics
+
+The following metrics are available:
+
+- **pyth_price_last_published_time**: The last published time of a price feed in unix timestamp
+- **pyth_price_updates_total**: Total number of price updates pushed to the chain
+- **pyth_price_update_duration_seconds**: Duration of price update operations in seconds
+- **pyth_active_price_feeds**: Number of active price feeds being monitored
+- **pyth_price_update_errors_total**: Total number of errors encountered during price updates
+- **pyth_price_update_attempts_total**: Total number of price update attempts
+
+### Configuration
+
+Metrics are enabled by default and can be configured using the following command-line options:
+
+- `--enable-metrics`: Enable or disable the Prometheus metrics server (default: true)
+- `--metrics-port`: Port for the Prometheus metrics server (default: 9090)
+
+Example:
+
+```bash
+node lib/index.js evm --config config.evm.mainnet.json --metrics-port 9091
+```
+
+### Running Locally with Docker
+
+You can run a local Prometheus instance to test the metrics:
+
+1. Create a `prometheus.yml` file:
+
+```yaml
+global:
+  scrape_interval: 15s
+
+scrape_configs:
+  - job_name: "price_pusher"
+    static_configs:
+      - targets: ["localhost:9090"]
+```
+
+2. Run Prometheus with Docker:
+
+```bash
+docker run -d --name prometheus -p 9090:9090 \
+  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
+  prom/prometheus
+```
+
+3. Run Grafana with Docker:
+
+```bash
+docker run -d --name grafana -p 3000:3000 grafana/grafana
+```
+
+4. Access Grafana at http://localhost:3000 (default credentials: admin/admin) and add Prometheus as a data source (URL: http://host.docker.internal:9090).
+
+### Example Grafana Queries
+
+Here are some example Grafana queries to monitor your price feeds:
+
+1. Last published time for each price feed:
+
+```
+pyth_price_last_published_time
+```
+
+2. Number of price updates in the last hour:
+
+```
+sum(increase(pyth_price_updates_total[1h]))
+```
+
+3. Price feeds not updated in the last hour:
+
+```
+time() - pyth_price_last_published_time > 3600
+```
+
+4. Average update duration:
+
+```
+rate(pyth_price_update_duration_seconds_sum[5m]) / rate(pyth_price_update_duration_seconds_count[5m])
+```

+ 47 - 0
apps/price_pusher/alerts.yml

@@ -0,0 +1,47 @@
+groups:
+  - name: price_pusher_alerts
+    rules:
+      - alert: PriceFeedNotUpdated
+        expr: time() - pyth_price_last_published_time > 3600
+        for: 5m
+        labels:
+          severity: warning
+        annotations:
+          summary: "Price feed not updated"
+          description: "Price feed {{ $labels.alias }} has not been updated for more than 1 hour"
+
+      - alert: HighErrorRate
+        expr: sum(increase(pyth_price_update_errors_total[15m])) > 5
+        for: 5m
+        labels:
+          severity: warning
+        annotations:
+          summary: "High error rate in price updates"
+          description: "There have been more than 5 errors in the last 15 minutes"
+
+      - alert: NoRecentPriceUpdates
+        expr: sum(increase(pyth_price_updates_total[30m])) == 0
+        for: 5m
+        labels:
+          severity: critical
+        annotations:
+          summary: "No recent price updates"
+          description: "No price updates have been pushed in the last 30 minutes"
+
+      - alert: PricePusherDown
+        expr: up{job=~"price_pusher.*"} == 0
+        for: 1m
+        labels:
+          severity: critical
+        annotations:
+          summary: "Price pusher service is down"
+          description: "The price pusher service {{ $labels.instance }} is down"
+
+      - alert: HighUpdateDuration
+        expr: rate(pyth_price_update_duration_seconds_sum[5m]) / rate(pyth_price_update_duration_seconds_count[5m]) > 5
+        for: 5m
+        labels:
+          severity: warning
+        annotations:
+          summary: "High update duration"
+          description: "Price updates are taking longer than 5 seconds on average"

+ 77 - 0
apps/price_pusher/docker-compose.metrics.sample.yaml

@@ -0,0 +1,77 @@
+version: "3"
+
+services:
+  prometheus:
+    image: prom/prometheus:latest
+    container_name: prometheus
+    ports:
+      - "9090:9090"
+    volumes:
+      - ./prometheus.yml:/etc/prometheus/prometheus.yml
+      - ./alerts.yml:/etc/prometheus/alerts.yml
+    command:
+      - "--config.file=/etc/prometheus/prometheus.yml"
+      - "--storage.tsdb.path=/prometheus"
+      - "--web.console.libraries=/usr/share/prometheus/console_libraries"
+      - "--web.console.templates=/usr/share/prometheus/consoles"
+    networks:
+      - monitoring
+
+  grafana:
+    image: grafana/grafana:latest
+    container_name: grafana
+    ports:
+      - "3000:3000"
+    volumes:
+      - grafana-storage:/var/lib/grafana
+    environment:
+      - GF_SECURITY_ADMIN_USER=admin
+      - GF_SECURITY_ADMIN_PASSWORD=admin
+      - GF_USERS_ALLOW_SIGN_UP=false
+    depends_on:
+      - prometheus
+    networks:
+      - monitoring
+
+  # Example price_pusher service for Ethereum
+  # price_pusher_ethereum:
+  #   image: your-price-pusher-image:latest
+  #   container_name: price_pusher_ethereum
+  #   volumes:
+  #     - ./config.evm.mainnet.json:/app/config.json
+  #     - ./mnemonic:/app/mnemonic
+  #   command: >
+  #     node lib/index.js evm
+  #     --config /app/config.json
+  #     --mnemonic-file /app/mnemonic
+  #     --metrics-port 9091
+  #   ports:
+  #     - "9091:9091"
+  #   networks:
+  #     - monitoring
+  #   restart: unless-stopped
+
+  # Example price_pusher service for Solana
+  # price_pusher_solana:
+  #   image: your-price-pusher-image:latest
+  #   container_name: price_pusher_solana
+  #   volumes:
+  #     - ./config.solana.mainnet.json:/app/config.json
+  #     - ./mnemonic:/app/mnemonic
+  #   command: >
+  #     node lib/index.js solana
+  #     --config /app/config.json
+  #     --mnemonic-file /app/mnemonic
+  #     --metrics-port 9092
+  #   ports:
+  #     - "9092:9092"
+  #   networks:
+  #     - monitoring
+  #   restart: unless-stopped
+
+networks:
+  monitoring:
+    driver: bridge
+
+volumes:
+  grafana-storage:

+ 553 - 0
apps/price_pusher/grafana-dashboard.json

@@ -0,0 +1,553 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": {
+          "type": "grafana",
+          "uid": "-- Grafana --"
+        },
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "fiscalYearStartMonth": 0,
+  "graphTooltip": 0,
+  "id": 1,
+  "links": [],
+  "liveNow": false,
+  "panels": [
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "prometheus"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 3600
+              }
+            ]
+          },
+          "unit": "s"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 1,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "title": "Time Since Last Update",
+      "type": "timeseries",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "editorMode": "code",
+          "expr": "time() - pyth_price_last_published_time",
+          "legendFormat": "{{alias}}",
+          "range": true,
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "prometheus"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 3600
+              }
+            ]
+          },
+          "unit": "s"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "id": 2,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "area",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": ["lastNotNull"],
+          "fields": "",
+          "values": false
+        },
+        "textMode": "auto"
+      },
+      "pluginVersion": "10.0.0",
+      "title": "Time Since Last Update (Stat)",
+      "type": "stat",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "editorMode": "code",
+          "expr": "time() - pyth_price_last_published_time",
+          "legendFormat": "{{alias}}",
+          "range": true,
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "prometheus"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 8
+      },
+      "id": 3,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "title": "Price Updates (Last Hour)",
+      "type": "timeseries",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "editorMode": "code",
+          "expr": "sum(increase(pyth_price_updates_total[1h]))",
+          "legendFormat": "Updates",
+          "range": true,
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "prometheus"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          },
+          "unit": "s"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "id": 4,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "title": "Average Update Duration",
+      "type": "timeseries",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "editorMode": "code",
+          "expr": "rate(pyth_price_update_duration_seconds_sum[5m]) / rate(pyth_price_update_duration_seconds_count[5m])",
+          "legendFormat": "{{alias}}",
+          "range": true,
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "prometheus"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 16
+      },
+      "id": 5,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "title": "Active Price Feeds",
+      "type": "timeseries",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "editorMode": "code",
+          "expr": "pyth_active_price_feeds",
+          "legendFormat": "Active Feeds",
+          "range": true,
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "datasource": {
+        "type": "prometheus",
+        "uid": "prometheus"
+      },
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisCenteredZero": false,
+            "axisColorMode": "text",
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 0,
+            "gradientMode": "none",
+            "hideFrom": {
+              "legend": false,
+              "tooltip": false,
+              "viz": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "auto",
+            "spanNulls": false,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 16
+      },
+      "id": 6,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom",
+          "showLegend": true
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "title": "Update Errors",
+      "type": "timeseries",
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "editorMode": "code",
+          "expr": "sum(increase(pyth_price_update_errors_total[5m]))",
+          "legendFormat": "Errors",
+          "range": true,
+          "refId": "A"
+        }
+      ]
+    }
+  ],
+  "refresh": "5s",
+  "schemaVersion": 38,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-1h",
+    "to": "now"
+  },
+  "timepicker": {},
+  "timezone": "",
+  "title": "Pyth Price Pusher Dashboard",
+  "uid": "pyth-price-pusher",
+  "version": 1,
+  "weekStart": ""
+}

+ 3 - 0
apps/price_pusher/package.json

@@ -45,6 +45,7 @@
   "license": "Apache-2.0",
   "devDependencies": {
     "@types/ethereum-protocol": "^1.0.2",
+    "@types/express": "^4.17.21",
     "@types/jest": "^27.4.1",
     "@types/yargs": "^17.0.10",
     "@typescript-eslint/eslint-plugin": "^6.0.0",
@@ -76,11 +77,13 @@
     "@ton/ton": "^15.1.0",
     "@types/pino": "^7.0.5",
     "aptos": "^1.8.5",
+    "express": "^4.18.2",
     "fuels": "^0.94.5",
     "jito-ts": "^3.0.1",
     "joi": "^17.6.0",
     "near-api-js": "^3.0.2",
     "pino": "^9.2.0",
+    "prom-client": "^15.1.0",
     "viem": "^2.19.4",
     "yaml": "^2.1.1",
     "yargs": "^17.5.1"

+ 66 - 0
apps/price_pusher/prometheus.yml

@@ -0,0 +1,66 @@
+global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+scrape_configs:
+  - job_name: "price_pusher"
+    static_configs:
+      - targets: ["host.docker.internal:9091"]
+    relabel_configs:
+      - source_labels: [__address__]
+        target_label: instance
+        replacement: "price_pusher"
+
+  # Add more price_pusher instances for different chains
+  # - job_name: 'price_pusher_ethereum'
+  #   static_configs:
+  #     - targets: ['localhost:9091']
+  #   relabel_configs:
+  #     - source_labels: [__address__]
+  #       target_label: instance
+  #       replacement: 'ethereum'
+  #     - source_labels: [__address__]
+  #       target_label: chain
+  #       replacement: 'ethereum'
+
+  # - job_name: 'price_pusher_solana'
+  #   static_configs:
+  #     - targets: ['localhost:9092']
+  #   relabel_configs:
+  #     - source_labels: [__address__]
+  #       target_label: instance
+  #       replacement: 'solana'
+  #     - source_labels: [__address__]
+  #       target_label: chain
+  #       replacement: 'solana'
+
+alerting:
+  alertmanagers:
+    - static_configs:
+        - targets:
+          # - alertmanager:9093
+
+# Alert rules
+rule_files:
+  - "alerts.yml"
+# Sample alerts.yml content:
+# groups:
+# - name: price_pusher_alerts
+#   rules:
+#   - alert: PriceFeedNotUpdated
+#     expr: time() - pyth_price_last_published_time > 3600
+#     for: 5m
+#     labels:
+#       severity: warning
+#     annotations:
+#       summary: "Price feed not updated"
+#       description: "Price feed {{ $labels.alias }} has not been updated for more than 1 hour"
+#
+#   - alert: HighErrorRate
+#     expr: sum(increase(pyth_price_update_errors_total[15m])) > 5
+#     for: 5m
+#     labels:
+#       severity: warning
+#     annotations:
+#       summary: "High error rate in price updates"
+#       description: "There have been more than 5 errors in the last 15 minutes"

+ 69 - 2
apps/price_pusher/src/controller.ts

@@ -3,9 +3,12 @@ import { DurationInSeconds, sleep } from "./utils";
 import { IPriceListener, IPricePusher } from "./interface";
 import { PriceConfig, shouldUpdate, UpdateCondition } from "./price-config";
 import { Logger } from "pino";
+import { PricePusherMetrics } from "./metrics";
 
 export class Controller {
   private pushingFrequency: DurationInSeconds;
+  private metrics?: PricePusherMetrics;
+
   constructor(
     private priceConfigs: PriceConfig[],
     private sourcePriceListener: IPriceListener,
@@ -14,9 +17,16 @@ export class Controller {
     private logger: Logger,
     config: {
       pushingFrequency: DurationInSeconds;
-    },
+      metrics?: PricePusherMetrics;
+    }
   ) {
     this.pushingFrequency = config.pushingFrequency;
+    this.metrics = config.metrics;
+
+    // Set the number of active price feeds if metrics are enabled
+    if (this.metrics) {
+      this.metrics.setActivePriceFeeds(this.priceConfigs.length);
+    }
   }
 
   async start() {
@@ -38,18 +48,34 @@ export class Controller {
 
       for (const priceConfig of this.priceConfigs) {
         const priceId = priceConfig.id;
+        const alias = priceConfig.alias;
 
         const targetLatestPrice =
           this.targetPriceListener.getLatestPriceInfo(priceId);
         const sourceLatestPrice =
           this.sourcePriceListener.getLatestPriceInfo(priceId);
 
+        // Update metrics for the last published time if available
+        if (this.metrics && targetLatestPrice) {
+          this.metrics.updateLastPublishedTime(
+            priceId,
+            alias,
+            targetLatestPrice
+          );
+        }
+
         const priceShouldUpdate = shouldUpdate(
           priceConfig,
           sourceLatestPrice,
           targetLatestPrice,
           this.logger,
         );
+
+        // Record price update attempt in metrics
+        if (this.metrics) {
+          this.metrics.recordPriceUpdateAttempt(priceId, alias);
+        }
+
         if (priceShouldUpdate == UpdateCondition.YES) {
           pushThresholdMet = true;
         }
@@ -75,7 +101,48 @@ export class Controller {
 
         // note that the priceIds are without leading "0x"
         const priceIds = pricesToPush.map((priceConfig) => priceConfig.id);
-        this.targetChainPricePusher.updatePriceFeed(priceIds, pubTimesToPush);
+
+        try {
+          // Start timers for each price update if metrics are enabled
+          const timers = this.metrics
+            ? pricesToPush.map((config) => ({
+                config,
+                timer: this.metrics!.startPriceUpdateTimer(
+                  config.id,
+                  config.alias
+                ),
+              }))
+            : [];
+
+          await this.targetChainPricePusher.updatePriceFeed(
+            priceIds,
+            pubTimesToPush
+          );
+
+          // Record successful updates and end timers
+          if (this.metrics) {
+            for (const { config, timer } of timers) {
+              this.metrics.recordPriceUpdate(config.id, config.alias);
+              timer(); // End the timer
+            }
+          }
+        } catch (error) {
+          this.logger.error(
+            { error, priceIds },
+            "Error pushing price updates to chain"
+          );
+
+          // Record errors in metrics
+          if (this.metrics) {
+            for (const config of pricesToPush) {
+              this.metrics.recordPriceUpdateError(
+                config.id,
+                config.alias,
+                error instanceof Error ? error.name : "unknown"
+              );
+            }
+          }
+        }
       } else {
         this.logger.info("None of the checks were triggered. No push needed.");
       }

+ 17 - 1
apps/price_pusher/src/evm/command.ts

@@ -11,6 +11,7 @@ import pino from "pino";
 import { createClient } from "./super-wallet";
 import { createPythContract } from "./pyth-contract";
 import { isWsEndpoint, filterInvalidPriceItems } from "../utils";
+import { PricePusherMetrics } from "../metrics";
 
 export default {
   command: "evm",
@@ -83,6 +84,8 @@ export default {
     ...options.pushingFrequency,
     ...options.logLevel,
     ...options.controllerLogLevel,
+    ...options.enableMetrics,
+    ...options.metricsPort,
   },
   handler: async function (argv: any) {
     // FIXME: type checks for this
@@ -103,6 +106,8 @@ export default {
       updateFeeMultiplier,
       logLevel,
       controllerLogLevel,
+      enableMetrics,
+      metricsPort,
     } = argv;
     console.log("***** priceServiceEndpoint *****", priceServiceEndpoint);
 
@@ -131,6 +136,14 @@ export default {
 
     priceItems = existingPriceItems;
 
+    // Initialize metrics if enabled
+    let metrics: PricePusherMetrics | undefined;
+    if (enableMetrics) {
+      metrics = new PricePusherMetrics(logger.child({ module: "Metrics" }));
+      metrics.start(metricsPort);
+      logger.info(`Metrics server started on port ${metricsPort}`);
+    }
+
     const pythListener = new PythPriceListener(
       hermesClient,
       priceItems,
@@ -183,7 +196,10 @@ export default {
       evmListener,
       evmPusher,
       logger.child({ module: "Controller" }, { level: controllerLogLevel }),
-      { pushingFrequency },
+      {
+        pushingFrequency,
+        metrics,
+      }
     );
 
     controller.start();

+ 3 - 0
apps/price_pusher/src/index.ts

@@ -9,6 +9,7 @@ import near from "./near/command";
 import solana from "./solana/command";
 import fuel from "./fuel/command";
 import ton from "./ton/command";
+import { enableMetrics, metricsPort } from "./options";
 
 yargs(hideBin(process.argv))
   .parserConfiguration({
@@ -16,6 +17,8 @@ yargs(hideBin(process.argv))
   })
   .config("config")
   .global("config")
+  .option("enable-metrics", enableMetrics["enable-metrics"])
+  .option("metrics-port", metricsPort["metrics-port"])
   .command(evm)
   .command(fuel)
   .command(injective)

+ 133 - 0
apps/price_pusher/src/metrics.ts

@@ -0,0 +1,133 @@
+import { Registry, Counter, Gauge, Histogram } from "prom-client";
+import express from "express";
+import { PriceInfo } from "./interface";
+import { Logger } from "pino";
+
+// Define the metrics we want to track
+export class PricePusherMetrics {
+  private registry: Registry;
+  private server: express.Express;
+  private logger: Logger;
+
+  // Metrics for price feed updates
+  public lastPublishedTime: Gauge<string>;
+  public priceUpdatesTotal: Counter<string>;
+  public priceUpdateDuration: Histogram<string>;
+  public activePriceFeeds: Gauge<string>;
+  public priceUpdateErrors: Counter<string>;
+  public priceUpdateAttempts: Counter<string>;
+
+  constructor(logger: Logger) {
+    this.logger = logger;
+    this.registry = new Registry();
+    this.server = express();
+
+    // Register the default metrics (memory, CPU, etc.)
+    this.registry.setDefaultLabels({ app: "price_pusher" });
+
+    // Create metrics
+    this.lastPublishedTime = new Gauge({
+      name: "pyth_price_last_published_time",
+      help: "The last published time of a price feed in unix timestamp",
+      labelNames: ["price_id", "alias"],
+      registers: [this.registry],
+    });
+
+    this.priceUpdatesTotal = new Counter({
+      name: "pyth_price_updates_total",
+      help: "Total number of price updates pushed to the chain",
+      labelNames: ["price_id", "alias"],
+      registers: [this.registry],
+    });
+
+    this.priceUpdateDuration = new Histogram({
+      name: "pyth_price_update_duration_seconds",
+      help: "Duration of price update operations in seconds",
+      labelNames: ["price_id", "alias"],
+      buckets: [0.1, 0.5, 1, 2, 5, 10],
+      registers: [this.registry],
+    });
+
+    this.activePriceFeeds = new Gauge({
+      name: "pyth_active_price_feeds",
+      help: "Number of active price feeds being monitored",
+      registers: [this.registry],
+    });
+
+    this.priceUpdateErrors = new Counter({
+      name: "pyth_price_update_errors_total",
+      help: "Total number of errors encountered during price updates",
+      labelNames: ["price_id", "alias", "error_type"],
+      registers: [this.registry],
+    });
+
+    this.priceUpdateAttempts = new Counter({
+      name: "pyth_price_update_attempts_total",
+      help: "Total number of price update attempts",
+      labelNames: ["price_id", "alias"],
+      registers: [this.registry],
+    });
+
+    // Setup the metrics endpoint
+    this.server.get("/metrics", async (req, res) => {
+      res.set("Content-Type", this.registry.contentType);
+      res.end(await this.registry.metrics());
+    });
+  }
+
+  // Start the metrics server
+  public start(port: number): void {
+    this.server.listen(port, () => {
+      this.logger.info(`Metrics server started on port ${port}`);
+    });
+  }
+
+  // Update the last published time for a price feed
+  public updateLastPublishedTime(
+    priceId: string,
+    alias: string,
+    priceInfo: PriceInfo
+  ): void {
+    this.lastPublishedTime.set(
+      { price_id: priceId, alias },
+      priceInfo.publishTime
+    );
+  }
+
+  // Record a successful price update
+  public recordPriceUpdate(priceId: string, alias: string): void {
+    this.priceUpdatesTotal.inc({ price_id: priceId, alias });
+  }
+
+  // Record a price update attempt
+  public recordPriceUpdateAttempt(priceId: string, alias: string): void {
+    this.priceUpdateAttempts.inc({ price_id: priceId, alias });
+  }
+
+  // Record a price update error
+  public recordPriceUpdateError(
+    priceId: string,
+    alias: string,
+    errorType: string
+  ): void {
+    this.priceUpdateErrors.inc({
+      price_id: priceId,
+      alias,
+      error_type: errorType,
+    });
+  }
+
+  // Set the number of active price feeds
+  public setActivePriceFeeds(count: number): void {
+    this.activePriceFeeds.set(count);
+  }
+
+  // Create a timer for measuring price update duration
+  public startPriceUpdateTimer(priceId: string, alias: string): () => void {
+    const end = this.priceUpdateDuration.startTimer({
+      price_id: priceId,
+      alias,
+    });
+    return end;
+  }
+}

+ 18 - 0
apps/price_pusher/src/options.ts

@@ -76,3 +76,21 @@ export const controllerLogLevel = {
     choices: ["trace", "debug", "info", "warn", "error"],
   } as Options,
 };
+
+export const enableMetrics = {
+  "enable-metrics": {
+    description: "Enable Prometheus metrics server",
+    type: "boolean",
+    required: false,
+    default: true,
+  } as Options,
+};
+
+export const metricsPort = {
+  "metrics-port": {
+    description: "Port for the Prometheus metrics server",
+    type: "number",
+    required: false,
+    default: 9090,
+  } as Options,
+};

+ 152 - 42
pnpm-lock.yaml

@@ -802,7 +802,10 @@ importers:
         version: 3.0.4(encoding@0.1.13)
       pino:
         specifier: ^9.2.0
-        version: 9.6.0
+        version: 9.5.0
+      prom-client:
+        specifier: ^15.1.0
+        version: 15.1.3
       viem:
         specifier: ^2.19.4
         version: 2.23.11(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2)
@@ -6278,6 +6281,10 @@ packages:
   '@octokit/types@13.8.0':
     resolution: {integrity: sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==}
 
+  '@opentelemetry/api@1.9.0':
+    resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
+    engines: {node: '>=8.0.0'}
+
   '@openzeppelin/contract-loader@0.6.3':
     resolution: {integrity: sha512-cOFIjBjwbGgZhDZsitNgJl0Ye1rd5yu/Yx5LMgeq3u0ZYzldm4uObzHDFq4gjDdoypvyORjjJa3BlFA7eAnVIg==}
 
@@ -11458,6 +11465,9 @@ packages:
   bindings@1.5.0:
     resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
 
+  bintrees@1.0.2:
+    resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
+
   bip32@2.0.6:
     resolution: {integrity: sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA==}
     engines: {node: '>=6.0.0'}
@@ -18176,6 +18186,10 @@ packages:
     resolution: {integrity: sha512-69agxLtnI8xBs9gUGqEnK26UfiexpHy+KUpBQWabiytQjnn5wFY8rklAi7GRfABIuPNnQ/ik48+LGLkYYJcy4A==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
 
+  prom-client@15.1.3:
+    resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
+    engines: {node: ^16 || ^18 || >=20}
+
   promise-all-reject-late@1.0.1:
     resolution: {integrity: sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==}
 
@@ -19941,9 +19955,11 @@ packages:
     resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
     engines: {node: '>=10'}
 
-  tar@7.4.3:
-    resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
-    engines: {node: '>=18'}
+  tdigest@0.1.2:
+    resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
+
+  telejson@7.2.0:
+    resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==}
 
   temp-dir@1.0.0:
     resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==}
@@ -23996,7 +24012,7 @@ snapshots:
       '@cosmjs/socket': 0.30.1(bufferutil@4.0.9)(utf-8-validate@6.0.3)
       '@cosmjs/stream': 0.30.1
       '@cosmjs/utils': 0.30.1
-      axios: 0.21.4(debug@4.4.0)
+      axios: 0.21.4(debug@4.3.7)
       readonly-date: 1.0.0
       xstream: 11.14.0
     transitivePeerDependencies:
@@ -24175,7 +24191,7 @@ snapshots:
       - supports-color
       - utf-8-validate
 
-  '@cprussin/jest-runner-prettier@1.0.0(jest@29.7.0(@types/node@22.13.10)(ts-node@10.9.2(@types/node@22.13.10)(typescript@5.8.2)))(prettier@3.5.3)':
+  '@cprussin/jest-config@1.4.1(@babel/core@7.25.8)(@jest/transform@29.7.0)(@jest/types@29.6.3)(@opentelemetry/api@1.9.0)(@types/node@22.8.2)(babel-jest@29.7.0(@babel/core@7.25.8))(bufferutil@4.0.8)(eslint@9.13.0(jiti@1.21.0))(sass@1.80.7)(ts-node@10.9.2(@types/node@22.8.2)(typescript@5.6.3))(utf-8-validate@5.0.10)':
     dependencies:
       create-lite-jest-runner: 1.1.2(jest@29.7.0(@types/node@22.13.10)(ts-node@10.9.2(@types/node@22.13.10)(typescript@5.8.2)))
       emphasize: 5.0.0
@@ -26323,7 +26339,7 @@ snapshots:
   '@jest/console@29.7.0':
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       chalk: 4.1.2
       jest-message-util: 29.7.0
       jest-util: 29.7.0
@@ -26547,7 +26563,7 @@ snapshots:
     dependencies:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       jest-mock: 29.7.0
 
   '@jest/expect-utils@29.7.0':
@@ -26565,7 +26581,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@sinonjs/fake-timers': 10.3.0
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       jest-message-util: 29.7.0
       jest-mock: 29.7.0
       jest-util: 29.7.0
@@ -26587,7 +26603,7 @@ snapshots:
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
       '@jridgewell/trace-mapping': 0.3.25
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       chalk: 4.1.2
       collect-v8-coverage: 1.0.2
       exit: 0.1.2
@@ -27637,9 +27653,62 @@ snapshots:
     dependencies:
       glob: 10.3.10
 
-  '@next/eslint-plugin-next@15.2.2':
+  '@next/swc-darwin-arm64@14.2.15':
+    optional: true
+
+  '@next/swc-darwin-arm64@15.1.2':
+    optional: true
+
+  '@next/swc-darwin-x64@14.2.15':
+    optional: true
+
+  '@next/swc-darwin-x64@15.1.2':
+    optional: true
+
+  '@next/swc-linux-arm64-gnu@14.2.15':
+    optional: true
+
+  '@next/swc-linux-arm64-gnu@15.1.2':
+    optional: true
+
+  '@next/swc-linux-arm64-musl@14.2.15':
+    optional: true
+
+  '@next/swc-linux-arm64-musl@15.1.2':
+    optional: true
+
+  '@next/swc-linux-x64-gnu@14.2.15':
+    optional: true
+
+  '@next/swc-linux-x64-gnu@15.1.2':
+    optional: true
+
+  '@next/swc-linux-x64-musl@14.2.15':
+    optional: true
+
+  '@next/swc-linux-x64-musl@15.1.2':
+    optional: true
+
+  '@next/swc-win32-arm64-msvc@14.2.15':
+    optional: true
+
+  '@next/swc-win32-arm64-msvc@15.1.2':
+    optional: true
+
+  '@next/swc-win32-ia32-msvc@14.2.15':
+    optional: true
+
+  '@next/swc-win32-x64-msvc@14.2.15':
+    optional: true
+
+  '@next/swc-win32-x64-msvc@15.1.2':
+    optional: true
+
+  '@next/third-parties@14.2.6(next@15.1.2(@babel/core@7.25.8)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0)':
     dependencies:
-      fast-glob: 3.3.1
+      next: 15.1.2(@babel/core@7.25.8)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7)
+      react: 19.0.0
+      third-party-capital: 1.0.20
 
   '@next/swc-darwin-arm64@15.2.2':
     optional: true
@@ -28120,6 +28189,8 @@ snapshots:
     dependencies:
       '@octokit/openapi-types': 23.0.1
 
+  '@opentelemetry/api@1.9.0': {}
+
   '@openzeppelin/contract-loader@0.6.3':
     dependencies:
       find-up: 4.1.0
@@ -33953,7 +34024,7 @@ snapshots:
 
   '@types/bn.js@4.11.6':
     dependencies:
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
 
   '@types/bn.js@5.1.6':
     dependencies:
@@ -33981,7 +34052,7 @@ snapshots:
 
   '@types/connect@3.4.38':
     dependencies:
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
 
   '@types/cors@2.8.12':
     optional: true
@@ -34075,7 +34146,7 @@ snapshots:
 
   '@types/graceful-fs@4.1.9':
     dependencies:
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
 
   '@types/hast@2.3.10':
     dependencies:
@@ -34123,7 +34194,7 @@ snapshots:
 
   '@types/keyv@3.1.4':
     dependencies:
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
 
   '@types/lodash.values@4.3.9':
     dependencies:
@@ -34188,7 +34259,7 @@ snapshots:
 
   '@types/pbkdf2@3.1.2':
     dependencies:
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
 
   '@types/pino@7.0.5':
     dependencies:
@@ -34210,11 +34281,11 @@ snapshots:
 
   '@types/responselike@1.0.3':
     dependencies:
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
 
   '@types/secp256k1@4.0.6':
     dependencies:
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
 
   '@types/seedrandom@3.0.1': {}
 
@@ -34271,7 +34342,7 @@ snapshots:
 
   '@types/ws@7.4.7':
     dependencies:
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
 
   '@types/ws@8.18.0':
     dependencies:
@@ -36739,6 +36810,8 @@ snapshots:
     dependencies:
       file-uri-to-path: 1.0.0
 
+  bintrees@1.0.2: {}
+
   bip32@2.0.6:
     dependencies:
       '@types/node': 10.12.18
@@ -42257,7 +42330,7 @@ snapshots:
       '@jest/expect': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       chalk: 4.1.2
       co: 4.6.0
       dedent: 1.5.3
@@ -42762,7 +42835,7 @@ snapshots:
       '@jest/environment': 29.7.0
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       jest-mock: 29.7.0
       jest-util: 29.7.0
 
@@ -42774,7 +42847,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@types/graceful-fs': 4.1.9
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       anymatch: 3.1.3
       fb-watchman: 2.0.2
       graceful-fs: 4.2.11
@@ -42820,7 +42893,7 @@ snapshots:
   jest-mock@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       jest-util: 29.7.0
 
   jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@@ -42867,7 +42940,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       chalk: 4.1.2
       emittery: 0.13.1
       graceful-fs: 4.2.11
@@ -42895,7 +42968,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       chalk: 4.1.2
       cjs-module-lexer: 1.4.3
       collect-v8-coverage: 1.0.2
@@ -42960,7 +43033,7 @@ snapshots:
     dependencies:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       emittery: 0.13.1
@@ -42981,7 +43054,7 @@ snapshots:
 
   jest-worker@29.7.0:
     dependencies:
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       jest-util: 29.7.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
@@ -44855,7 +44928,34 @@ snapshots:
 
   next@15.2.2(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1):
     dependencies:
-      '@next/env': 15.2.2
+      '@next/env': 14.2.15
+      '@swc/helpers': 0.5.5
+      busboy: 1.6.0
+      caniuse-lite: 1.0.30001669
+      graceful-fs: 4.2.11
+      postcss: 8.4.31
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+      styled-jsx: 5.1.1(@babel/core@7.25.8)(react@18.3.1)
+    optionalDependencies:
+      '@next/swc-darwin-arm64': 14.2.15
+      '@next/swc-darwin-x64': 14.2.15
+      '@next/swc-linux-arm64-gnu': 14.2.15
+      '@next/swc-linux-arm64-musl': 14.2.15
+      '@next/swc-linux-x64-gnu': 14.2.15
+      '@next/swc-linux-x64-musl': 14.2.15
+      '@next/swc-win32-arm64-msvc': 14.2.15
+      '@next/swc-win32-ia32-msvc': 14.2.15
+      '@next/swc-win32-x64-msvc': 14.2.15
+      '@opentelemetry/api': 1.9.0
+      sass: 1.80.7
+    transitivePeerDependencies:
+      - '@babel/core'
+      - babel-plugin-macros
+
+  next@15.1.2(@babel/core@7.25.8)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@19.0.0))(react@19.0.0)(sass@1.80.7):
+    dependencies:
+      '@next/env': 15.1.2
       '@swc/counter': 0.1.3
       '@swc/helpers': 0.5.15
       busboy: 1.6.0
@@ -44865,15 +44965,16 @@ snapshots:
       react-dom: 19.0.0(react@19.0.0)
       styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.0.0)
     optionalDependencies:
-      '@next/swc-darwin-arm64': 15.2.2
-      '@next/swc-darwin-x64': 15.2.2
-      '@next/swc-linux-arm64-gnu': 15.2.2
-      '@next/swc-linux-arm64-musl': 15.2.2
-      '@next/swc-linux-x64-gnu': 15.2.2
-      '@next/swc-linux-x64-musl': 15.2.2
-      '@next/swc-win32-arm64-msvc': 15.2.2
-      '@next/swc-win32-x64-msvc': 15.2.2
-      sass: 1.85.1
+      '@next/swc-darwin-arm64': 15.1.2
+      '@next/swc-darwin-x64': 15.1.2
+      '@next/swc-linux-arm64-gnu': 15.1.2
+      '@next/swc-linux-arm64-musl': 15.1.2
+      '@next/swc-linux-x64-gnu': 15.1.2
+      '@next/swc-linux-x64-musl': 15.1.2
+      '@next/swc-win32-arm64-msvc': 15.1.2
+      '@next/swc-win32-x64-msvc': 15.1.2
+      '@opentelemetry/api': 1.9.0
+      sass: 1.80.7
       sharp: 0.33.5
     transitivePeerDependencies:
       - '@babel/core'
@@ -46214,6 +46315,11 @@ snapshots:
 
   proggy@2.0.0: {}
 
+  prom-client@15.1.3:
+    dependencies:
+      '@opentelemetry/api': 1.9.0
+      tdigest: 0.1.2
+
   promise-all-reject-late@1.0.1: {}
 
   promise-call-limit@3.0.2: {}
@@ -46274,7 +46380,7 @@ snapshots:
       '@protobufjs/pool': 1.1.0
       '@protobufjs/utf8': 1.1.0
       '@types/long': 4.0.2
-      '@types/node': 18.19.80
+      '@types/node': 20.14.15
       long: 4.0.0
 
   protobufjs@7.4.0:
@@ -46289,8 +46395,8 @@ snapshots:
       '@protobufjs/path': 1.1.2
       '@protobufjs/pool': 1.1.0
       '@protobufjs/utf8': 1.1.0
-      '@types/node': 18.19.80
-      long: 5.3.1
+      '@types/node': 20.14.15
+      long: 5.2.3
 
   protocols@2.0.2: {}
 
@@ -48723,7 +48829,11 @@ snapshots:
       mkdirp: 1.0.4
       yallist: 4.0.0
 
-  tar@7.4.3:
+  tdigest@0.1.2:
+    dependencies:
+      bintrees: 1.0.2
+
+  telejson@7.2.0:
     dependencies:
       '@isaacs/fs-minipass': 4.0.1
       chownr: 3.0.0