main.go 6.8 KB
Newer Older
1 2 3
package main

import (
4
	"context"
5
	"fmt"
6
	"io/ioutil"
7 8 9 10 11
	"net/http"
	"os"
	"sync"
	"time"

12
	"github.com/ethereum-optimism/optimism/go/op_exporter/k8sClient"
13 14 15 16 17 18
	"github.com/ethereum-optimism/optimism/go/op_exporter/version"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	log "github.com/sirupsen/logrus"
	"github.com/ybbus/jsonrpc"
	"gopkg.in/alecthomas/kingpin.v2"
19 20
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
)

var (
	listenAddress = kingpin.Flag(
		"web.listen-address",
		"Address on which to expose metrics and web interface.",
	).Default(":9100").String()
	rpcProvider = kingpin.Flag(
		"rpc.provider",
		"Address for RPC provider.",
	).Default("http://127.0.0.1:8545").String()
	networkLabel = kingpin.Flag(
		"label.network",
		"Label to apply to the metrics to identify the network.",
	).Default("mainnet").String()
	versionFlag = kingpin.Flag(
		"version",
		"Display binary version.",
	).Default("False").Bool()
	unhealthyTimePeriod = kingpin.Flag(
		"wait.minutes",
		"Number of minutes to wait for the next block before marking provider unhealthy.",
	).Default("10").Int()
44 45 46 47
	enableK8sQuery = kingpin.Flag(
		"k8s.enable",
		"Enable kubernetes info lookup.",
	).Default("true").Bool()
48 49 50
)

type healthCheck struct {
51 52 53 54 55 56
	mu             *sync.RWMutex
	height         uint64
	healthy        bool
	updateTime     time.Time
	allowedMethods []string
	version        *string
57 58 59 60 61 62
}

func healthHandler(health *healthCheck) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		health.mu.RLock()
		defer health.mu.RUnlock()
63
		w.Write([]byte(fmt.Sprintf(`{ "healthy": "%t", "version": "%s" }`, health.healthy, *health.version)))
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
	}
}

func main() {
	kingpin.HelpFlag.Short('h')
	kingpin.Parse()
	if *versionFlag {
		fmt.Printf("(version=%s, gitcommit=%s)\n", version.Version, version.GitCommit)
		fmt.Printf("(go=%s, user=%s, date=%s)\n", version.GoVersion, version.BuildUser, version.BuildDate)
		os.Exit(0)
	}
	log.Infoln("exporter config", *listenAddress, *rpcProvider, *networkLabel)
	log.Infoln("Starting op_exporter", version.Info())
	log.Infoln("Build context", version.BuildContext())

	health := healthCheck{
80 81 82 83 84 85
		mu:             new(sync.RWMutex),
		height:         0,
		healthy:        false,
		updateTime:     time.Now(),
		allowedMethods: nil,
		version:        nil,
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
	}
	http.Handle("/metrics", promhttp.Handler())
	http.Handle("/health", healthHandler(&health))
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(`<html>
		<head><title>OP Exporter</title></head>
		<body>
		<h1>OP Exporter</h1>
		<p><a href="/metrics">Metrics</a></p>
		<p><a href="/health">Health</a></p>
		</body>
		</html>`))
	})
	go getRollupGasPrices()
	go getBlockNumber(&health)
101 102 103 104 105 106 107
	if *enableK8sQuery {
		client, err := k8sClient.Newk8sClient()
		if err != nil {
			log.Fatal(err)
		}
		go getSequencerVersion(&health, client)
	}
108 109 110 111 112 113 114
	log.Infoln("Listening on", *listenAddress)
	if err := http.ListenAndServe(*listenAddress, nil); err != nil {
		log.Fatal(err)
	}

}

