github.com/prysmaticlabs/prysm@v1.4.4/tools/eth1exporter/main.go (about)

     1  // Prometheus exporter for Ethereum address balances.
     2  // Forked from https://github.com/hunterlong/ethexporter
     3  package main
     4  
     5  import (
     6  	"bufio"
     7  	"context"
     8  	"flag"
     9  	"fmt"
    10  	"log"
    11  	"math/big"
    12  	"net/http"
    13  	"os"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/ethereum/go-ethereum/common"
    18  	"github.com/ethereum/go-ethereum/ethclient"
    19  	"github.com/ethereum/go-ethereum/params"
    20  	_ "github.com/prysmaticlabs/prysm/shared/maxprocs"
    21  	"github.com/sirupsen/logrus"
    22  )
    23  
    24  var (
    25  	allWatching []*Watching
    26  	loadSeconds float64
    27  	totalLoaded int64
    28  	eth         *ethclient.Client
    29  )
    30  
    31  var (
    32  	port            = flag.Int("port", 9090, "Port to serve /metrics")
    33  	web3URL         = flag.String("web3-provider", "https://goerli.prylabs.net", "Web3 URL to access information about ETH1")
    34  	prefix          = flag.String("prefix", "", "Metrics prefix.")
    35  	addressFilePath = flag.String("addresses", "", "File path to addresses text file.")
    36  )
    37  
    38  func main() {
    39  	flag.Parse()
    40  
    41  	if *addressFilePath == "" {
    42  		log.Println("--addresses is required")
    43  		return
    44  	}
    45  
    46  	err := OpenAddresses(*addressFilePath)
    47  	if err != nil {
    48  		panic(err)
    49  	}
    50  
    51  	err = ConnectionToGeth(*web3URL)
    52  	if err != nil {
    53  		panic(err)
    54  	}
    55  
    56  	// check address balances
    57  	go func() {
    58  		for {
    59  			totalLoaded = 0
    60  			t1 := time.Now()
    61  			fmt.Printf("Checking %v wallets...\n", len(allWatching))
    62  			for _, v := range allWatching {
    63  				v.Balance = EthBalance(v.Address).String()
    64  				totalLoaded++
    65  			}
    66  			t2 := time.Now()
    67  			loadSeconds = t2.Sub(t1).Seconds()
    68  			fmt.Printf("Finished checking %v wallets in %0.0f seconds, sleeping for %v seconds.\n", len(allWatching), loadSeconds, 15)
    69  			time.Sleep(15 * time.Second)
    70  		}
    71  	}()
    72  
    73  	block := CurrentBlock()
    74  
    75  	fmt.Printf("ETHexporter has started on port %v using web3 server: %v at block #%v\n", *port, *web3URL, block)
    76  
    77  	http.HandleFunc("/metrics", MetricsHTTP)
    78  	http.HandleFunc("/reload", ReloadHTTP)
    79  	log.Fatal(http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", *port), nil))
    80  }
    81  
    82  // Watching address wrapper
    83  type Watching struct {
    84  	Name    string
    85  	Address string
    86  	Balance string
    87  }
    88  
    89  // ConnectionToGeth - Connect to remote server.
    90  func ConnectionToGeth(url string) error {
    91  	var err error
    92  	eth, err = ethclient.Dial(url)
    93  	return err
    94  }
    95  
    96  // EthBalance from remote server.
    97  func EthBalance(address string) *big.Float {
    98  	balance, err := eth.BalanceAt(context.TODO(), common.HexToAddress(address), nil)
    99  	if err != nil {
   100  		fmt.Printf("Error fetching ETH Balance for address: %v\n", address)
   101  	}
   102  	return ToEther(balance)
   103  }
   104  
   105  // CurrentBlock in ETH1.
   106  func CurrentBlock() uint64 {
   107  	block, err := eth.BlockByNumber(context.TODO(), nil)
   108  	if err != nil {
   109  		fmt.Printf("Error fetching current block height: %v\n", err)
   110  		return 0
   111  	}
   112  	return block.NumberU64()
   113  }
   114  
   115  // ToEther from Wei.
   116  func ToEther(o *big.Int) *big.Float {
   117  	wei := big.NewFloat(0)
   118  	wei.SetInt(o)
   119  	return new(big.Float).Quo(wei, big.NewFloat(params.Ether))
   120  }
   121  
   122  // MetricsHTTP - HTTP response handler for /metrics.
   123  func MetricsHTTP(w http.ResponseWriter, _ *http.Request) {
   124  	var allOut []string
   125  	total := big.NewFloat(0)
   126  	for _, v := range allWatching {
   127  		if v.Balance == "" {
   128  			v.Balance = "0"
   129  		}
   130  		bal := big.NewFloat(0)
   131  		bal.SetString(v.Balance)
   132  		total.Add(total, bal)
   133  		allOut = append(allOut, fmt.Sprintf("%veth_balance{name=\"%v\",address=\"%v\"} %v", *prefix, v.Name, v.Address, v.Balance))
   134  	}
   135  	allOut = append(allOut,
   136  		fmt.Sprintf("%veth_balance_total %0.18f", *prefix, total),
   137  		fmt.Sprintf("%veth_load_seconds %0.2f", *prefix, loadSeconds),
   138  		fmt.Sprintf("%veth_loaded_addresses %v", *prefix, totalLoaded),
   139  		fmt.Sprintf("%veth_total_addresses %v", *prefix, len(allWatching)))
   140  
   141  	if _, err := fmt.Fprintln(w, strings.Join(allOut, "\n")); err != nil {
   142  		logrus.WithError(err).Error("Failed to write metrics")
   143  	}
   144  }
   145  
   146  // ReloadHTTP reloads the addresses from disk.
   147  func ReloadHTTP(w http.ResponseWriter, _ *http.Request) {
   148  	if err := OpenAddresses(*addressFilePath); err != nil {
   149  		w.WriteHeader(http.StatusInternalServerError)
   150  		return
   151  	}
   152  	w.WriteHeader(http.StatusOK)
   153  	log.Println("Reloaded addresses")
   154  }
   155  
   156  // OpenAddresses from text file (name:address)
   157  func OpenAddresses(filename string) error {
   158  	file, err := os.Open(filename)
   159  	if err != nil {
   160  		return err
   161  	}
   162  	defer func() {
   163  		if err := file.Close(); err != nil {
   164  			panic(err)
   165  		}
   166  	}()
   167  	scanner := bufio.NewScanner(file)
   168  	allWatching = []*Watching{}
   169  	for scanner.Scan() {
   170  		object := strings.Split(scanner.Text(), ":")
   171  		if common.IsHexAddress(object[1]) {
   172  			w := &Watching{
   173  				Name:    object[0],
   174  				Address: object[1],
   175  			}
   176  			allWatching = append(allWatching, w)
   177  		}
   178  	}
   179  	if err := scanner.Err(); err != nil {
   180  		return err
   181  	}
   182  	return err
   183  }