From 845fbb460300646d11783648d85c170ce713f4d1 Mon Sep 17 00:00:00 2001
From: Ceyhun Onur <ceyhun.onur@avalabs.org>
Date: Wed, 14 Feb 2024 20:58:50 +0300
Subject: [PATCH] improve metrics (#1093)

* improve metrics

* printout metric descriptions

* use json output
---
 cmd/simulator/config/flags.go    | 40 ++++++++++++++++-------------
 cmd/simulator/load/loader.go     | 15 +++++++----
 cmd/simulator/metrics/metrics.go | 44 ++++++++++++++++++++------------
 3 files changed, 59 insertions(+), 40 deletions(-)

diff --git a/cmd/simulator/config/flags.go b/cmd/simulator/config/flags.go
index d1db9f1b68..8fb3d3c5fd 100644
--- a/cmd/simulator/config/flags.go
+++ b/cmd/simulator/config/flags.go
@@ -28,6 +28,7 @@ const (
 	TimeoutKey        = "timeout"
 	BatchSizeKey      = "batch-size"
 	MetricsPortKey    = "metrics-port"
+	MetricsOutputKey  = "metrics-output"
 )
 
 var (
@@ -37,28 +38,30 @@ var (
 )
 
 type Config struct {
-	Endpoints    []string      `json:"endpoints"`
-	MaxFeeCap    int64         `json:"max-fee-cap"`
-	MaxTipCap    int64         `json:"max-tip-cap"`
-	Workers      int           `json:"workers"`
-	TxsPerWorker uint64        `json:"txs-per-worker"`
-	KeyDir       string        `json:"key-dir"`
-	Timeout      time.Duration `json:"timeout"`
-	BatchSize    uint64        `json:"batch-size"`
-	MetricsPort  uint64        `json:"metrics-port"`
+	Endpoints     []string      `json:"endpoints"`
+	MaxFeeCap     int64         `json:"max-fee-cap"`
+	MaxTipCap     int64         `json:"max-tip-cap"`
+	Workers       int           `json:"workers"`
+	TxsPerWorker  uint64        `json:"txs-per-worker"`
+	KeyDir        string        `json:"key-dir"`
+	Timeout       time.Duration `json:"timeout"`
+	BatchSize     uint64        `json:"batch-size"`
+	MetricsPort   uint64        `json:"metrics-port"`
+	MetricsOutput string        `json:"metrics-output"`
 }
 
 func BuildConfig(v *viper.Viper) (Config, error) {
 	c := Config{
-		Endpoints:    v.GetStringSlice(EndpointsKey),
-		MaxFeeCap:    v.GetInt64(MaxFeeCapKey),
-		MaxTipCap:    v.GetInt64(MaxTipCapKey),
-		Workers:      v.GetInt(WorkersKey),
-		TxsPerWorker: v.GetUint64(TxsPerWorkerKey),
-		KeyDir:       v.GetString(KeyDirKey),
-		Timeout:      v.GetDuration(TimeoutKey),
-		BatchSize:    v.GetUint64(BatchSizeKey),
-		MetricsPort:  v.GetUint64(MetricsPortKey),
+		Endpoints:     v.GetStringSlice(EndpointsKey),
+		MaxFeeCap:     v.GetInt64(MaxFeeCapKey),
+		MaxTipCap:     v.GetInt64(MaxTipCapKey),
+		Workers:       v.GetInt(WorkersKey),
+		TxsPerWorker:  v.GetUint64(TxsPerWorkerKey),
+		KeyDir:        v.GetString(KeyDirKey),
+		Timeout:       v.GetDuration(TimeoutKey),
+		BatchSize:     v.GetUint64(BatchSizeKey),
+		MetricsPort:   v.GetUint64(MetricsPortKey),
+		MetricsOutput: v.GetString(MetricsOutputKey),
 	}
 	if len(c.Endpoints) == 0 {
 		return c, ErrNoEndpoints
@@ -122,4 +125,5 @@ func addSimulatorFlags(fs *pflag.FlagSet) {
 	fs.String(LogLevelKey, "info", "Specify the log level to use in the simulator")
 	fs.Uint64(BatchSizeKey, 100, "Specify the batchsize for the worker to issue and confirm txs")
 	fs.Uint64(MetricsPortKey, 8082, "Specify the port to use for the metrics server")
+	fs.String(MetricsOutputKey, "", "Specify the file to write metrics in json format, or empy to write to stdout (defaults to stdout)")
 }
diff --git a/cmd/simulator/load/loader.go b/cmd/simulator/load/loader.go
index 80eaeeaacf..0dffe80bb7 100644
--- a/cmd/simulator/load/loader.go
+++ b/cmd/simulator/load/loader.go
@@ -150,7 +150,8 @@ func ExecuteLoader(ctx context.Context, config config.Config) error {
 	}()
 
 	m := metrics.NewDefaultMetrics()
-	ms := m.Serve(ctx, strconv.Itoa(int(config.MetricsPort)), MetricsEndpoint)
+	metricsCtx := context.Background()
+	ms := m.Serve(metricsCtx, strconv.Itoa(int(config.MetricsPort)), MetricsEndpoint)
 	defer ms.Shutdown()
 
 	// Construct the arguments for the load simulator
@@ -186,13 +187,13 @@ func ExecuteLoader(ctx context.Context, config config.Config) error {
 	// to fund gas for all of their transactions.
 	maxFeeCap := new(big.Int).Mul(big.NewInt(params.GWei), big.NewInt(config.MaxFeeCap))
 	minFundsPerAddr := new(big.Int).Mul(maxFeeCap, big.NewInt(int64(config.TxsPerWorker*params.TxGas)))
-
+	fundStart := time.Now()
 	log.Info("Distributing funds", "numTxsPerWorker", config.TxsPerWorker, "minFunds", minFundsPerAddr)
 	keys, err = DistributeFunds(ctx, clients[0], keys, config.Workers, minFundsPerAddr, m)
 	if err != nil {
 		return err
 	}
-	log.Info("Distributed funds successfully")
+	log.Info("Distributed funds successfully", "time", time.Since(fundStart))
 
 	pks := make([]*ecdsa.PrivateKey, 0, len(keys))
 	senders := make([]common.Address, 0, len(keys))
@@ -225,11 +226,12 @@ func ExecuteLoader(ctx context.Context, config config.Config) error {
 			Value:     common.Big0,
 		})
 	}
-
+	txSequenceStart := time.Now()
 	txSequences, err := txs.GenerateTxSequences(ctx, txGenerator, clients[0], pks, config.TxsPerWorker, false)
 	if err != nil {
 		return err
 	}
+	log.Info("Created transaction sequences successfully", "time", time.Since(txSequenceStart))
 
 	workers := make([]txs.Worker[*types.Transaction], 0, len(clients))
 	for i, client := range clients {
@@ -237,6 +239,9 @@ func ExecuteLoader(ctx context.Context, config config.Config) error {
 	}
 	loader := New(workers, txSequences, config.BatchSize, m)
 	err = loader.Execute(ctx)
-	ms.Print() // Print regardless of execution error
+	prerr := m.Print(config.MetricsOutput) // Print regardless of execution error
+	if prerr != nil {
+		log.Warn("Failed to print metrics", "error", prerr)
+	}
 	return err
 }
diff --git a/cmd/simulator/metrics/metrics.go b/cmd/simulator/metrics/metrics.go
index a711d47514..f9d44bbbcb 100644
--- a/cmd/simulator/metrics/metrics.go
+++ b/cmd/simulator/metrics/metrics.go
@@ -5,11 +5,11 @@ package metrics
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"net/http"
-	"strings"
+	"os"
 
 	"github.com/ethereum/go-ethereum/log"
 	"github.com/prometheus/client_golang/prometheus"
@@ -108,22 +108,32 @@ func (ms *MetricsServer) Shutdown() {
 	<-ms.stopCh
 }
 
-func (ms *MetricsServer) Print() {
-	// Get response from server
-	resp, err := http.Get(fmt.Sprintf("http://localhost:%s%s", ms.metricsPort, ms.metricsEndpoint))
+func (m *Metrics) Print(outputFile string) error {
+	metrics, err := m.reg.Gather()
 	if err != nil {
-		log.Error("cannot get response from metrics servers", "err", err)
-		return
+		return err
 	}
-	// Read response body
-	respBody, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		log.Error("cannot read response body", "err", err)
-		return
-	}
-	// Print out formatted individual metrics
-	parts := strings.Split(string(respBody), "\n")
-	for _, s := range parts {
-		fmt.Printf("       \t\t\t%s\n", s)
+
+	if outputFile == "" {
+		// Printout to stdout
+		fmt.Println("*** Metrics ***")
+		for _, mf := range metrics {
+			for _, m := range mf.GetMetric() {
+				fmt.Printf("Type: %s, Name: %s, Description: %s, Values: %s\n", mf.GetType().String(), mf.GetName(), mf.GetHelp(), m.String())
+			}
+		}
+		fmt.Println("***************")
+	} else {
+		jsonFile, err := os.Create(outputFile)
+		if err != nil {
+			return err
+		}
+		defer jsonFile.Close()
+
+		if err := json.NewEncoder(jsonFile).Encode(metrics); err != nil {
+			return err
+		}
 	}
+
+	return nil
 }