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 }