Skip to content

Commit

Permalink
feat: public rpc node height (#4)
Browse files Browse the repository at this point in the history
* feat: refactor into cmd package

* feat: implement node height

* fix: docker build

* fix: apply feedback

- Returning errors instead of fatalF
- Using CMDConfig
- Removed usage of metric.type
- Removed mutex
- Added todos
- Using limited reader
- Parsing uint64

* chore: rename package to sl-exporter

* fix: use is.ReadFile
  • Loading branch information
nourspace authored Apr 20, 2023
1 parent 2562c2d commit c6d7285
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 106 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
!go.mod
!go.sum
!*.go
!cmd
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ RUN go mod download

# Copy the go source
COPY *.go .
COPY cmd/ cmd/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ go build -o exporter main.go

Enriches Relayer metrics (e.g. `cosmos_relayer_wallet_balance`) to dynamically create Grafana panels and alerts.


## More use cases

- Chain current block height
Expand Down Expand Up @@ -49,3 +48,11 @@ docker run --rm -p 9100:9100 -v $(pwd)/config.yaml:/config.yaml sl-exporter --bi
## Release

Simply push any tag to this repository

# Todos

- Add testing
- Enable linting on CI
- Use https://github.com/spf13/viper to simplify loading config and maybe flags as well
- Use slog for structured logging: https://pkg.go.dev/golang.org/x/exp/slog
- Load config once and don't recreate the registry
70 changes: 70 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cmd

import (
"flag"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"os"
"time"
)

type Config struct {
Metrics map[string]Metric `yaml:"metrics"`
}

type Metric struct {
Description string `yaml:"description"`
Labels []string `yaml:"labels"`
Samples []Sample `yaml:"samples,omitempty"`
Chains []ChainWithRPCs `yaml:"chains,omitempty"`
}

type ChainWithRPCs struct {
Name string `yaml:"name"`
RPCs []string `yaml:"rpcs"`
}

type Sample struct {
Labels []string `yaml:"labels"`
Value float64 `yaml:"value"`
}

type CMDConfig struct {
ConfigFile string
Bind string
Interval time.Duration
LogLevel string
}

var cmdConfig CMDConfig

func init() {
flag.StringVar(&cmdConfig.ConfigFile, "config", "config.yaml", "configuration file")
flag.StringVar(&cmdConfig.Bind, "bind", "localhost:9100", "bind")
flag.DurationVar(&cmdConfig.Interval, "interval", 15*time.Second, "duration interval")
flag.StringVar(&cmdConfig.LogLevel, "loglevel", "info", "Log level (debug, info, warn, error)")
flag.Parse()

level, err := log.ParseLevel(cmdConfig.LogLevel)
if err != nil {
log.Fatalf("Invalid log level: %v", err)
}
log.SetLevel(level)

log.Debugf("Config File: %s\n", cmdConfig.ConfigFile)
log.Debugf("Interval: %s\n", cmdConfig.Interval)
log.Debugf("Bind: %s\n", cmdConfig.Bind)
}

// readConfig reads config.yaml from disk
func readConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
72 changes: 72 additions & 0 deletions cmd/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cmd

import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"net/url"
)

// registerMetrics iterates config metrics and passes them to relevant handler
func registerMetrics(config *Config, registry *prometheus.Registry) error {
for metricName, metric := range config.Metrics {
var collector prometheus.Collector

if len(metric.Samples) > 0 {
collector = sampleMetrics(metric, metricName)
} else if len(metric.Chains) > 0 {
collector = rpcMetrics(metric, metricName)
} else {
return fmt.Errorf("unsupported metric: %s", metricName)
}

if err := registry.Register(collector); err != nil {
return fmt.Errorf("error registering %s: %v", metricName, err)
}
log.Infof("Register collector - %s", metricName)
}
return nil
}

// sampleMetrics handles static gauge samples
func sampleMetrics(metric Metric, metricName string) *prometheus.GaugeVec {
gaugeVec := prometheus.NewGaugeVec(
prometheus.GaugeOpts{Name: prometheus.BuildFQName(namespace, subsystem, metricName), Help: metric.Description},
metric.Labels,
)
for _, sample := range metric.Samples {
gaugeVec.WithLabelValues(sample.Labels...).Set(sample.Value)
}
return gaugeVec
}

// rpcMetrics handles dynamic gauge metrics for public_rpc_node_height
func rpcMetrics(metric Metric, metricName string) *prometheus.GaugeVec {
gaugeVec := prometheus.NewGaugeVec(
prometheus.GaugeOpts{Name: prometheus.BuildFQName(namespace, subsystem, metricName), Help: metric.Description},
metric.Labels,
)

for _, chain := range metric.Chains {
for _, rpc := range chain.RPCs {
// Fetch and set the metric value for the rpc node
value, err := fetchRPCNodeHeight(rpc)
if err != nil {
log.Errorf("Error fetching height for rpc %s: %v", rpc, err)
continue
}
gaugeVec.WithLabelValues(chain.Name, urlHost(rpc)).Set(float64(value))
}
}
return gaugeVec
}

