synapsecns/sanguine

View on GitHub
ethergo/listener/otel.go

Summary

Maintainability
A
35 mins
Test Coverage
package listener

import (
    "context"
    "fmt"
    "time"

    "github.com/synapsecns/sanguine/core/metrics"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/metric"
)

const meterName = "github.com/synapsecns/sanguine/ethergo/listener"

// generate an interface for otelRecorder that exports the public method.
// this allows us to avoid using recordX externally anad makes the package less confusing.
//
// =============================================================================
// =============================================================================
// IMPORTANT: DO NOT REMOVE THIS COMMENT.
// NOTICE: PLEASE MAKE SURE YOU UPDATE BOTH THE DOCS AND THE GRAFANA DASHBOARD (IF NEEDED) AFTER UPDATING METRICS.
// =============================================================================
// =============================================================================
//
//go:generate go run github.com/vburenin/ifacemaker -f otel.go -s otelRecorder -i iOtelRecorder -p listener -o otel_generated.go -c "autogenerated file"
type otelRecorder struct {
    metrics metrics.Handler
    // meter is the metrics meter.
    meter metric.Meter
    // lastBlockGauge is the gauge for the last block.
    lastBlockGauge metric.Int64ObservableGauge
    // lastFetchedBlockAgeGauge is the gauge for the last block age.
    lastFetchedBlockAgeGauge metric.Float64ObservableGauge
    // lastBlock is the last block processed by the listener.
    lastBlock *uint64
    // lastBlockFetchTime is the time the last block was fetched (used to calculate last block age).
    lastBlockFetchTime *time.Time
    // chainID is the chain ID for the listener.
    chainID int
    // listenerName is the name of the listener.
    listenerName string
}

func newOtelRecorder(meterHandler metrics.Handler, chainID int, name string) (_ iOtelRecorder, err error) {
    or := otelRecorder{
        metrics:            meterHandler,
        meter:              meterHandler.Meter(meterName),
        lastBlock:          nil,
        lastBlockFetchTime: nil,
        chainID:            chainID,
        listenerName:       name,
    }

    or.lastBlockGauge, err = or.meter.Int64ObservableGauge("last_block")
    if err != nil {
        return nil, fmt.Errorf("could not create last block gauge")
    }

    or.lastFetchedBlockAgeGauge, err = or.meter.Float64ObservableGauge("last_block_age")
    if err != nil {
        return nil, fmt.Errorf("could not create last block age gauge")
    }

    _, err = or.meter.RegisterCallback(or.recordLastBlock, or.lastBlockGauge)
    if err != nil {
        return nil, fmt.Errorf("could not register callback for last block gauge")
    }

    _, err = or.meter.RegisterCallback(or.recordLastFetchedBlockAge, or.lastFetchedBlockAgeGauge)
    if err != nil {
        return nil, fmt.Errorf("could not register callback for last block age gauge")
    }

    return &or, nil
}

func (o *otelRecorder) recordLastBlock(_ context.Context, observer metric.Observer) (err error) {
    if o.metrics == nil || o.lastBlockGauge == nil || o.lastBlock == nil {
        return nil
    }

    opts := metric.WithAttributes(
        attribute.Int(metrics.ChainID, o.chainID),
        attribute.String("listener_name", o.listenerName),
    )
    observer.ObserveInt64(o.lastBlockGauge, int64(*o.lastBlock), opts)

    return nil
}

func (o *otelRecorder) recordLastFetchedBlockAge(_ context.Context, observer metric.Observer) (err error) {
    if o.metrics == nil || o.lastFetchedBlockAgeGauge == nil || o.lastBlockFetchTime == nil {
        return nil
    }

    age := time.Since(*o.lastBlockFetchTime).Seconds()
    opts := metric.WithAttributes(
        attribute.Int(metrics.ChainID, o.chainID),
        attribute.String("listener_name", o.listenerName),
    )
    observer.ObserveFloat64(o.lastFetchedBlockAgeGauge, age, opts)

    return nil
}

// RecordLastBlock records the last block processed by the listener.
func (o *otelRecorder) RecordLastBlock(lastBlock uint64) {
    // verify if the last block has changed
    var hasChanged bool
    if o.lastBlock == nil {
        hasChanged = true
    } else {
        hasChanged = *o.lastBlock != lastBlock
    }
    if !hasChanged {
        return
    }

    // record the block
    o.lastBlock = &lastBlock
    fetchTime := time.Now()
    o.lastBlockFetchTime = &fetchTime
}