115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
func getSequencerVersion(health *healthCheck, client *kubernetes.Clientset) {
	ns, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
	if err != nil {
		log.Fatalf("Unable to read namespace file: %s", err)
	}
	ticker := time.NewTicker(30 * time.Second)
	for {
		<-ticker.C
		getOpts := metav1.GetOptions{
			TypeMeta:        metav1.TypeMeta{},
			ResourceVersion: "",
		}
		sequencerStatefulSet, err := client.AppsV1().StatefulSets(string(ns)).Get(context.TODO(), "sequencer", getOpts)
		if err != nil {
			unknownStatus := "UNKNOWN"
			health.version = &unknownStatus
			log.Errorf("Unable to retrieve a sequencer StatefulSet: %s", err)
			continue
		}
		for _, c := range sequencerStatefulSet.Spec.Template.Spec.Containers {
			log.Infof("Checking container %s", c.Name)
			switch {
			case c.Name == "sequencer":
				log.Infof("The sequencer version is: %s", c.Image)
				health.version = &c.Image
			default:
				log.Infof("Unable to find the sequencer container in the statefulset?!?")
			}
		}

	}
}

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
func getBlockNumber(health *healthCheck) {
	rpcClient := jsonrpc.NewClientWithOpts(*rpcProvider, &jsonrpc.RPCClientOpts{})
	var blockNumberResponse *string
	for {
		if err := rpcClient.CallFor(&blockNumberResponse, "eth_blockNumber"); err != nil {
			health.mu.Lock()
			health.healthy = false
			health.mu.Unlock()
			log.Warnln("Error calling eth_blockNumber, setting unhealthy", err)
		} else {
			log.Infoln("Got block number: ", *blockNumberResponse)
			health.mu.Lock()
			currentHeight, err := hexutil.DecodeUint64(*blockNumberResponse)
			blockNumber.WithLabelValues(
				*networkLabel, "layer2").Set(float64(currentHeight))
			if err != nil {
				log.Warnln("Error decoding block height", err)
				continue
			}
			lastHeight := health.height
			// If the currentHeight is the same as the lastHeight, check that
			// the unhealthyTimePeriod has passed and update health.healthy
			if currentHeight == lastHeight {
				currentTime := time.Now()
				lastTime := health.updateTime
				log.Warnln(fmt.Sprintf("Heights are the same, %v, %v", currentTime, lastTime))
				if lastTime.Add(time.Duration(*unhealthyTimePeriod) * time.Minute).Before(currentTime) {
					health.healthy = false
					log.Warnln("Heights are the same for the unhealthyTimePeriod, setting unhealthy")
				}
			} else {
				log.Warnln("New block height detected, setting healthy")
				health.height = currentHeight
				health.updateTime = time.Now()
				health.healthy = true
			}
			if health.healthy {
				healthySequencer.WithLabelValues(
					*networkLabel).Set(1)
			} else {
				healthySequencer.WithLabelValues(
					*networkLabel).Set(0)
			}

			health.mu.Unlock()
		}
		time.Sleep(time.Duration(30) * time.Second)
	}
}

func getRollupGasPrices() {
	rpcClient := jsonrpc.NewClientWithOpts(*rpcProvider, &jsonrpc.RPCClientOpts{})
	var rollupGasPrices *GetRollupGasPrices
	for {
		if err := rpcClient.CallFor(&rollupGasPrices, "rollup_gasPrices"); err != nil {
			log.Warnln("Error calling rollup_gasPrices", err)
		} else {
			l1GasPriceString := rollupGasPrices.L1GasPrice
			l1GasPrice, err := hexutil.DecodeUint64(l1GasPriceString)
			if err != nil {
				log.Warnln("Error converting gasPrice " + l1GasPriceString)
			}
			gasPrice.WithLabelValues(
				*networkLabel, "layer1").Set(float64(l1GasPrice))
			l2GasPriceString := rollupGasPrices.L2GasPrice
			l2GasPrice, err := hexutil.DecodeUint64(l2GasPriceString)
			if err != nil {
				log.Warnln("Error converting gasPrice " + l2GasPriceString)
			}
			gasPrice.WithLabelValues(
				*networkLabel, "layer2").Set(float64(l2GasPrice))
			log.Infoln("Got L1 gas string: ", l1GasPriceString)
			log.Infoln("Got L1 gas prices: ", l1GasPrice)
			log.Infoln("Got L2 gas string: ", l2GasPriceString)
			log.Infoln("Got L2 gas prices: ", l2GasPrice)
		}
		time.Sleep(time.Duration(30) * time.Second)
	}
}