contrib/promexporter/exporters/otel.go
package exporters
import (
"context"
"math/big"
"github.com/cornelk/hashmap"
"github.com/ethereum/go-ethereum/common"
"github.com/hedzr/log"
"github.com/synapsecns/sanguine/core"
"github.com/synapsecns/sanguine/core/metrics"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)
type submitterMetadata struct {
address common.Address
name string
nonce int64
balance float64
}
type relayerMetadata struct {
address common.Address
balance float64
usdcBalance float64
}
//go:generate go run github.com/vburenin/ifacemaker -f otel.go -s otelRecorder -i iOtelRecorder -p exporters -o otel_generated.go -c "autogenerated file"
type otelRecorder struct {
metrics metrics.Handler
meter metric.Meter
// VPRICE
vPrice *hashmap.Map[int, float64]
vpriceGauge metric.Float64ObservableGauge
// BRIDGE
// chainID -> []tokenData
td *hashmap.Map[int, []tokenData]
// How much gas is left on the bridge.
gasBalance *hashmap.Map[int, float64]
// tokenData field gauges
gasBalanceGauge metric.Float64ObservableGauge
bridgeBalanceGauge metric.Float64ObservableGauge
feeBalanceGauge metric.Float64ObservableGauge
totalSupplyGauge metric.Float64ObservableGauge
// dfk stats
stuckHeroes *hashmap.Map[string, int64]
stuckHeroesGauge metric.Int64ObservableGauge
// submitter stats
submitters *hashmap.Map[int, []submitterMetadata]
balanceGauge metric.Float64ObservableGauge
nonceGauge metric.Int64ObservableGauge
// relayer stats
relayerBalance *hashmap.Map[int, []relayerMetadata]
relayerBalanceGauge metric.Float64ObservableGauge
relayerUSDCBalanceGuage metric.Float64ObservableGauge
}
// TODO: unexport all methods.
// nolint: cyclop
func newOtelRecorder(meterHandler metrics.Handler) iOtelRecorder {
otr := otelRecorder{
metrics: meterHandler,
meter: meterHandler.Meter(meterName),
stuckHeroes: hashmap.New[string, int64](),
vPrice: hashmap.New[int, float64](),
gasBalance: hashmap.New[int, float64](),
td: hashmap.New[int, []tokenData](),
submitters: hashmap.New[int, []submitterMetadata](),
relayerBalance: hashmap.New[int, []relayerMetadata](),
}
var err error
if otr.vpriceGauge, err = otr.meter.Float64ObservableGauge("vpriceMetric"); err != nil {
log.Warnf("failed to create vprice gauge: %v", err)
}
if otr.bridgeBalanceGauge, err = otr.meter.Float64ObservableGauge("bridgeBalanceMetric"); err != nil {
log.Warnf("failed to create bridgeBalance gauge: %v", err)
}
if otr.feeBalanceGauge, err = otr.meter.Float64ObservableGauge("feeBalance_total"); err != nil {
log.Warnf("failed to create feeBalance gauge: %v", err)
}
if otr.totalSupplyGauge, err = otr.meter.Float64ObservableGauge("totalSupply"); err != nil {
log.Warnf("failed to create totalSupply gauge: %v", err)
}
if otr.gasBalanceGauge, err = otr.meter.Float64ObservableGauge("gasBalance"); err != nil {
log.Warnf("failed to create gasBalance gauge: %v", err)
}
if otr.balanceGauge, err = otr.meter.Float64ObservableGauge("gas_balance"); err != nil {
log.Warnf("failed to create balance gauge: %v", err)
}
if otr.nonceGauge, err = otr.meter.Int64ObservableGauge("nonce"); err != nil {
log.Warnf("failed to create nonce gauge: %v", err)
}
if otr.stuckHeroesGauge, err = otr.meter.Int64ObservableGauge("dfk_pending_heroes"); err != nil {
log.Warnf("failed to create stuckHeroes gauge: %v", err)
}
if otr.relayerBalanceGauge, err = otr.meter.Float64ObservableGauge("relayer_balance"); err != nil {
log.Warnf("failed to create relayerBalance gauge: %v", err)
}
if otr.relayerUSDCBalanceGuage, err = otr.meter.Float64ObservableGauge("relayer_usdc_balance"); err != nil {
log.Warnf("failed to create relayerUSDCBalance gauge: %v", err)
}
// Register VPrice callback
if _, err = otr.meter.RegisterCallback(otr.recordVpriceGauge, otr.vpriceGauge); err != nil {
log.Warnf("failed to register callback for vprice metrics: %v", err)
}
// Register DFK Stuck Heroes Callback
if _, err = otr.meter.RegisterCallback(otr.recordStuckHeroCount, otr.stuckHeroesGauge); err != nil {
log.Warnf("failed to register callback for dfk stuck heroes metrics: %v", err)
}
// Register Token Balance Callback
if _, err = otr.meter.RegisterCallback(
otr.recordTokenBalance,
otr.bridgeBalanceGauge,
otr.feeBalanceGauge,
otr.totalSupplyGauge,
); err != nil {
log.Warnf("failed to register callback for bridge metrics : %v", err)
}
// Register Submitter Stats Callback
if _, err = otr.meter.RegisterCallback(otr.recordSubmitterStats, otr.balanceGauge, otr.nonceGauge); err != nil {
log.Warnf("failed to register callback for submitter metrics: %v", err)
}
if _, err = otr.meter.RegisterCallback(otr.recordBridgeGasBalance, otr.gasBalanceGauge); err != nil {
log.Warnf("failed to register callback for bridge gas balance metrics: %v", err)
}
// The Relayer code of interest.
if _, err = otr.meter.RegisterCallback(otr.recordRelayerBalance, otr.relayerBalanceGauge, otr.relayerUSDCBalanceGuage); err != nil {
log.Warnf("failed to register callback for relayer balance metrics: %v", err)
}
return &otr
}
// Virtual Price Metrics.
func (o *otelRecorder) RecordVPrice(chainid int, vPrice float64) {
o.vPrice.Set(chainid, vPrice)
}
func (o *otelRecorder) recordVpriceGauge(
_ context.Context,
observer metric.Observer,
) (err error) {
if o.metrics == nil || o.vpriceGauge == nil {
return nil
}
o.vPrice.Range(
func(chainid int, vprice float64) bool {
observer.ObserveFloat64(
o.vpriceGauge,
vprice,
metric.WithAttributes(attribute.Int(metrics.ChainID, chainid)),
)
return true
},
)
return nil
}
// Token Balance Metrics.
func (o *otelRecorder) RecordBridgeGasBalance(chainid int, gasBalance float64) {
o.gasBalance.Set(chainid, gasBalance)
}
func (o *otelRecorder) recordBridgeGasBalance(_ context.Context, observer metric.Observer) (err error) {
if o.metrics == nil || o.bridgeBalanceGauge == nil {
return nil
}
o.gasBalance.Range(func(chainID int, gasBalance float64) bool {
observer.ObserveFloat64(
o.gasBalanceGauge,
gasBalance,
metric.WithAttributes(attribute.Int(metrics.ChainID, chainID)),
)
return true
})
return nil
}
type tokenData struct {
metadata TokenConfig
contractBalance *big.Int
totalSuppply *big.Int
feeBalance *big.Int
}
func (o *otelRecorder) RecordTokenBalance(
chainID int,
tokenData tokenData,
) {
td, _ := o.td.Get(chainID)
td = append(td, tokenData)
o.td.Set(chainID, td)
}
func (o *otelRecorder) recordTokenBalance(
_ context.Context,
observer metric.Observer,
) (err error) {
if o.metrics == nil || o.bridgeBalanceGauge == nil {
return nil
}
o.td.Range(func(_ int, td []tokenData) bool {
for _, token := range td {
tokenAttributes := attribute.NewSet(
attribute.String("tokenID", token.metadata.TokenID),
attribute.Int(metrics.ChainID, token.metadata.ChainID),
)
bridgeBalance := core.BigToDecimals(token.contractBalance, token.metadata.TokenDecimals)
observer.ObserveFloat64(
o.bridgeBalanceGauge,
bridgeBalance,
metric.WithAttributeSet(tokenAttributes),
)
feeBalance := core.BigToDecimals(token.feeBalance, token.metadata.TokenDecimals)
observer.ObserveFloat64(
o.feeBalanceGauge,
feeBalance,
metric.WithAttributeSet(tokenAttributes),
)
totalSupply := core.BigToDecimals(token.totalSuppply, token.metadata.TokenDecimals)
observer.ObserveFloat64(
o.totalSupplyGauge,
totalSupply,
metric.WithAttributeSet(tokenAttributes),
)
}
return true
})
return nil
}
// DFK Metrics.
func (o *otelRecorder) RecordStuckHeroCount(stuckHeroes int64, chainname string) {
o.stuckHeroes.Set(chainname, stuckHeroes)
}
func (o *otelRecorder) recordStuckHeroCount(
_ context.Context,
observer metric.Observer,
) (err error) {
if o.metrics == nil || o.stuckHeroesGauge == nil {
return nil
}
o.stuckHeroes.Range(
func(chainName string, stuckHeroes int64) bool {
observer.ObserveInt64(
o.stuckHeroesGauge,
stuckHeroes,
metric.WithAttributes(
attribute.String("chain_name", chainName),
),
)
return true
})
return nil
}
// Submitter stats.
func (o *otelRecorder) RecordSubmitterStats(chainid int, metadata submitterMetadata) {
submitters, _ := o.submitters.Get(chainid)
submitters = append(submitters, metadata)
o.submitters.Set(chainid, submitters)
}
func (o *otelRecorder) recordSubmitterStats(
_ context.Context,
observer metric.Observer,
) (err error) {
if o.metrics == nil || o.nonceGauge == nil {
return nil
}
o.submitters.Range(func(chainID int, submitters []submitterMetadata) bool {
for _, submitter := range submitters {
observer.ObserveInt64(
o.nonceGauge,
submitter.nonce,
metric.WithAttributes(attribute.Int(metrics.ChainID, chainID),
attribute.String(metrics.EOAAddress, submitter.address.String()),
attribute.String("name", submitter.name),
),
)
observer.ObserveFloat64(
o.balanceGauge,
submitter.balance,
metric.WithAttributes(
attribute.Int(metrics.ChainID, chainID),
attribute.String(metrics.EOAAddress, submitter.address.String()),
attribute.String("name", submitter.name),
),
)
}
return true
})
return nil
}
// RELAYER CODE.
func (o *otelRecorder) RecordRelayerBalance(chainID int, relayer relayerMetadata) {
relayerBalances, _ := o.relayerBalance.Get(chainID)
relayerBalances = append(relayerBalances, relayer)
o.relayerBalance.Set(chainID, relayerBalances)
}
func (o *otelRecorder) recordRelayerBalance(
_ context.Context,
observer metric.Observer,
) (err error) {
if o.metrics == nil || o.relayerBalance == nil {
return nil
}
o.relayerBalance.Range(func(chainID int, relayerBalances []relayerMetadata) bool {
for _, relayer := range relayerBalances {
observer.ObserveFloat64(
o.relayerBalanceGauge,
relayer.balance,
metric.WithAttributes(
attribute.Int(metrics.ChainID, chainID),
attribute.String("relayer_address", relayer.address.String()),
),
)
observer.ObserveFloat64(
o.relayerUSDCBalanceGuage,
relayer.usdcBalance,
metric.WithAttributes(
attribute.Int(metrics.ChainID, chainID),
attribute.String("relayer_address", relayer.address.String()),
),
)
}
return true
})
return nil
}