github.com/GoogleCloudPlatform/testgrid@v0.0.174/util/metrics/logmetrics/log.go (about)

     1  /*
     2  Copyright 2021 The TestGrid Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package logmetrics
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strconv"
    23  	"sync"
    24  	"time"
    25  
    26  	"bitbucket.org/creachadair/stringset"
    27  	"github.com/GoogleCloudPlatform/testgrid/util/metrics"
    28  	"github.com/sirupsen/logrus"
    29  )
    30  
    31  // Valuer extends a metric to include a report on its values.
    32  type Valuer interface {
    33  	metrics.Metric
    34  	Values() map[string]map[string]interface{}
    35  }
    36  
    37  // Reporter is a collection of metric values to report.
    38  type Reporter []Valuer
    39  
    40  // ReportNow reports all metrics once, immediately
    41  func (r *Reporter) ReportNow(log logrus.FieldLogger) {
    42  	if log == nil {
    43  		log = logrus.New()
    44  	}
    45  	for _, metric := range *r {
    46  		log := log.WithField("metric", metric.Name())
    47  		for field, values := range metric.Values() {
    48  			log := log.WithField("field", field)
    49  			for value, measurement := range values {
    50  				log = log.WithField(value, measurement)
    51  			}
    52  			log.Info("Current status")
    53  		}
    54  	}
    55  }
    56  
    57  // Report the status of its metrics every freq until the context expires.
    58  func (r *Reporter) Report(ctx context.Context, log logrus.FieldLogger, freq time.Duration) error {
    59  	if log == nil {
    60  		log = logrus.New()
    61  	}
    62  	ticker := time.NewTicker(freq)
    63  	defer ticker.Stop()
    64  	names := stringset.NewSize(len(*r))
    65  	metricMap := make(map[string]int, len(*r))
    66  	for i, m := range *r {
    67  		name := m.Name()
    68  		names.Add(name)
    69  		metricMap[name] = i
    70  	}
    71  	for {
    72  		select {
    73  		case <-ticker.C:
    74  		case <-ctx.Done():
    75  			return ctx.Err()
    76  		}
    77  
    78  		for _, name := range names.Elements() {
    79  			i := metricMap[name]
    80  			metric := (*r)[i]
    81  			log := log.WithField("metric", metric.Name())
    82  			for field, values := range metric.Values() {
    83  				log := log.WithField("field", field)
    84  				for value, measurement := range values {
    85  					log = log.WithField(value, measurement)
    86  				}
    87  				log.Info("Current status")
    88  			}
    89  		}
    90  	}
    91  }
    92  
    93  // Int64 configures a new Int64 metric to report.
    94  func (r *Reporter) Int64(name, desc string, log logrus.FieldLogger, fields ...string) metrics.Int64 {
    95  	current := make([]map[string][]int64, len(fields))
    96  	for i := range fields {
    97  		current[i] = make(map[string][]int64, 1)
    98  	}
    99  
   100  	out := &logInt64{
   101  		name:    name,
   102  		desc:    desc,
   103  		log:     log,
   104  		fields:  fields,
   105  		current: current,
   106  	}
   107  	*r = append(*r, out)
   108  	return out
   109  }
   110  
   111  // Counter configures a new Counter metric to report
   112  func (r *Reporter) Counter(name, desc string, log logrus.FieldLogger, fields ...string) metrics.Counter {
   113  	current := make([]map[string]int64, len(fields))
   114  	previous := make([]map[string]int64, len(fields))
   115  	for i := range fields {
   116  		current[i] = make(map[string]int64, 1)
   117  		previous[i] = make(map[string]int64, 1)
   118  	}
   119  
   120  	out := &logCounter{
   121  		name:     name,
   122  		desc:     desc,
   123  		log:      log,
   124  		fields:   fields,
   125  		current:  current,
   126  		previous: previous,
   127  		last:     time.Now(),
   128  	}
   129  	*r = append(*r, out)
   130  	return out
   131  }
   132  
   133  type logInt64 struct {
   134  	name    string
   135  	desc    string
   136  	fields  []string
   137  	log     logrus.FieldLogger
   138  	current []map[string][]int64
   139  	lock    sync.Mutex
   140  }
   141  
   142  // Name returns the metric's name.
   143  func (m *logInt64) Name() string {
   144  	return m.name
   145  }
   146  
   147  // Set the metric's value to the given number.
   148  func (m *logInt64) Set(n int64, fields ...string) {
   149  	m.lock.Lock()
   150  	defer m.lock.Unlock()
   151  	if len(fields) != len(m.fields) {
   152  		m.log.WithFields(logrus.Fields{
   153  			"want": m.fields,
   154  			"got":  fields,
   155  		}).Fatal("Wrong number of fields")
   156  	}
   157  	for i, fieldValue := range fields {
   158  		m.current[i][fieldValue] = append(m.current[i][fieldValue], n)
   159  	}
   160  }
   161  
   162  func (m *logInt64) Values() map[string]map[string]interface{} {
   163  	m.lock.Lock()
   164  	defer m.lock.Unlock()
   165  
   166  	trend := make(map[string]map[string]interface{}, len(m.current))
   167  	for i, field := range m.fields {
   168  		current := m.current[i]
   169  		if len(current) == 0 {
   170  			continue
   171  		}
   172  		trend[field] = make(map[string]interface{}, len(current))
   173  		for fieldValue, measurements := range current {
   174  
   175  			trend[field][fieldValue] = mean{measurements}
   176  			delete(current, fieldValue)
   177  		}
   178  
   179  	}
   180  	return trend
   181  }
   182  
   183  type mean struct {
   184  	values []int64
   185  }
   186  
   187  func (m mean) String() string {
   188  	tot := len(m.values)
   189  	switch tot {
   190  	case 0:
   191  		return "0 values"
   192  	case 1:
   193  		return strconv.FormatInt(m.values[0], 10)
   194  	}
   195  	var val float64
   196  	n := float64(tot)
   197  	for _, v := range m.values {
   198  		val += (float64(v) / n)
   199  	}
   200  	return fmt.Sprintf("%s average (%d values)", strconv.FormatFloat(val, 'g', 3, 64), tot)
   201  }
   202  
   203  type logCounter struct {
   204  	name     string
   205  	desc     string
   206  	fields   []string
   207  	log      logrus.FieldLogger
   208  	current  []map[string]int64
   209  	previous []map[string]int64
   210  	last     time.Time
   211  	lock     sync.Mutex
   212  }
   213  
   214  // Name returns the metric's name.
   215  func (m *logCounter) Name() string {
   216  	return m.name
   217  }
   218  
   219  // Add the given number to the counter.
   220  func (m *logCounter) Add(n int64, fields ...string) {
   221  	m.lock.Lock()
   222  	defer m.lock.Unlock()
   223  	if len(fields) != len(m.fields) {
   224  		m.log.WithFields(logrus.Fields{
   225  			"want": m.fields,
   226  			"got":  fields,
   227  		}).Fatal("Wrong number of fields")
   228  	}
   229  	if n < 0 {
   230  		m.log.WithField("value", n).Fatal("Negative value are invalid")
   231  	}
   232  	for i, fieldValue := range fields {
   233  		m.current[i][fieldValue] += n
   234  	}
   235  }
   236  
   237  func (m *logCounter) Values() map[string]map[string]interface{} {
   238  	m.lock.Lock()
   239  	defer m.lock.Unlock()
   240  
   241  	dur := time.Since(m.last)
   242  	m.last = time.Now()
   243  	rate := make(map[string]map[string]interface{}, len(m.current))
   244  	for i, field := range m.fields {
   245  		current := m.current[i]
   246  		previous := m.previous[i]
   247  		rate[field] = make(map[string]interface{}, len(current))
   248  		for fieldValue, now := range current {
   249  			delta := now - previous[fieldValue]
   250  			rate[field][fieldValue] = gauge{now, delta, dur}
   251  			previous[fieldValue] = now
   252  		}
   253  	}
   254  	return rate
   255  }
   256  
   257  type gauge struct {
   258  	total int64
   259  	delta int64
   260  	dur   time.Duration
   261  }
   262  
   263  func (g gauge) qps() string {
   264  	s := g.dur.Seconds()
   265  	if s == 0 {
   266  		return "0 per second"
   267  	}
   268  	qps := float64(g.delta) / s
   269  	if qps == 0 {
   270  		return "0 per second"
   271  	}
   272  	if qps > 0.5 {
   273  		return fmt.Sprintf("%.2f per second", qps)
   274  	}
   275  	seconds := time.Duration(1 / qps * float64(time.Second))
   276  	seconds = seconds.Round(time.Millisecond)
   277  	return fmt.Sprintf("once per %s", seconds)
   278  }
   279  
   280  func (g gauge) String() string {
   281  	return fmt.Sprintf("%s (%d total)", g.qps(), g.total)
   282  }