// Copyright 2017 The OPA Authors. All rights reserved. // Use of this source code is governed by an Apache2 // license that can be found in the LICENSE file. // Package metrics contains helpers for performance metric management inside the policy engine. package metrics import ( "encoding/json" "fmt" "sort" "strings" "sync" "sync/atomic" "time" go_metrics "github.com/rcrowley/go-metrics" ) // Well-known metric names. const ( ServerHandler = "server_handler" ServerQueryCacheHit = "server_query_cache_hit" RegoQueryCompile = "rego_query_compile" RegoQueryEval = "rego_query_eval" RegoQueryParse = "rego_query_parse" RegoModuleParse = "rego_module_parse" RegoDataParse = "rego_data_parse" RegoModuleCompile = "rego_module_compile" RegoPartialEval = "rego_partial_eval" RegoInputParse = "rego_input_parse" RegoLoadFiles = "rego_load_files" RegoLoadBundles = "rego_load_bundles" ) // Info contains attributes describing the underlying metrics provider. type Info struct { Name string `json:"name"` // name is a unique human-readable identifier for the provider. } // Metrics defines the interface for a collection of performance metrics in the // policy engine. type Metrics interface { Info() Info Timer(name string) Timer Histogram(name string) Histogram Counter(name string) Counter All() map[string]interface{} Clear() json.Marshaler } type metrics struct { mtx sync.Mutex timers map[string]Timer histograms map[string]Histogram counters map[string]Counter } // New returns a new Metrics object. func New() Metrics { m := &metrics{} m.Clear() return m } type metric struct { Key string Value interface{} } func (m *metrics) Info() Info { return Info{ Name: "", } } func (m *metrics) String() string { all := m.All() sorted := make([]metric, 0, len(all)) for key, value := range all { sorted = append(sorted, metric{ Key: key, Value: value, }) } sort.Slice(sorted, func(i, j int) bool { return sorted[i].Key < sorted[j].Key }) buf := make([]string, len(sorted)) for i := range sorted { buf[i] = fmt.Sprintf("%v:%v", sorted[i].Key, sorted[i].Value) } return strings.Join(buf, " ") } func (m *metrics) MarshalJSON() ([]byte, error) { return json.Marshal(m.All()) } func (m *metrics) Timer(name string) Timer { m.mtx.Lock() defer m.mtx.Unlock() t, ok := m.timers[name] if !ok { t = &timer{} m.timers[name] = t } return t } func (m *metrics) Histogram(name string) Histogram { m.mtx.Lock() defer m.mtx.Unlock() h, ok := m.histograms[name] if !ok { h = newHistogram() m.histograms[name] = h } return h } func (m *metrics) Counter(name string) Counter { m.mtx.Lock() defer m.mtx.Unlock() c, ok := m.counters[name] if !ok { zero := counter{} c = &zero m.counters[name] = c } return c } func (m *metrics) All() map[string]interface{} { m.mtx.Lock() defer m.mtx.Unlock() result := map[string]interface{}{} for name, timer := range m.timers { result[m.formatKey(name, timer)] = timer.Value() } for name, hist := range m.histograms { result[m.formatKey(name, hist)] = hist.Value() } for name, cntr := range m.counters { result[m.formatKey(name, cntr)] = cntr.Value() } return result } func (m *metrics) Clear() { m.mtx.Lock() defer m.mtx.Unlock() m.timers = map[string]Timer{} m.histograms = map[string]Histogram{} m.counters = map[string]Counter{} } func (m *metrics) formatKey(name string, metrics interface{}) string { switch metrics.(type) { case Timer: return "timer_" + name + "_ns" case Histogram: return "histogram_" + name case Counter: return "counter_" + name default: return name } } // Timer defines the interface for a restartable timer that accumulates elapsed // time. type Timer interface { Value() interface{} Int64() int64 Start() Stop() int64 } type timer struct { mtx sync.Mutex start time.Time value int64 } func (t *timer) Start() { t.mtx.Lock() defer t.mtx.Unlock() t.start = time.Now() } func (t *timer) Stop() int64 { t.mtx.Lock() defer t.mtx.Unlock() delta := time.Now().Sub(t.start).Nanoseconds() t.value += delta return delta } func (t *timer) Value() interface{} { return t.Int64() } func (t *timer) Int64() int64 { t.mtx.Lock() defer t.mtx.Unlock() return t.value } // Histogram defines the interface for a histogram with hardcoded percentiles. type Histogram interface { Value() interface{} Update(int64) } type histogram struct { hist go_metrics.Histogram // is thread-safe because of the underlying ExpDecaySample } func newHistogram() Histogram { // NOTE(tsandall): the reservoir size and alpha factor are taken from // https://github.com/rcrowley/go-metrics. They may need to be tweaked in // the future. sample := go_metrics.NewExpDecaySample(1028, 0.015) hist := go_metrics.NewHistogram(sample) return &histogram{hist} } func (h *histogram) Update(v int64) { h.hist.Update(v) } func (h *histogram) Value() interface{} { values := map[string]interface{}{} snap := h.hist.Snapshot() percentiles := snap.Percentiles([]float64{ 0.5, 0.75, 0.9, 0.95, 0.99, 0.999, 0.9999, }) values["count"] = snap.Count() values["min"] = snap.Min() values["max"] = snap.Max() values["mean"] = snap.Mean() values["stddev"] = snap.StdDev() values["median"] = percentiles[0] values["75%"] = percentiles[1] values["90%"] = percentiles[2] values["95%"] = percentiles[3] values["99%"] = percentiles[4] values["99.9%"] = percentiles[5] values["99.99%"] = percentiles[6] return values } // Counter defines the interface for a monotonic increasing counter. type Counter interface { Value() interface{} Incr() } type counter struct { c uint64 } func (c *counter) Incr() { atomic.AddUint64(&c.c, 1) } func (c *counter) Value() interface{} { return atomic.LoadUint64(&c.c) }