stats/stats.go (144 lines of code) (raw):

// Copyright (c) Facebook, Inc. and its affiliates. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stats import ( "encoding/csv" "flag" "fmt" "io" "os" "sync" "time" "github.com/facebookincubator/flog" ) // Stats encapsulates functionallity of incrementing counters and incrementing values type Stats struct { OutputFile string LogToStderr bool counters map[string]int64 countersLock sync.RWMutex values map[string]float64 valuesLock sync.RWMutex } // New creates new Stats object func New() *Stats { s := Stats{} s.Clear() // this will also initialize the maps return &s } // AreLogged returns whether this stats object is getting logged // can be used to determine whether to keep adding stuff to it or not // if it's not being logged, one shouldn't even increment counters func (s *Stats) AreLogged() bool { return s.LogToStderr || s.OutputFile != "" } // AddFlags adds configuration flags for a stats object func (s *Stats) AddFlags() { flag.StringVar(&s.OutputFile, "output_stats", "", "output stats to this file") flag.BoolVar(&s.LogToStderr, "log_stats", false, "log stats to stderr") } // IncrementCounter increments the counter associated with the key by 1 func (s *Stats) IncrementCounter(key string) { s.IncrementCounterBy(key, 1) } // IncrementCounterBy increments the counter associated with the key by the given value func (s *Stats) IncrementCounterBy(key string, value int64) { s.countersLock.Lock() s.counters[key] += value s.countersLock.Unlock() } // AddToValue adds to the value associated with the key func (s *Stats) AddToValue(key string, value float64) { s.valuesLock.Lock() s.values[key] += value s.valuesLock.Unlock() } // TrackTime will track how much time was elapsed from start time, and add that // value (in given unit) to the value associated with the key // should be used like this // func Something() { // defer stats.TrackTime(key, time.Now(), time.Second) // .. do some operation // } // func (s *Stats) TrackTime(key string, start time.Time, unit time.Duration) { elapsed := time.Since(start) value := float64(elapsed / unit) s.AddToValue(key, value) } // GetCounter returns the count associated with the key func (s *Stats) GetCounter(key string) int64 { s.countersLock.RLock() defer s.countersLock.RUnlock() return s.counters[key] } // GetValue returns the value associated with the key func (s *Stats) GetValue(key string) float64 { s.valuesLock.RLock() defer s.valuesLock.RUnlock() return s.values[key] } // Clear will empty out the stats, all counters are set to 0, all values set to 0 func (s *Stats) Clear() { s.counters = make(map[string]int64) s.values = make(map[string]float64) } // Write will write all stats to stderr and/or a file. configured through stats.OutputFile and stats.sLogToStderr func (s *Stats) Write() error { if s.LogToStderr { if err := s.write(os.Stderr); err != nil { return fmt.Errorf("failed to write stats to stderr: %v", err) } } if s.OutputFile != "" { f, err := os.OpenFile(s.OutputFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) if err != nil { return fmt.Errorf("failed to open stats file: %v", err) } defer f.Close() if err = s.write(f); err != nil { return fmt.Errorf("failed to write stats to file: %v", err) } } return nil } // WriteAndLogError is just a wrapper around Write which also logs the error to stderr if it occurs func (s *Stats) WriteAndLogError() { if err := s.Write(); err != nil { flog.Errorf("failed to write stats: %v", err) } } // global stats var ( // global is the global stats object. It can be used when you only need one stats object between multiple modules in a program global = New() ) // AreLogged returns whether global stats are getting logged func AreLogged() bool { return global.AreLogged() } // AddFlags adds configuration flags for the global stats object func AddFlags() { global.AddFlags() } // IncrementCounter increments the global counter associated with the key by 1 func IncrementCounter(key string) { global.IncrementCounter(key) } // IncrementCounterBy increments the global counter associated with the key by the given value func IncrementCounterBy(key string, value int64) { global.IncrementCounterBy(key, value) } // AddToValue adds to the value associated with the global key func AddToValue(key string, value float64) { global.AddToValue(key, value) } // TrackTime will track how much time was elapsed from start time, and add that // value (in given unit) to the global value associated with the key // should be used like this // func Something() { // defer stats.TrackTime(key, time.Now(), time.Second) // .. do some operation // } // func TrackTime(key string, start time.Time, unit time.Duration) { global.TrackTime(key, start, unit) } // GetCounter returns the global count associated with the key func GetCounter(key string) int64 { return global.GetCounter(key) } // GetValue returns the global value associated with the key func GetValue(key string) float64 { return global.GetValue(key) } // Clear will empty out the global stats, all counters are set to 0, all values set to 0 func Clear() { global.Clear() } // Write will write all global stats to stderr and/or a file. configured through stats.OutputFile and stats.sLogToStderr func Write() error { return global.Write() } // WriteAndLogError is just a wrapper around Write which also logs the error to stderr if it occurs func WriteAndLogError() { global.WriteAndLogError() } // internal func (s *Stats) write(w io.Writer) (err error) { cw := csv.NewWriter(w) defer func() { cw.Flush() if err == nil { // don't override the existing error with possible flush error err = cw.Error() } }() for key, counter := range s.counters { record := []string{key, fmt.Sprintf("%d", counter)} if err = cw.Write(record); err != nil { return err } } for key, value := range s.values { record := []string{key, fmt.Sprintf("%.2f", value)} if err = cw.Write(record); err != nil { return err } } return nil }