Skip to content

Commit

Permalink
feat: Add prometheus metrics (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
gitferry authored Feb 22, 2024
1 parent 28ed172 commit d6176c3
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 8 deletions.
12 changes: 12 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type Config struct {

BTCNetParams chaincfg.Params

Metrics *MetricsConfig `group:"metrics" namespace:"metrics"`

BabylonConfig *BBNConfig `group:"babylon" namespace:"babylon"`
}

Expand Down Expand Up @@ -83,6 +85,14 @@ func LoadConfig(homePath string) (*Config, error) {
// illegal values or combination of values are set. All file system paths are
// normalized. The cleaned up config is returned on success.
func (cfg *Config) Validate() error {
if cfg.Metrics == nil {
return fmt.Errorf("empty metrics config")
}

if err := cfg.Metrics.Validate(); err != nil {
return fmt.Errorf("invalid metrics config")
}

switch cfg.BitcoinNetwork {
case "mainnet":
cfg.BTCNetParams = chaincfg.MainNetParams
Expand Down Expand Up @@ -117,13 +127,15 @@ func DefaultConfigWithHomePath(homePath string) Config {
bbnCfg := DefaultBBNConfig()
bbnCfg.Key = defaultCovenantKeyName
bbnCfg.KeyDirectory = homePath
metricsCfg := DefaultMetricsConfig()
cfg := Config{
LogLevel: defaultLogLevel,
QueryInterval: defaultQueryInterval,
DelegationLimit: defaultDelegationLimit,
SigsBatchSize: defaultSigsBatchSize,
BitcoinNetwork: defaultBitcoinNetwork,
BTCNetParams: defaultBTCNetParams,
Metrics: &metricsCfg,
BabylonConfig: &bbnCfg,
}

Expand Down
48 changes: 48 additions & 0 deletions config/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package config

import (
"fmt"
"net"
"time"
)

const (
defaultMetricsPort = 2112
defaultMetricsHost = "127.0.0.1"
defaultMetricsUpdateInterval = 100 * time.Millisecond
)

// MetricsConfig defines the server's basic configuration
type MetricsConfig struct {
Host string `long:"host" description:"IP of the Prometheus server"`
Port int `long:"port" description:"Port of the Prometheus server"`
UpdateInterval time.Duration `long:"updateinterval" description:"The interval of Prometheus metrics updated"`
}

func (cfg *MetricsConfig) Validate() error {
if cfg.Port < 0 || cfg.Port > 65535 {
return fmt.Errorf("invalid port: %d", cfg.Port)
}

ip := net.ParseIP(cfg.Host)
if ip == nil {
return fmt.Errorf("invalid host: %v", cfg.Host)
}

return nil
}

func (cfg *MetricsConfig) Address() (string, error) {
if err := cfg.Validate(); err != nil {
return "", err
}
return fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), nil
}

func DefaultMetricsConfig() MetricsConfig {
return MetricsConfig{
Port: defaultMetricsPort,
Host: defaultMetricsHost,
UpdateInterval: defaultMetricsUpdateInterval,
}
}
79 changes: 74 additions & 5 deletions covenant/covenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package covenant

import (
"bytes"
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"strings"
"sync"
"time"

"github.com/btcsuite/btcd/btcec/v2/schnorr"

"github.com/avast/retry-go/v4"
"github.com/btcsuite/btcd/btcec/v2"

Expand Down Expand Up @@ -99,6 +101,14 @@ func NewCovenantEmulator(
}, nil
}

func (ce *CovenantEmulator) Config() *covcfg.Config {
return ce.config
}

func (ce *CovenantEmulator) PublicKeyStr() string {
return hex.EncodeToString(schnorr.SerializePubKey(ce.pk))
}

func (ce *CovenantEmulator) UpdateParams() error {
params, err := ce.getParamsWithRetry()
if err != nil {
Expand Down Expand Up @@ -212,6 +222,10 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) (
}

// 5. sign covenant staking sigs
// record metrics
startSignTime := time.Now()
metricsTimeKeeper.SetPreviousSignStart(&startSignTime)

covenantPrivKey, err := ce.getPrivKey()
if err != nil {
return nil, fmt.Errorf("failed to get Covenant private key: %w", err)
Expand Down Expand Up @@ -300,6 +314,11 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) (
covSlashingSigs = append(covSlashingSigs, covenantSig.MustMarshal())
}

// record metrics
finishSignTime := time.Now()
metricsTimeKeeper.SetPreviousSignFinish(&finishSignTime)
timedSignDelegationLag.Observe(time.Since(startSignTime).Seconds())

// 8. collect covenant sigs
covenantSigs = append(covenantSigs, &types.CovenantSigs{
PublicKey: ce.pk,
Expand All @@ -309,8 +328,20 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) (
SlashingUnbondingSigs: covSlashingSigs,
})
}

// 9. submit covenant sigs
return ce.cc.SubmitCovenantSigs(covenantSigs)
res, err := ce.cc.SubmitCovenantSigs(covenantSigs)
if err != nil {
ce.recordMetricsFailedSignDelegations(len(covenantSigs))
return nil, err
}

// record metrics
submittedTime := time.Now()
metricsTimeKeeper.SetPreviousSubmission(&submittedTime)
ce.recordMetricsTotalSignDelegationsSubmitted(len(covenantSigs))

return res, nil
}

func (ce *CovenantEmulator) getPrivKey() (*btcec.PrivateKey, error) {
Expand Down Expand Up @@ -368,6 +399,9 @@ func (ce *CovenantEmulator) covenantSigSubmissionLoop() {
limit := ce.config.DelegationLimit
covenantSigTicker := time.NewTicker(interval)

ce.logger.Info("starting signature submission loop",
zap.Float64("interval seconds", interval.Seconds()))

for {
select {
case <-covenantSigTicker.C:
Expand All @@ -383,6 +417,10 @@ func (ce *CovenantEmulator) covenantSigSubmissionLoop() {
ce.logger.Debug("failed to get pending delegations", zap.Error(err))
continue
}

// record delegation metrics
ce.recordMetricsCurrentPendingDelegations(len(dels))

if len(dels) == 0 {
ce.logger.Debug("no pending delegations are found")
}
Expand All @@ -409,6 +447,26 @@ func (ce *CovenantEmulator) covenantSigSubmissionLoop() {

}

func (ce *CovenantEmulator) metricsUpdateLoop() {
defer ce.wg.Done()

interval := ce.config.Metrics.UpdateInterval
ce.logger.Info("starting metrics update loop",
zap.Float64("interval seconds", interval.Seconds()))
updateTicker := time.NewTicker(interval)

for {
select {
case <-updateTicker.C:
metricsTimeKeeper.UpdatePrometheusMetrics()
case <-ce.quit:
updateTicker.Stop()
ce.logger.Info("exiting metrics update loop")
return
}
}
}

func CreateCovenantKey(keyringDir, chainID, keyName, backend, passphrase, hdPath string) (*types.ChainKeyInfo, error) {
sdkCtx, err := keyring.CreateClientCtx(
keyringDir, chainID,
Expand Down Expand Up @@ -455,13 +513,26 @@ func (ce *CovenantEmulator) getParamsWithRetry() (*types.StakingParams, error) {
return params, nil
}

func (ce *CovenantEmulator) recordMetricsFailedSignDelegations(n int) {
failedSignDelegations.WithLabelValues(ce.PublicKeyStr()).Add(float64(n))
}

func (ce *CovenantEmulator) recordMetricsTotalSignDelegationsSubmitted(n int) {
totalSignDelegationsSubmitted.WithLabelValues(ce.PublicKeyStr()).Add(float64(n))
}

func (ce *CovenantEmulator) recordMetricsCurrentPendingDelegations(n int) {
currentPendingDelegations.WithLabelValues(ce.PublicKeyStr()).Set(float64(n))
}

func (ce *CovenantEmulator) Start() error {
var startErr error
ce.startOnce.Do(func() {
ce.logger.Info("Starting Covenant Emulator")

ce.wg.Add(1)
ce.wg.Add(2)
go ce.covenantSigSubmissionLoop()
go ce.metricsUpdateLoop()
})

return startErr
Expand All @@ -472,8 +543,6 @@ func (ce *CovenantEmulator) Stop() error {
ce.stopOnce.Do(func() {
ce.logger.Info("Stopping Covenant Emulator")

// Always stop the submission loop first to not generate additional events and actions
ce.logger.Debug("Stopping submission loop")
close(ce.quit)
ce.wg.Wait()

Expand Down
100 changes: 100 additions & 0 deletions covenant/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package covenant

import (
"sync"
"time"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

type metricsTimer struct {
mu sync.Mutex
previousSubmission *time.Time
previousLocalSignStart, previousLocalSignFinish *time.Time
}

func newMetricsTimer() *metricsTimer {
return &metricsTimer{
mu: sync.Mutex{},
}
}

func (mt *metricsTimer) SetPreviousSubmission(t *time.Time) {
mt.mu.Lock()
defer mt.mu.Unlock()
mt.previousSubmission = t
}

func (mt *metricsTimer) SetPreviousSignStart(t *time.Time) {
mt.mu.Lock()
defer mt.mu.Unlock()
mt.previousLocalSignStart = t
}

func (mt *metricsTimer) SetPreviousSignFinish(t *time.Time) {
mt.mu.Lock()
defer mt.mu.Unlock()
mt.previousLocalSignFinish = t
}

func (mt *metricsTimer) UpdatePrometheusMetrics() {
mt.mu.Lock()
defer mt.mu.Unlock()

// only start updating metrics after the first submission is finished
if mt.previousSubmission == nil || mt.previousLocalSignStart == nil || mt.previousLocalSignFinish == nil {
return
}

// Update Prometheus Gauges
secondsSinceLastSubmission.Set(time.Since(*mt.previousSubmission).Seconds())
secondsSinceLastSignStart.Set(time.Since(*mt.previousLocalSignStart).Seconds())
secondsSinceLastSignFinish.Set(time.Since(*mt.previousLocalSignFinish).Seconds())
}

var (
// Variables to calculate Prometheus Metrics
metricsTimeKeeper = newMetricsTimer()

// Prometheus metrics
totalSignDelegationsSubmitted = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "ce_total_sign_delegations_submitted",
Help: "Total number of signed delegations submitted",
},
[]string{"covenant_pk"},
)
failedSignDelegations = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "ce_total_failed_sign_delegations",
Help: "Total number of failed sign delegations",
},
[]string{"covenant_pk"},
)
currentPendingDelegations = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "ce_current_pending_delegations",
Help: "The number of current pending delegations",
},
[]string{"covenant_pk"},
)
secondsSinceLastSubmission = promauto.NewGauge(prometheus.GaugeOpts{
Name: "ce_seconds_since_last_submission",
Help: "Seconds since last submission of signatures",
})
secondsSinceLastSignStart = promauto.NewGauge(prometheus.GaugeOpts{
Name: "ce_seconds_since_last_sign_start_time",
Help: "Seconds since last sign start",
})
secondsSinceLastSignFinish = promauto.NewGauge(prometheus.GaugeOpts{
Name: "ce_seconds_since_last_sign_finish_time",
Help: "Seconds since last sign finish",
})

timedSignDelegationLag = promauto.NewSummary(prometheus.SummaryOpts{
Name: "ce_sign_delegation_lag_seconds",
Help: "Seconds taken to sign a delegation",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
})
)
Loading

0 comments on commit d6176c3

Please sign in to comment.