github.com/unicornultrafoundation/go-u2u@v1.0.0-rc1.0.20240205080301-e74a83d3fadc/integration/metric.go (about)

     1  package integration
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/unicornultrafoundation/go-helios/u2udb"
    11  	"github.com/unicornultrafoundation/go-u2u/log"
    12  	"github.com/unicornultrafoundation/go-u2u/metrics"
    13  )
    14  
    15  const (
    16  	// metricsGatheringInterval specifies the interval to retrieve leveldb database
    17  	// compaction, io and pause stats to report to the user.
    18  	metricsGatheringInterval = 3 * time.Second
    19  )
    20  
    21  type DBProducerWithMetrics struct {
    22  	u2udb.IterableDBProducer
    23  }
    24  
    25  type StoreWithMetrics struct {
    26  	u2udb.Store
    27  
    28  	diskSizeGauge  metrics.Gauge // Gauge for tracking the size of all the levels in the database
    29  	diskReadMeter  metrics.Meter // Meter for measuring the effective amount of data read
    30  	diskWriteMeter metrics.Meter // Meter for measuring the effective amount of data written
    31  
    32  	quitLock sync.Mutex      // Mutex protecting the quit channel access
    33  	quitChan chan chan error // Quit channel to stop the metrics collection before closing the database
    34  
    35  	log log.Logger // Contextual logger tracking the database path
    36  }
    37  
    38  func WrapDatabaseWithMetrics(db u2udb.IterableDBProducer) u2udb.IterableDBProducer {
    39  	wrapper := &DBProducerWithMetrics{db}
    40  	return wrapper
    41  }
    42  
    43  func WrapStoreWithMetrics(ds u2udb.Store) *StoreWithMetrics {
    44  	wrapper := &StoreWithMetrics{
    45  		Store:    ds,
    46  		quitChan: make(chan chan error),
    47  	}
    48  	return wrapper
    49  }
    50  
    51  func (ds *StoreWithMetrics) Close() error {
    52  	ds.quitLock.Lock()
    53  	defer ds.quitLock.Unlock()
    54  
    55  	if ds.quitChan != nil {
    56  		errc := make(chan error)
    57  		ds.quitChan <- errc
    58  		if err := <-errc; err != nil {
    59  			ds.log.Error("Metrics collection failed", "err", err)
    60  		}
    61  		ds.quitChan = nil
    62  	}
    63  	return ds.Store.Close()
    64  }
    65  
    66  func (ds *StoreWithMetrics) meter(refresh time.Duration) {
    67  	// Create storage for iostats.
    68  	var iostats [2]float64
    69  
    70  	var (
    71  		errc chan error
    72  		merr error
    73  	)
    74  
    75  	timer := time.NewTimer(refresh)
    76  	defer timer.Stop()
    77  	// Iterate ad infinitum and collect the stats
    78  	for i := 1; errc == nil && merr == nil; i++ {
    79  		// Retrieve the database size
    80  		diskSize, err := ds.Stat("disk.size")
    81  		if err != nil {
    82  			ds.log.Error("Failed to read database stats", "err", err)
    83  			merr = err
    84  			continue
    85  		}
    86  		var nDiskSize int64
    87  		if n, err := fmt.Sscanf(diskSize, "%d", &nDiskSize); n != 1 || err != nil {
    88  			ds.log.Error("Bad syntax of disk size entry", "size", diskSize)
    89  			merr = err
    90  			continue
    91  		}
    92  		// Update all the disk size meters
    93  		if ds.diskSizeGauge != nil {
    94  			ds.diskSizeGauge.Update(nDiskSize)
    95  		}
    96  
    97  		// Retrieve the database iostats.
    98  		ioStats, err := ds.Stat("iostats")
    99  		if err != nil {
   100  			ds.log.Error("Failed to read database iostats", "err", err)
   101  			merr = err
   102  			continue
   103  		}
   104  		var nRead, nWrite float64
   105  		parts := strings.Split(ioStats, " ")
   106  		if len(parts) < 2 {
   107  			ds.log.Error("Bad syntax of ioStats", "ioStats", ioStats)
   108  			merr = fmt.Errorf("bad syntax of ioStats %s", ioStats)
   109  			continue
   110  		}
   111  		if n, err := fmt.Sscanf(parts[0], "Read(MB):%f", &nRead); n != 1 || err != nil {
   112  			ds.log.Error("Bad syntax of read entry", "entry", parts[0])
   113  			merr = err
   114  			continue
   115  		}
   116  		if n, err := fmt.Sscanf(parts[1], "Write(MB):%f", &nWrite); n != 1 || err != nil {
   117  			log.Error("Bad syntax of write entry", "entry", parts[1])
   118  			merr = err
   119  			continue
   120  		}
   121  		if ds.diskReadMeter != nil {
   122  			ds.diskReadMeter.Mark(int64((nRead - iostats[0]) * 1024 * 1024))
   123  		}
   124  		if ds.diskWriteMeter != nil {
   125  			ds.diskWriteMeter.Mark(int64((nWrite - iostats[1]) * 1024 * 1024))
   126  		}
   127  		iostats[0], iostats[1] = nRead, nWrite
   128  
   129  		// Sleep a bit, then repeat the stats collection
   130  		select {
   131  		case errc = <-ds.quitChan:
   132  			// Quit requesting, stop hammering the database
   133  		case <-timer.C:
   134  			timer.Reset(refresh)
   135  			// Timeout, gather a new set of stats
   136  		}
   137  	}
   138  	if errc == nil {
   139  		errc = <-ds.quitChan
   140  	}
   141  	errc <- merr
   142  }
   143  
   144  var tmpDbNameMask = regexp.MustCompile("^([A-z]+)(-[0-9]+)$")
   145  
   146  func genericNameOfTmpDB(name string) string {
   147  	match := tmpDbNameMask.FindStringSubmatch(name)
   148  	if len(match) == 3 {
   149  		return match[1] + "-tmp"
   150  	} else {
   151  		return name
   152  	}
   153  }
   154  
   155  func (db *DBProducerWithMetrics) OpenDB(name string) (u2udb.Store, error) {
   156  	ds, err := db.IterableDBProducer.OpenDB(name)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	dm := WrapStoreWithMetrics(ds)
   161  
   162  	name = genericNameOfTmpDB(name)
   163  
   164  	dm.log = log.New("database", name)
   165  
   166  	metric := "u2u/chaindata/" + strings.ReplaceAll(name, "-", "_")
   167  	dm.diskReadMeter = metrics.GetOrRegisterMeter(metric+"/disk/read", nil)
   168  	dm.diskWriteMeter = metrics.GetOrRegisterMeter(metric+"/disk/write", nil)
   169  	// reset size metric as far as previous db will be dropped soon
   170  	metrics.Unregister(metric + "/disk/size")
   171  	dm.diskSizeGauge = metrics.NewRegisteredGauge(metric+"/disk/size", nil)
   172  
   173  	// Start up the metrics gathering and return
   174  	go dm.meter(metricsGatheringInterval)
   175  	return dm, nil
   176  }