github.com/rudderlabs/rudder-go-kit@v0.30.0/stats/memstats/stats.go (about)

     1  package memstats
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/samber/lo"
    14  	"github.com/spf13/cast"
    15  	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
    16  	sdktrace "go.opentelemetry.io/otel/sdk/trace"
    17  	"go.opentelemetry.io/otel/trace"
    18  	"go.opentelemetry.io/otel/trace/noop"
    19  
    20  	"github.com/rudderlabs/rudder-go-kit/stats"
    21  	"github.com/rudderlabs/rudder-go-kit/stats/testhelper/tracemodel"
    22  )
    23  
    24  var (
    25  	_ stats.Stats       = (*Store)(nil)
    26  	_ stats.Measurement = (*Measurement)(nil)
    27  )
    28  
    29  type Store struct {
    30  	mu    sync.Mutex
    31  	byKey map[string]*Measurement
    32  	now   func() time.Time
    33  
    34  	withTracing       bool
    35  	tracingBuffer     *bytes.Buffer
    36  	tracingTimestamps bool
    37  	tracerProvider    trace.TracerProvider
    38  }
    39  
    40  type Measurement struct {
    41  	mu  sync.Mutex
    42  	now func() time.Time
    43  
    44  	tags  stats.Tags
    45  	name  string
    46  	mType string
    47  
    48  	sum       float64
    49  	values    []float64
    50  	durations []time.Duration
    51  }
    52  
    53  // Metric captures the name, tags and value(s) depending on type.
    54  //
    55  //	For Count and Gauge, Value is used.
    56  //	For Histogram, Values is used.
    57  //	For Timer, Durations is used.
    58  type Metric struct {
    59  	Name      string
    60  	Tags      stats.Tags
    61  	Value     float64         // Count, Gauge
    62  	Values    []float64       // Histogram
    63  	Durations []time.Duration // Timer
    64  }
    65  
    66  func (m *Measurement) LastValue() float64 {
    67  	m.mu.Lock()
    68  	defer m.mu.Unlock()
    69  
    70  	if len(m.values) == 0 {
    71  		return 0
    72  	}
    73  
    74  	return m.values[len(m.values)-1]
    75  }
    76  
    77  func (m *Measurement) Values() []float64 {
    78  	m.mu.Lock()
    79  	defer m.mu.Unlock()
    80  
    81  	s := make([]float64, len(m.values))
    82  	copy(s, m.values)
    83  
    84  	return s
    85  }
    86  
    87  func (m *Measurement) LastDuration() time.Duration {
    88  	m.mu.Lock()
    89  	defer m.mu.Unlock()
    90  
    91  	if len(m.durations) == 0 {
    92  		return 0
    93  	}
    94  
    95  	return m.durations[len(m.durations)-1]
    96  }
    97  
    98  func (m *Measurement) Durations() []time.Duration {
    99  	m.mu.Lock()
   100  	defer m.mu.Unlock()
   101  
   102  	s := make([]time.Duration, len(m.durations))
   103  	copy(s, m.durations)
   104  
   105  	return s
   106  }
   107  
   108  // Count implements stats.Measurement
   109  func (m *Measurement) Count(n int) {
   110  	m.mu.Lock()
   111  	defer m.mu.Unlock()
   112  
   113  	if m.mType != stats.CountType {
   114  		panic("operation Count not supported for measurement type:" + m.mType)
   115  	}
   116  
   117  	m.sum += float64(n)
   118  	m.values = append(m.values, m.sum)
   119  }
   120  
   121  // Increment implements stats.Measurement
   122  func (m *Measurement) Increment() {
   123  	if m.mType != stats.CountType {
   124  		panic("operation Increment not supported for measurement type:" + m.mType)
   125  	}
   126  
   127  	m.Count(1)
   128  }
   129  
   130  // Gauge implements stats.Measurement
   131  func (m *Measurement) Gauge(value interface{}) {
   132  	if m.mType != stats.GaugeType {
   133  		panic("operation Gauge not supported for measurement type:" + m.mType)
   134  	}
   135  
   136  	m.mu.Lock()
   137  	defer m.mu.Unlock()
   138  
   139  	m.values = append(m.values, cast.ToFloat64(value))
   140  }
   141  
   142  // Observe implements stats.Measurement
   143  func (m *Measurement) Observe(value float64) {
   144  	if m.mType != stats.HistogramType {
   145  		panic("operation Observe not supported for measurement type:" + m.mType)
   146  	}
   147  
   148  	m.mu.Lock()
   149  	defer m.mu.Unlock()
   150  
   151  	m.values = append(m.values, value)
   152  }
   153  
   154  // Since implements stats.Measurement
   155  func (m *Measurement) Since(start time.Time) {
   156  	if m.mType != stats.TimerType {
   157  		panic("operation Since not supported for measurement type:" + m.mType)
   158  	}
   159  
   160  	m.SendTiming(m.now().Sub(start))
   161  }
   162  
   163  // SendTiming implements stats.Measurement
   164  func (m *Measurement) SendTiming(duration time.Duration) {
   165  	if m.mType != stats.TimerType {
   166  		panic("operation SendTiming not supported for measurement type:" + m.mType)
   167  	}
   168  
   169  	m.mu.Lock()
   170  	defer m.mu.Unlock()
   171  
   172  	m.durations = append(m.durations, duration)
   173  }
   174  
   175  // RecordDuration implements stats.Measurement
   176  func (m *Measurement) RecordDuration() func() {
   177  	if m.mType != stats.TimerType {
   178  		panic("operation RecordDuration not supported for measurement type:" + m.mType)
   179  	}
   180  
   181  	start := m.now()
   182  	return func() {
   183  		m.Since(start)
   184  	}
   185  }
   186  
   187  type Opts func(*Store)
   188  
   189  func WithNow(nowFn func() time.Time) Opts {
   190  	return func(s *Store) {
   191  		s.now = nowFn
   192  	}
   193  }
   194  
   195  func WithTracing() Opts {
   196  	return func(s *Store) {
   197  		s.withTracing = true
   198  	}
   199  }
   200  
   201  func WithTracingTimestamps() Opts {
   202  	return func(s *Store) {
   203  		s.tracingTimestamps = true
   204  	}
   205  }
   206  
   207  func New(opts ...Opts) (*Store, error) {
   208  	s := &Store{
   209  		byKey: make(map[string]*Measurement),
   210  		now:   time.Now,
   211  	}
   212  	for _, opt := range opts {
   213  		opt(s)
   214  	}
   215  	if !s.withTracing {
   216  		s.tracerProvider = noop.NewTracerProvider()
   217  		return s, nil
   218  	}
   219  
   220  	s.tracingBuffer = &bytes.Buffer{}
   221  
   222  	tracingOpts := []stdouttrace.Option{
   223  		stdouttrace.WithWriter(s.tracingBuffer),
   224  		stdouttrace.WithPrettyPrint(),
   225  	}
   226  	if !s.tracingTimestamps {
   227  		tracingOpts = append(tracingOpts, stdouttrace.WithoutTimestamps())
   228  	}
   229  
   230  	traceExporter, err := stdouttrace.New(tracingOpts...)
   231  	if err != nil {
   232  		return nil, fmt.Errorf("cannot create trace exporter: %w", err)
   233  	}
   234  	s.tracerProvider = sdktrace.NewTracerProvider(
   235  		sdktrace.WithSampler(sdktrace.AlwaysSample()),
   236  		sdktrace.WithSyncer(traceExporter),
   237  	)
   238  
   239  	return s, nil
   240  }
   241  
   242  func (ms *Store) NewTracer(name string) stats.Tracer {
   243  	return stats.NewTracerFromOpenTelemetry(ms.tracerProvider.Tracer(name))
   244  }
   245  
   246  // NewStat implements stats.Stats
   247  func (ms *Store) NewStat(name, statType string) (m stats.Measurement) {
   248  	return ms.NewTaggedStat(name, statType, nil)
   249  }
   250  
   251  // NewTaggedStat implements stats.Stats
   252  func (ms *Store) NewTaggedStat(name, statType string, tags stats.Tags) stats.Measurement {
   253  	return ms.NewSampledTaggedStat(name, statType, tags)
   254  }
   255  
   256  // NewSampledTaggedStat implements stats.Stats
   257  func (ms *Store) NewSampledTaggedStat(name, statType string, tags stats.Tags) stats.Measurement {
   258  	ms.mu.Lock()
   259  	defer ms.mu.Unlock()
   260  
   261  	m := &Measurement{
   262  		name:  name,
   263  		tags:  tags,
   264  		mType: statType,
   265  
   266  		now: ms.now,
   267  	}
   268  
   269  	ms.byKey[ms.getKey(name, tags)] = m
   270  	return m
   271  }
   272  
   273  // Get the stored measurement with the name and tags.
   274  // If no measurement is found, nil is returned.
   275  func (ms *Store) Get(name string, tags stats.Tags) *Measurement {
   276  	ms.mu.Lock()
   277  	defer ms.mu.Unlock()
   278  
   279  	return ms.byKey[ms.getKey(name, tags)]
   280  }
   281  
   282  func (ms *Store) Spans() ([]tracemodel.Span, error) {
   283  	if ms.tracingBuffer.Len() == 0 {
   284  		return nil, nil
   285  	}
   286  
   287  	// adding missing curly brackets and converting to a valid JSON array
   288  	jsonData := "[" + ms.tracingBuffer.String() + "]"
   289  	jsonData = strings.Replace(jsonData, "}\n{", "},{", -1)
   290  
   291  	var spans []tracemodel.Span
   292  	err := json.Unmarshal([]byte(jsonData), &spans)
   293  
   294  	return spans, err
   295  }
   296  
   297  // GetAll returns the metric for all name/tags register in the store.
   298  func (ms *Store) GetAll() []Metric {
   299  	return ms.getAllByName("")
   300  }
   301  
   302  // GetByName returns the metric for each tag variation with the given name.
   303  func (ms *Store) GetByName(name string) []Metric {
   304  	if name == "" {
   305  		panic("name cannot be empty")
   306  	}
   307  	return ms.getAllByName(name)
   308  }
   309  
   310  func (ms *Store) getAllByName(name string) []Metric {
   311  	ms.mu.Lock()
   312  	defer ms.mu.Unlock()
   313  
   314  	keys := lo.Filter(lo.Keys(ms.byKey), func(k string, index int) bool {
   315  		return name == "" || ms.byKey[k].name == name
   316  	})
   317  	sort.SliceStable(keys, func(i, j int) bool {
   318  		return keys[i] < keys[j]
   319  	})
   320  	return lo.Map(keys, func(key string, index int) Metric {
   321  		m := ms.byKey[key]
   322  		switch m.mType {
   323  		case stats.CountType, stats.GaugeType:
   324  			return Metric{
   325  				Name:  m.name,
   326  				Tags:  m.tags,
   327  				Value: m.LastValue(),
   328  			}
   329  		case stats.HistogramType:
   330  			return Metric{
   331  				Name:   m.name,
   332  				Tags:   m.tags,
   333  				Values: m.Values(),
   334  			}
   335  		case stats.TimerType:
   336  			return Metric{
   337  				Name:      m.name,
   338  				Tags:      m.tags,
   339  				Durations: m.Durations(),
   340  			}
   341  		default:
   342  			panic("unknown measurement type:" + m.mType)
   343  		}
   344  	})
   345  }
   346  
   347  // Start implements stats.Stats
   348  func (*Store) Start(_ context.Context, _ stats.GoRoutineFactory) error { return nil }
   349  
   350  // Stop implements stats.Stats
   351  func (*Store) Stop() {}
   352  
   353  // getKey maps name and tags, to a store lookup key.
   354  func (*Store) getKey(name string, tags stats.Tags) string {
   355  	return name + tags.String()
   356  }