Skip to content
This repository has been archived by the owner on Oct 2, 2022. It is now read-only.

Commit

Permalink
Tests, documentation, & more
Browse files Browse the repository at this point in the history
  • Loading branch information
Janos Pasztor committed Nov 23, 2020
1 parent 74925be commit 63c4cd0
Show file tree
Hide file tree
Showing 18 changed files with 790 additions and 145 deletions.
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,71 @@ This library provides centralized metrics collection across modules. It also pro

## Collecting metrics

The core component of the metrics is the `metrics.Collector` interface. You can create a new instance of this interface by calling `metrics.New()` with a GeoIP lookup provider from the [geoip library](https://github.com/containerssh/geoip) as a parameter. You can then dynamically create metrics:

```go
m := metrics.New(geoip)
testCounter, err := m.CreateCounter(
"test", // Name of the metric
"MB", // Unit of the metric
"This is a test", // Help text of the metric
)
```

You can then increment the counter:

```go
testCounter.Increment()
testCounter.IncrementBy(5)
```

Alternatively, you can also create a CounterGeo to make a label automatically based on GeoIP lookup:

```go
testCounter, err := m.CreateCounterGeo(
"test", // Name of the metric
"MB", // Unit of the metric
"This is a test", // Help text of the metric
)
testCounter.Increment(net.ParseIP("127.0.0.1"))
```

If you need a metric that can be decremented or set directly you can use the `Gauge` type instead.

## Using the metrics server

The metrics server exposes the collected metrics on an HTTP webserver in the Prometheus / OpenMetrics format. It requires the [service library](https://github.com/containerssh/service) and a logger from the [log library](https://github.com/containerssh/log) to work properly:

```go
server := metrics.NewServer(
metrics.Config{
ServerConfiguration: http.ServerConfiguration{
Listen: "127.0.0.1:8080",
},
Enable: true,
Path: "/metrics",
},
metricsCollector,
logger,
)

lifecycle := service.NewLifecycle(server)
go func() {
if err := lifecycle.Run(); err != nil {
// Handle crash
}
}()

//Later:
lifecycle.Stop(context.Background())
```

Alternatively, you can skip the full HTTP server and request a handler that you can embed in any Go HTTP server:

```go
handler := metrics.NewHandler(
"/metrics",
metricsCollector
)
http.ListenAndServe("0.0.0.0:8080", handler)
```
251 changes: 152 additions & 99 deletions collector.go
Original file line number Diff line number Diff line change
@@ -1,132 +1,185 @@
package metrics

import (
"errors"
"fmt"
"net"
"sort"
"sync"
"strings"
"time"
)

// MetricType is the enum for tye types of metrics supported
type MetricType string

const (
// MetricTypeCounter is a data type that contains ever increasing numbers from the start of the server.
MetricTypeCounter MetricType = "counter"

"github.com/containerssh/geoip"
// MetricTypeGauge is a metric type that can increase or decrease depending on the current value.
MetricTypeGauge MetricType = "gauge"
)

type MetricCollector struct {
mutex *sync.Mutex
metricKeys map[string]*Metric
metrics map[string]map[string]float64
help map[string]string
types map[string]MetricType
geoIpLookupProvider geoip.LookupProvider
// Metric is a descriptor for metrics.
type Metric struct {
// Name is the name for the metric.
Name string

// Help is the help text for this metric.
Help string

// Unit describes the unit of the metric.
Unit string

// Created describes the time the metric was created. This is important for counters.
Created time.Time

// Type describes how the metric behaves.
Type MetricType
}

func New(geoIpLookupProvider geoip.LookupProvider) *MetricCollector {
return &MetricCollector{
&sync.Mutex{},
make(map[string]*Metric, 0),
make(map[string]map[string]float64, 0),
make(map[string]string, 0),
make(map[string]MetricType, 0),
geoIpLookupProvider,
}
// String formats a metric as the OpenMetrics metadata
func (metric Metric) String() string {
return fmt.Sprintf(
"# HELP %s %s\n"+
"# UNIT %s %s\n"+
"# TYPE %s %s\n",
metric.Name,
metric.Help,
metric.Name,
metric.Unit,
metric.Name,
metric.Type)
}

func (collector *MetricCollector) GetHelp(metricName string) string {
if val, ok := collector.help[metricName]; ok {
return val
}
return ""
// MetricValue is a structure that contains a value for a specific metric name and set of values.
type MetricValue struct {
// Name contains the name of the value.
Name string

// Labels contains a key-value map of labels to which the Value is specific.
Labels map[string]string

// Value contains the specific value stored.
Value float64
}

func (collector *MetricCollector) GetType(metricName string) MetricType {
if val, ok := collector.types[metricName]; ok {
return val
// CombinedName returns the name and labels combined.
func (metricValue MetricValue) CombinedName() string {
var labelList []string

keys := make([]string, 0, len(metricValue.Labels))
for k := range metricValue.Labels {
keys = append(keys, k)
}
return ""
}
sort.Strings(keys)

func (collector *MetricCollector) GetMetricNames() []string {
names := make([]string, len(collector.metrics))
i := 0
for k := range collector.metrics {
names[i] = k
i = i + 1
for _, k := range keys {
// TODO escaping
labelList = append(labelList, k+"=\""+metricValue.Labels[k]+"\"")
}
sort.Slice(names, func(i, j int) bool {
return names[i] < names[j]
})
return names
}

func (collector *MetricCollector) GetMetrics(name string) map[string]float64 {
if val, ok := collector.metrics[name]; ok {
return val
var labels string
if len(labelList) > 0 {
labels = "{" + strings.Join(labelList, ",") + "}"
} else {
return make(map[string]float64, 0)
labels = ""
}
}

func (collector *MetricCollector) SetMetricMeta(metricName string, help string, metricType MetricType) {
collector.mutex.Lock()
collector.help[metricName] = help
collector.types[metricName] = metricType
collector.mutex.Unlock()
return metricValue.Name + labels
}

func (collector *MetricCollector) Get(metric Metric) float64 {
collector.mutex.Lock()
defer collector.mutex.Unlock()
return collector.get(metric)
}
func (collector *MetricCollector) Set(metric Metric, value float64) {
collector.mutex.Lock()
defer collector.mutex.Unlock()
collector.set(metric, value)
}
func (collector *MetricCollector) Increment(metric Metric) float64 {
collector.mutex.Lock()
defer collector.mutex.Unlock()
value := collector.get(metric)
value = value + 1
collector.set(metric, value)
return value
// String creates a string out of the name, labels, and value.
func (metricValue MetricValue) String() string {
return fmt.Sprintf("%s %f\n", metricValue.CombinedName(), metricValue.Value)
}

func (collector *MetricCollector) IncrementGeo(metric Metric, remoteAddr net.IP) float64 {
metric.Labels["country"] = collector.geoIpLookupProvider.Lookup(remoteAddr)
return collector.Increment(metric)
// MetricAlreadyExists is an error that is returned from the Create functions when the metric already exists.
var MetricAlreadyExists = errors.New("the specified metric already exists")

// CounterCannotBeIncrementedByNegative is an error returned by counters when they are incremented with a negative
// number.
var CounterCannotBeIncrementedByNegative = errors.New("a counter cannot be incremented by a negative number")

// Collector is the main interface for interacting with the metrics collector.
type Collector interface {
// CreateCounter creates a monotonic (increasing) counter with the specified name and help text.
CreateCounter(name string, unit string, help string) (SimpleCounter, error)

// CreateCounterGeo creates a monotonic (increasing) counter that is labeled with the country from the GeoIP lookup
// with the specified name and help text.
CreateCounterGeo(name string, unit string, help string) (SimpleGeoCounter, error)

// CreateGauge creates a freely modifiable numeric gauge with the specified name and help text.
CreateGauge(name string, unit string, help string) (SimpleGauge, error)

// CreateGaugeGeo creates a freely modifiable numeric gauge that is labeled with the country from the GeoIP lookup
// with the specified name and help text.
CreateGaugeGeo(name string, unit string, help string) (SimpleGeoGauge, error)

// ListMetrics returns a list of metrics metadata stored in the collector.
ListMetrics() []Metric

// GetMetric returns a set of values with labels for a specified metric name.
GetMetric(name string) []MetricValue

// String returns a Prometheus/OpenMetrics-compatible document with all metrics.
String() string
}

func (collector *MetricCollector) Decrement(metric Metric) float64 {
collector.mutex.Lock()
defer collector.mutex.Unlock()
value := collector.get(metric)
value = value - 1
collector.set(metric, value)
return value
// SimpleCounter is a simple counter that can only be incremented.
type SimpleCounter interface {
// Increment increments the counter by 1
Increment()

// IncrementBy increments the counter by the specified number. Only returns an error if the passed by parameter is
// negative.
IncrementBy(by float64) error
}

func (collector *MetricCollector) DecrementGeo(metric Metric, remoteAddr net.IP) float64 {
metric.Labels["country"] = collector.geoIpLookupProvider.Lookup(remoteAddr)
return collector.Decrement(metric)
// SimpleGeoCounter is a simple counter that can only be incremented and is labeled with the country from a GeoIP
// lookup.
type SimpleGeoCounter interface {
// Increment increments the counter for the country from the specified ip by 1.
Increment(ip net.IP)

// IncrementBy increments the counter for the country from the specified ip by the specified value.
// Only returns an error if the passed by parameter is negative.
IncrementBy(ip net.IP, by float64) error
}

func (collector *MetricCollector) get(metric Metric) float64 {
if _, ok := collector.metricKeys[(&metric).ToString()]; !ok {
collector.metricKeys[(&metric).ToString()] = &metric
}
if _, ok := collector.metrics[metric.Name]; !ok {
collector.metrics[metric.Name] = make(map[string]float64, 0)
return 0
}
if _, ok := collector.metrics[metric.Name][(&metric).ToString()]; ok {
return collector.metrics[metric.Name][(&metric).ToString()]
}
return 0
// SimpleGauge is a metric that can be incremented and decremented.
type SimpleGauge interface {
// Increment increments the counter by 1
Increment()

// IncrementBy increments the counter by the specified number.
IncrementBy(by float64)

// Decrement decreases the metric by 1.
Decrement()

// Decrement decreases the metric by the specified value.
DecrementBy(by float64)

// Set sets the value of the metric to an exact value.
Set(value float64)
}
func (collector *MetricCollector) set(metric Metric, value float64) {
if _, ok := collector.metricKeys[(&metric).ToString()]; !ok {
collector.metricKeys[(&metric).ToString()] = &metric
}
if _, ok := collector.metrics[metric.Name]; !ok {
collector.metrics[metric.Name] = make(map[string]float64, 0)
}
collector.metrics[metric.Name][(&metric).ToString()] = value

// SimpleGeoGauge is a metric that can be incremented and decremented and is labeled by the country from a GeoIP lookup.
type SimpleGeoGauge interface {
// Increment increments the counter for the country from the specified ip by 1.
Increment(ip net.IP)

// IncrementBy increments the counter for the country from the specified ip by the specified value.
IncrementBy(ip net.IP, by float64)

// Decrement decreases the value for the country looked up from the specified IP by 1.
Decrement(ip net.IP)

// DecrementBy decreases the value for the country looked up from the specified IP by the specified value.
DecrementBy(ip net.IP, by float64)

// Set sets the value of the metric for the country looked up from the specified IP.
Set(ip net.IP, value float64)
}
18 changes: 18 additions & 0 deletions collector_factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package metrics

import (
"sync"

"github.com/containerssh/geoip"
)

// New creates the metric collector.
func New(geoIpLookupProvider geoip.LookupProvider) Collector {
return &collector{
geoIpLookupProvider: geoIpLookupProvider,
mutex: &sync.Mutex{},
metricsMap: map[string]Metric{},
metrics: []Metric{},
values: map[string]*metricValue{},
}
}
Loading

0 comments on commit 63c4cd0

Please sign in to comment.