diff --git a/prometheus/bloom.go b/prometheus/bloom.go new file mode 100644 index 0000000..eb9b988 --- /dev/null +++ b/prometheus/bloom.go @@ -0,0 +1,99 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + */ + +package prometheus + +import ( + "sync" + + "github.com/bits-and-blooms/bitset" +) + +const defaultSize = 2 << 24 + +var seeds = []uint{7, 11, 13, 31, 37, 61} + +type bloomFilter struct { + m sync.Mutex + Set *bitset.BitSet + Funcs [6]simpleHash +} + +type BloomFilter interface { + Add(value string) + Contains(value string) bool +} + +func NewBloomFilter() BloomFilter { + bf := new(bloomFilter) + + for i := 0; i < len(bf.Funcs); i++ { + bf.Funcs[i] = simpleHash{defaultSize, seeds[i]} + } + + bf.Set = bitset.New(defaultSize) + + return bf +} + +func (bf *bloomFilter) Add(value string) { + bf.m.Lock() + defer bf.m.Unlock() + + for _, f := range bf.Funcs { + bf.Set.Set(f.hash(value)) + } +} + +func (bf *bloomFilter) Contains(value string) bool { + if value == "" { + return false + } + + ret := true + + bf.m.Lock() + defer bf.m.Unlock() + + for _, f := range bf.Funcs { + ret = ret && bf.Set.Test(f.hash(value)) + } + + return ret +} + +type simpleHash struct { + Cap uint + Seed uint +} + +func (s *simpleHash) hash(value string) uint { + var result uint = 0 + for i := 0; i < len(value); i++ { + result = result*s.Seed + uint(value[i]) + } + return (s.Cap - 1) & result +} diff --git a/prometheus/interface.go b/prometheus/interface.go new file mode 100644 index 0000000..bfc8b44 --- /dev/null +++ b/prometheus/interface.go @@ -0,0 +1,67 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + */ + +package prometheus + +import ( + "sync/atomic" + "time" + + "github.com/gin-gonic/gin" +) + +const ( + DefaultSlowTime = int32(5) +) + +type Prometheus interface { + Expose(c *gin.Context) + MiddleWare(c *gin.Context) + CollectMetrics(c *gin.Context, start time.Time) + + ExcludePath(startWith ...string) + + GetMetric(name string) *metrics + SetMetric(metric Metrics) error + AddMetric(metric Metrics) error + ListMetric() []string + + SetSlowTime(slowTime int32) + GetSlowTime() int32 + + SetDuration(duration []float64) + GetDuration() []float64 +} + +// New will return a new object that implement interface GinPrometheus. +func New() Prometheus { + return &monitor{ + slowTime: DefaultSlowTime, + reqDuration: []float64{0.1, 0.3, 1.2, 5, 10}, + metrics: make(map[string]*atomic.Value), + exclude: make([]string, 0), + } +} diff --git a/prometheus/metrics.go b/prometheus/metrics.go new file mode 100644 index 0000000..c0a5209 --- /dev/null +++ b/prometheus/metrics.go @@ -0,0 +1,239 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + */ + +package prometheus + +import ( + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" +) + +type Metrics interface { + SetGaugeValue(labelValues []string, value float64) error + Inc(labelValues []string) error + Add(labelValues []string, value float64) error + Observe(labelValues []string, value float64) error + + Collect(c *gin.Context, start time.Time) + + GetName() string + GetType() MetricType + SetDesc(desc string) + + AddLabel(label ...string) + AddBuckets(bucket ...float64) + AddObjective(key, value float64) + SetCollect(fct FuncCollect) +} + +type FuncCollect func(m Metrics, c *gin.Context, start time.Time) + +func NewMetrics(name string, metricType MetricType) Metrics { + return &metrics{ + t: metricType, + n: name, + d: "", + l: make([]string, 0), + b: make([]float64, 0), + o: make(map[float64]float64, 0), + f: nil, + v: nil, + } +} + +// metrics defines a metric object. Users can use it to save +// metric data. Every metric should be globally unique by name. +type metrics struct { + m sync.Mutex + t MetricType + n string + d string + l []string + b []float64 + o map[float64]float64 + f FuncCollect + v prometheus.Collector +} + +// SetGaugeValue set data for Gauge type metrics. +func (m *metrics) SetGaugeValue(labelValues []string, value float64) error { + m.m.Lock() + defer m.m.Unlock() + + if m.t == None { + return errors.Errorf("metric '%s' not existed.", m.n) + } + + if m.t != Gauge { + return errors.Errorf("metric '%s' not Gauge type", m.n) + } + + m.v.(*prometheus.GaugeVec).WithLabelValues(labelValues...).Set(value) + return nil +} + +// Inc increases value for Counter/Gauge type metric, increments +// the counter by 1 +func (m *metrics) Inc(labelValues []string) error { + m.m.Lock() + defer m.m.Unlock() + + if m.t == None { + return errors.Errorf("metric '%s' not existed.", m.n) + } + + if m.t != Gauge && m.t != Counter { + return errors.Errorf("metric '%s' not Gauge or Counter type", m.n) + } + + switch m.t { + case Counter: + m.v.(*prometheus.CounterVec).WithLabelValues(labelValues...).Inc() + break + case Gauge: + m.v.(*prometheus.GaugeVec).WithLabelValues(labelValues...).Inc() + break + } + + return nil +} + +// Add adds the given value to the metrics object. Only +// for Counter/Gauge type metric. +func (m *metrics) Add(labelValues []string, value float64) error { + m.m.Lock() + defer m.m.Unlock() + + if m.t == None { + return errors.Errorf("metric '%s' not existed.", m.n) + } + + if m.t != Gauge && m.t != Counter { + return errors.Errorf("metric '%s' not Gauge or Counter type", m.n) + } + + switch m.t { + case Counter: + m.v.(*prometheus.CounterVec).WithLabelValues(labelValues...).Add(value) + break + case Gauge: + m.v.(*prometheus.GaugeVec).WithLabelValues(labelValues...).Add(value) + break + } + + return nil +} + +// Observe is used by Histogram and Summary type metric to +// add observations. +func (m *metrics) Observe(labelValues []string, value float64) error { + m.m.Lock() + defer m.m.Unlock() + + if m.t == 0 { + return errors.Errorf("metric '%s' not existed.", m.n) + } + + if m.t != Histogram && m.t != Summary { + return errors.Errorf("metric '%s' not Histogram or Summary type", m.n) + } + + switch m.t { + case Histogram: + m.v.(*prometheus.HistogramVec).WithLabelValues(labelValues...).Observe(value) + break + case Summary: + m.v.(*prometheus.SummaryVec).WithLabelValues(labelValues...).Observe(value) + break + } + + return nil +} + +func (m *metrics) Collect(c *gin.Context, start time.Time) { + if m.f != nil { + m.f(m, c, start) + } +} + +func (m *metrics) GetName() string { + return m.n +} + +func (m *metrics) GetType() MetricType { + return m.t +} + +func (m *metrics) SetDesc(desc string) { + m.m.Lock() + defer m.m.Unlock() + + m.d = desc +} + +func (m *metrics) AddLabel(label ...string) { + m.m.Lock() + defer m.m.Unlock() + + if len(m.l) < 1 { + m.l = make([]string, 0) + } + + m.l = append(m.l, label...) +} + +func (m *metrics) AddBuckets(bucket ...float64) { + m.m.Lock() + defer m.m.Unlock() + + if len(m.b) < 1 { + m.b = make([]float64, 0) + } + + m.b = append(m.b, bucket...) +} + +func (m *metrics) AddObjective(key, value float64) { + m.m.Lock() + defer m.m.Unlock() + + if len(m.o) < 1 { + m.o = make(map[float64]float64, 0) + } + + m.o[key] = value +} + +func (m *metrics) SetCollect(fct FuncCollect) { + m.m.Lock() + defer m.m.Unlock() + + m.f = fct +} diff --git a/prometheus/monitor.go b/prometheus/monitor.go new file mode 100644 index 0000000..b624e2b --- /dev/null +++ b/prometheus/monitor.go @@ -0,0 +1,209 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + */ + +package prometheus + +import ( + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// GinPrometheus is an object that uses to set gin server monitor. +type monitor struct { + m sync.Mutex + + exclude []string + slowTime int32 + reqDuration []float64 + + metrics map[string]*atomic.Value +} + +// Expose adds metric path to a given router. +// The router can be different with the one passed to UseWithoutExposingEndpoint. +// This allows to expose metrics on different port. +func (m *monitor) Expose(c *gin.Context) { + promhttp.Handler().ServeHTTP(c.Writer, c.Request) +} + +// MiddleWare as gin monitor middleware. +func (m *monitor) MiddleWare(c *gin.Context) { + startTime := time.Now() + + if len(m.exclude) > 0 { + r := c.Request.URL.Path + if !strings.HasPrefix(r, "/") { + r = "/" + r + } + for _, p := range m.exclude { + if p != "" && strings.HasPrefix(r, p) { + return + } + } + } + + // execute normal process. + c.Next() + + // after request + m.CollectMetrics(c, startTime) +} + +func (m *monitor) CollectMetrics(c *gin.Context, start time.Time) { + for _, k := range m.ListMetric() { + metric := m.GetMetric(k) + metric.Collect(c, start) + _ = m.SetMetric(metric) + } +} + +func (m *monitor) ExcludePath(startWith ...string) { + m.m.Lock() + defer m.m.Unlock() + + for _, p := range startWith { + if p != "" { + m.exclude = append(m.exclude, p) + } + } +} + +// SetSlowTime set slowTime property. slowTime is used to determine whether +// the request is slow. For "gin_slow_request_total" metric. +func (m *monitor) SetSlowTime(slowTime int32) { + m.m.Lock() + defer m.m.Unlock() + + m.slowTime = slowTime +} + +// GetSlowTime retrieve the slowTime property. slowTime is used to determine whether +// the request is slow. For "gin_slow_request_total" metric. +func (m *monitor) GetSlowTime() int32 { + m.m.Lock() + defer m.m.Unlock() + + return m.slowTime +} + +// SetDuration set duration property. duration is used to ginRequestDuration +// metric buckets. +func (m *monitor) SetDuration(duration []float64) { + m.m.Lock() + defer m.m.Unlock() + + m.reqDuration = duration +} + +// GetDuration retrieve the duration property. duration is used to ginRequestDuration +// metric buckets. +func (m *monitor) GetDuration() []float64 { + m.m.Lock() + defer m.m.Unlock() + + return m.reqDuration +} + +// GetMetric used to get metric object by metric_name. +func (m *monitor) GetMetric(name string) *metrics { + m.m.Lock() + defer m.m.Unlock() + + if a, ok := m.metrics[name]; !ok || a == nil { + return &metrics{} + } else if i := a.Load(); i == nil { + return &metrics{} + } else if o, ok := i.(*metrics); !ok || o == nil { + return &metrics{} + } else { + return o + } +} + +// SetMetric used to store an atomic value of the metric object by metric_name. +func (m *monitor) SetMetric(metric Metrics) error { + m.m.Lock() + defer m.m.Unlock() + + var ( + ok bool + o *metrics + ) + + if o, ok = metric.(*metrics); !ok { + return errors.Errorf("metric is not a valid metric instance") + } + + if o.n == "" { + return errors.Errorf("metric name cannot be empty.") + } + + if o.f == nil { + return errors.Errorf("metric collect func cannot be empty.") + } + + if _, ok = m.metrics[o.n]; !ok { + if err := o.t.Register(o); err != nil { + return err + } + + prometheus.MustRegister(o.v) + } + + if m.metrics[o.n] == nil { + m.metrics[o.n] = new(atomic.Value) + } + + m.metrics[o.n].Store(metric) + + return nil +} + +// AddMetric add custom monitor metric. +func (m *monitor) AddMetric(metric Metrics) error { + return m.SetMetric(metric) +} + +// ListMetric retrieve a slice of metrics' name registered +func (m *monitor) ListMetric() []string { + var res = make([]string, 0) + + m.m.Lock() + defer m.m.Unlock() + + for k := range m.metrics { + res = append(res, k) + } + + return res +} diff --git a/prometheus/types.go b/prometheus/types.go new file mode 100644 index 0000000..eba5dee --- /dev/null +++ b/prometheus/types.go @@ -0,0 +1,95 @@ +/* + * MIT License + * + * Copyright (c) 2022 Nicolas JUHEL + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * + */ + +package prometheus + +import ( + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" +) + +type MetricType int + +const ( + None MetricType = iota + Counter + Gauge + Histogram + Summary +) + +func (m MetricType) Register(metric *metrics) error { + switch metric.t { + case Counter: + return m.counterHandler(metric) + case Gauge: + return m.gaugeHandler(metric) + case Histogram: + return m.histogramHandler(metric) + case Summary: + return m.summaryHandler(metric) + } + + return errors.Errorf("metric type is not compatible.") +} + +func (m MetricType) counterHandler(metric *metrics) error { + metric.v = prometheus.NewCounterVec( + prometheus.CounterOpts{Name: metric.n, Help: metric.d}, + metric.l, + ) + return nil +} + +func (m MetricType) gaugeHandler(metric *metrics) error { + metric.v = prometheus.NewGaugeVec( + prometheus.GaugeOpts{Name: metric.n, Help: metric.d}, + metric.l, + ) + return nil +} + +func (m MetricType) histogramHandler(metric *metrics) error { + if len(metric.b) == 0 { + return errors.Errorf("metric '%s' is histogram type, cannot lose bucket param.", metric.n) + } + metric.v = prometheus.NewHistogramVec( + prometheus.HistogramOpts{Name: metric.n, Help: metric.d, Buckets: metric.b}, + metric.l, + ) + return nil +} + +func (m MetricType) summaryHandler(metric *metrics) error { + if len(metric.o) == 0 { + return errors.Errorf("metric '%s' is summary type, cannot lose objectives param.", metric.n) + } + prometheus.NewSummaryVec( + prometheus.SummaryOpts{Name: metric.n, Help: metric.d, Objectives: metric.o}, + metric.l, + ) + return nil +}