// urlHost extracts host from given rpc url
func urlHost(rpc string) string {
parsedURL, err := url.Parse(rpc)
if err != nil {
log.Warnf("Error parsing URL: %v\n", err)
return rpc
}
return parsedURL.Hostname()
}
42 changes: 42 additions & 0 deletions cmd/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cmd

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/version"
"net/http"
)

const (
collector = "sl_exporter"
// Todo (nour): should we add namespace and subsystem
namespace = ""
subsystem = ""
)

var (
metricsRegistry *prometheus.Registry
)

func updateRegistry(config *Config) (*prometheus.Registry, error) {
// Create sampleMetrics new registry for the updated metrics
newRegistry := prometheus.NewRegistry()

// Register build_info metric
if err := newRegistry.Register(version.NewCollector(collector)); err != nil {
return nil, err
}

if err := registerMetrics(config, newRegistry); err != nil {
return nil, err
}

return newRegistry, nil
}

func metricsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler := promhttp.HandlerFor(metricsRegistry, promhttp.HandlerOpts{})
handler.ServeHTTP(w, r)
})
}
41 changes: 41 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cmd

import (
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"net/http"
"time"
)

func Execute() {
// Initialize metricsRegistry
metricsRegistry = prometheus.NewRegistry()

// Start background goroutine to re-evaluate the config and update the registry at `interval`
go func() {
ticker := time.NewTicker(cmdConfig.Interval)
defer ticker.Stop()

for {
select {
case <-ticker.C:
config, err := readConfig(cmdConfig.ConfigFile)
if err != nil {
log.Errorf("Error reading config file %s: %v", cmdConfig.ConfigFile, err)
continue
}
if updatedRegistry, err := updateRegistry(config); err != nil {
log.Errorf("error updating registery: %v", err)
} else {
metricsRegistry = updatedRegistry
}
}
}
}()

http.Handle("/metrics", metricsHandler())
log.Infof("Starting Prometheus metrics server - %s", cmdConfig.Bind)
if err := http.ListenAndServe(cmdConfig.Bind, nil); err != nil {
log.Fatalf("Failed to start http server: %v", err)
}
}
57 changes: 57 additions & 0 deletions cmd/rpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cmd

import (
"encoding/json"
"fmt"
log "github.com/sirupsen/logrus"
"io"
"net/http"
"strconv"
)

type SyncInfo struct {
LatestBlockHeight string `json:"latest_block_height"`
}

type Result struct {
SyncInfo SyncInfo `json:"sync_info"`
}

type Response struct {
Result Result `json:"result"`
}

// fetchRPCNodeHeight fetches node height from the rpcURL
func fetchRPCNodeHeight(rpcURL string) (uint64, error) {
// Make a GET request to the REST endpoint
resp, err := http.Get(rpcURL + "/status")
if err != nil {
return 0, fmt.Errorf("error making GET request: %v", err)
}
defer resp.Body.Close()

// Read the response body
limitedReader := &io.LimitedReader{R: resp.Body, N: 4 * 1024}
body, err := io.ReadAll(limitedReader)
if err != nil {
return 0, fmt.Errorf("error reading response body: %v", err)
}

// Unmarshal JSON data into Response struct
var response Response
err = json.Unmarshal(body, &response)
if err != nil {
return 0, fmt.Errorf("error unmarshaling JSON data: %v", err)
}

// Extract the latest_block_height as a number
latestBlockHeightStr := response.Result.SyncInfo.LatestBlockHeight
latestBlockHeight, err := strconv.ParseUint(latestBlockHeightStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("error converting latest_block_height to a number: %v", err)
}

log.Debugf("Latest block height [%s]: %d\n", rpcURL, latestBlockHeight)

return latestBlockHeight, nil
}
13 changes: 12 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
metrics:
# Metric name
cosmos_asset_exponent:
type: "gauge"
description: "Exponent value for a Cosmos asset"
# Metric labels
labels: [ "chain", "denom" ]
Expand All @@ -16,3 +15,15 @@ metrics:
- { labels: [ "osmosis-1", "uosmo" ], value: 6 }
- { labels: [ "sommelier-3", "usomm" ], value: 6 }
- { labels: [ "theta-testnet-001", "uatom" ], value: 6 }
public_rpc_node_height:
description: "Node height of a public RPC node"
labels: [ "chain_id", "source" ]
chains:
- name: agoric-emerynet-5
rpcs:
- https://agoric-rpc.easy2stake.com
- https://agoric-rpc.polkachu.com
- name: axelar-dojo-1
rpcs:
- https://axelar-rpc.quantnode.tech
- https://rpc.axelar.bh.rocks
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module sl-exporter
module github.com/strangelove-ventures/sl-exporter

go 1.20

Expand Down
Loading

0 comments on commit c6d7285

Please sign in to comment.