diff --git a/CHANGELOG.md b/CHANGELOG.md index b69d0c9..9cbd298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.9.3: Custom label support + +Each of the metric methods now allow adding extra labels: + +```go +testCounter.Increment( + net.ParseIP("127.0.0.1"), + metrics.Label("foo", "bar"), + metrics.Label("somelabel","somevalue") +) +``` + +The following rules apply and will cause a `panic` if violated: + +- Label names and values cannot be empty. +- The `country` label name is reserved for GeoIP usage. + ## 0.9.2: Fixed JSON and YAML marshalling In the previous version the JSON and YAML configuration marshalling / unmarshalling created an unnecessary sub-map, which was incompatible to ContainerSSH 0.3. This release fixes that and restores compatibility. diff --git a/README.md b/README.md index 251bfa3..e3506ac 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,23 @@ 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. +### Custom labels + +Each of the metric methods allow adding extra labels: + +```go +testCounter.Increment( + net.ParseIP("127.0.0.1"), + metrics.Label("foo", "bar"), + metrics.Label("somelabel","somevalue") +) +``` + +The following rules apply and will cause a `panic` if violated: + +- Label names and values cannot be empty. +- The `country` label name is reserved for GeoIP usage. + ## 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: @@ -79,4 +96,5 @@ handler := metrics.NewHandler( metricsCollector ) http.ListenAndServe("0.0.0.0:8080", handler) -``` \ No newline at end of file +``` + diff --git a/collector.go b/collector.go index cd23b40..6d29786 100644 --- a/collector.go +++ b/collector.go @@ -49,7 +49,8 @@ func (metric Metric) String() string { metric.Name, metric.Unit, metric.Name, - metric.Type) + metric.Type, + ) } // MetricValue is a structure that contains a value for a specific metric name and set of values. @@ -64,6 +65,52 @@ type MetricValue struct { Value float64 } +// MetricLabel is a struct that can be used with the metrics to pass additional labels. Create it using the +// Label function. +type MetricLabel struct { + name string + value string +} + +type metricLabels []MetricLabel + +func (labels metricLabels) toMap() map[string]string { + result := map[string]string{} + for _, label := range labels { + result[label.name] = label.value + + } + return result +} + +// Label creates a MetricLabel for use with the metric functions. Panics if name or value are empty. The name "country" +// is reserved. +func Label(name string, value string) MetricLabel { + if name == "" { + panic("BUG: the name cannot be empty") + } + if name == "country" { + panic("BUG: the name 'country' is reserved for GeoIP lookups") + } + if value == "" { + panic("BUG: the value cannot be empty") + } + return MetricLabel{ + name: name, + value: value, + } +} + +// Name returns the name of the label. +func (m MetricLabel) Name() string { + return m.name +} + +// Value returns the value of the label. +func (m MetricLabel) Value() string { + return m.value +} + // CombinedName returns the name and labels combined. func (metricValue MetricValue) CombinedName() string { var labelList []string @@ -74,9 +121,9 @@ func (metricValue MetricValue) CombinedName() string { } sort.Strings(keys) + replacer := strings.NewReplacer(`"`, `\"`, `\`, `\\`) for _, k := range keys { - // TODO escaping - labelList = append(labelList, k+"=\""+metricValue.Labels[k]+"\"") + labelList = append(labelList, k+"=\""+replacer.Replace(metricValue.Labels[k])+"\"") } var labels string @@ -130,56 +177,84 @@ type Collector interface { // SimpleCounter is a simple counter that can only be incremented. type SimpleCounter interface { // Increment increments the counter by 1 - Increment() + // + // - labels is a set of labels to apply. Can be created using the Label function. + Increment(labels ...MetricLabel) // IncrementBy increments the counter by the specified number. Only returns an error if the passed by parameter is // negative. - IncrementBy(by float64) error + // + // - labels is a set of labels to apply. Can be created using the Label function. + IncrementBy(by float64, labels ...MetricLabel) error } // 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) + // + // - labels is a set of labels to apply. Can be created using the Label function. + Increment(ip net.IP, labels ...MetricLabel) // 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 + // + // - labels is a set of labels to apply. Can be created using the Label function. + IncrementBy(ip net.IP, by float64, labels ...MetricLabel) error } // SimpleGauge is a metric that can be incremented and decremented. type SimpleGauge interface { - // Increment increments the counter by 1 - Increment() + // Increment increments the counter by 1. + // + // - labels is a set of labels to apply. Can be created using the Label function. + Increment(labels ...MetricLabel) // IncrementBy increments the counter by the specified number. - IncrementBy(by float64) + // + // - labels is a set of labels to apply. Can be created using the Label function. + IncrementBy(by float64, labels ...MetricLabel) // Decrement decreases the metric by 1. - Decrement() + // + // - labels is a set of labels to apply. Can be created using the Label function. + Decrement(labels ...MetricLabel) // Decrement decreases the metric by the specified value. - DecrementBy(by float64) + // + // - labels is a set of labels to apply. Can be created using the Label function. + DecrementBy(by float64, labels ...MetricLabel) // Set sets the value of the metric to an exact value. - Set(value float64) + // + // - labels is a set of labels to apply. Can be created using the Label function. + Set(value float64, labels ...MetricLabel) } // 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) + // + // - labels is a set of labels to apply. Can be created using the Label function. + Increment(ip net.IP, labels ...MetricLabel) // IncrementBy increments the counter for the country from the specified ip by the specified value. - IncrementBy(ip net.IP, by float64) + // + // - labels is a set of labels to apply. Can be created using the Label function. + IncrementBy(ip net.IP, by float64, labels ...MetricLabel) // Decrement decreases the value for the country looked up from the specified IP by 1. - Decrement(ip net.IP) + // + // - labels is a set of labels to apply. Can be created using the Label function. + Decrement(ip net.IP, labels ...MetricLabel) // DecrementBy decreases the value for the country looked up from the specified IP by the specified value. - DecrementBy(ip net.IP, by float64) + // + // - labels is a set of labels to apply. Can be created using the Label function. + DecrementBy(ip net.IP, by float64, labels ...MetricLabel) // Set sets the value of the metric for the country looked up from the specified IP. - Set(ip net.IP, value float64) + // + // - labels is a set of labels to apply. Can be created using the Label function. + Set(ip net.IP, value float64, labels ...MetricLabel) } diff --git a/counter.go b/counter.go index 55633cb..316e47f 100644 --- a/counter.go +++ b/counter.go @@ -5,11 +5,11 @@ type counterImpl struct { collector *collector } -func (c *counterImpl) Increment() { - _ = c.IncrementBy(1) +func (c *counterImpl) Increment(labels ...MetricLabel) { + _ = c.IncrementBy(1, labels...) } -func (c *counterImpl) IncrementBy(by float64) error { +func (c *counterImpl) IncrementBy(by float64, labels ...MetricLabel) error { c.collector.mutex.Lock() defer c.collector.mutex.Unlock() @@ -17,7 +17,8 @@ func (c *counterImpl) IncrementBy(by float64) error { return CounterCannotBeIncrementedByNegative } - value := c.collector.get(c.name, map[string]string{}) - c.collector.set(c.name, map[string]string{}, value+by) + realLabels := metricLabels(labels).toMap() + value := c.collector.get(c.name, realLabels) + c.collector.set(c.name, realLabels, value+by) return nil } diff --git a/counter_test.go b/counter_test.go index a40e6ac..8ce0011 100644 --- a/counter_test.go +++ b/counter_test.go @@ -51,4 +51,14 @@ func TestCounter(t *testing.T) { metrics.CounterCannotBeIncrementedByNegative.Error(), "incrementing a counter by negative number did not return an error", ) + + counter.Increment(metrics.Label("foo", "bar")) + metric = collector.GetMetric("test") + for _, m := range metric { + if m.CombinedName() == "test{foo=\"bar\"}" { + assert.Equal(t, float64(1), m.Value) + } else { + assert.Equal(t, float64(4), m.Value) + } + } } diff --git a/countergeo.go b/countergeo.go index 783c387..326da52 100644 --- a/countergeo.go +++ b/countergeo.go @@ -9,11 +9,11 @@ type counterGeoImpl struct { collector *collector } -func (c *counterGeoImpl) Increment(ip net.IP) { - _ = c.IncrementBy(ip, 1) +func (c *counterGeoImpl) Increment(ip net.IP, labels ...MetricLabel) { + _ = c.IncrementBy(ip, 1, labels...) } -func (c *counterGeoImpl) IncrementBy(ip net.IP, by float64) error { +func (c *counterGeoImpl) IncrementBy(ip net.IP, by float64, labels ...MetricLabel) error { c.collector.mutex.Lock() defer c.collector.mutex.Unlock() @@ -21,11 +21,10 @@ func (c *counterGeoImpl) IncrementBy(ip net.IP, by float64) error { return CounterCannotBeIncrementedByNegative } - labels := map[string]string{ - "country": c.collector.geoIpLookupProvider.Lookup(ip), - } + realLabels := metricLabels(labels).toMap() + realLabels["country"] = c.collector.geoIpLookupProvider.Lookup(ip) - value := c.collector.get(c.name, labels) - c.collector.set(c.name, labels, value+by) + value := c.collector.get(c.name, realLabels) + c.collector.set(c.name, realLabels, value+by) return nil } diff --git a/countergeo_test.go b/countergeo_test.go index 28a4090..84a8748 100644 --- a/countergeo_test.go +++ b/countergeo_test.go @@ -44,4 +44,12 @@ func TestCounterGeo(t *testing.T) { assert.Equal(t, 2, len(metric)) assert.Equal(t, float64(1), metric[0].Value) assert.Equal(t, float64(1), metric[1].Value) + + counter.Increment(net.ParseIP("127.0.0.2"), metrics.Label("foo", "bar")) + metric = collector.GetMetric("test") + for _, m := range metric { + if m.CombinedName() == "test{country=\"XX\",foo=\"bar\"}" { + assert.Equal(t, float64(1), m.Value) + } + } } diff --git a/gauge.go b/gauge.go index 79f4871..21352a0 100644 --- a/gauge.go +++ b/gauge.go @@ -5,33 +5,36 @@ type gaugeImpl struct { collector *collector } -func (g *gaugeImpl) Increment() { - g.IncrementBy(1) +func (g *gaugeImpl) Increment(labels ...MetricLabel) { + g.IncrementBy(1, labels...) } -func (g *gaugeImpl) IncrementBy(by float64) { +func (g *gaugeImpl) IncrementBy(by float64, labels ...MetricLabel) { g.collector.mutex.Lock() defer g.collector.mutex.Unlock() - value := g.collector.get(g.name, map[string]string{}) - g.collector.set(g.name, map[string]string{}, value+by) + realLabels := metricLabels(labels).toMap() + value := g.collector.get(g.name, realLabels) + g.collector.set(g.name, realLabels, value+by) } -func (g *gaugeImpl) Decrement() { - g.DecrementBy(1) +func (g *gaugeImpl) Decrement(labels ...MetricLabel) { + g.DecrementBy(1, labels...) } -func (g *gaugeImpl) DecrementBy(by float64) { +func (g *gaugeImpl) DecrementBy(by float64, labels ...MetricLabel) { g.collector.mutex.Lock() defer g.collector.mutex.Unlock() - value := g.collector.get(g.name, map[string]string{}) - g.collector.set(g.name, map[string]string{}, value-by) + realLabels := metricLabels(labels).toMap() + value := g.collector.get(g.name, realLabels) + g.collector.set(g.name, realLabels, value-by) } -func (g *gaugeImpl) Set(value float64) { +func (g *gaugeImpl) Set(value float64, labels ...MetricLabel) { g.collector.mutex.Lock() defer g.collector.mutex.Unlock() - g.collector.set(g.name, map[string]string{}, value) + realLabels := metricLabels(labels).toMap() + g.collector.set(g.name, realLabels, value) } diff --git a/gaugegeo.go b/gaugegeo.go index acd46ae..d666758 100644 --- a/gaugegeo.go +++ b/gaugegeo.go @@ -9,45 +9,42 @@ type gaugeGeoImpl struct { collector *collector } -func (g *gaugeGeoImpl) Increment(ip net.IP) { - g.IncrementBy(ip, 1) +func (g *gaugeGeoImpl) Increment(ip net.IP, labels ...MetricLabel) { + g.IncrementBy(ip, 1, labels...) } -func (g *gaugeGeoImpl) IncrementBy(ip net.IP, by float64) { +func (g *gaugeGeoImpl) IncrementBy(ip net.IP, by float64, labels ...MetricLabel) { g.collector.mutex.Lock() defer g.collector.mutex.Unlock() - labels := map[string]string{ - "country": g.collector.geoIpLookupProvider.Lookup(ip), - } + realLabels := metricLabels(labels).toMap() + realLabels["country"] = g.collector.geoIpLookupProvider.Lookup(ip) - value := g.collector.get(g.name, labels) - g.collector.set(g.name, labels, value+by) + value := g.collector.get(g.name, realLabels) + g.collector.set(g.name, realLabels, value+by) } -func (g *gaugeGeoImpl) Decrement(ip net.IP) { - g.DecrementBy(ip, 1) +func (g *gaugeGeoImpl) Decrement(ip net.IP, labels ...MetricLabel) { + g.DecrementBy(ip, 1, labels...) } -func (g *gaugeGeoImpl) DecrementBy(ip net.IP, by float64) { +func (g *gaugeGeoImpl) DecrementBy(ip net.IP, by float64, labels ...MetricLabel) { g.collector.mutex.Lock() defer g.collector.mutex.Unlock() - labels := map[string]string{ - "country": g.collector.geoIpLookupProvider.Lookup(ip), - } + realLabels := metricLabels(labels).toMap() + realLabels["country"] = g.collector.geoIpLookupProvider.Lookup(ip) - value := g.collector.get(g.name, labels) - g.collector.set(g.name, labels, value-by) + value := g.collector.get(g.name, realLabels) + g.collector.set(g.name, realLabels, value-by) } -func (g *gaugeGeoImpl) Set(ip net.IP, value float64) { +func (g *gaugeGeoImpl) Set(ip net.IP, value float64, labels ...MetricLabel) { g.collector.mutex.Lock() defer g.collector.mutex.Unlock() - labels := map[string]string{ - "country": g.collector.geoIpLookupProvider.Lookup(ip), - } + realLabels := metricLabels(labels).toMap() + realLabels["country"] = g.collector.geoIpLookupProvider.Lookup(ip) - g.collector.set(g.name, labels, value) + g.collector.set(g.name, realLabels, value) } diff --git a/go.mod b/go.mod index 492c2df..b7b2ed4 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.14 require ( github.com/containerssh/geoip v0.9.3 - github.com/containerssh/http v0.9.1 - github.com/containerssh/log v0.9.5 + github.com/containerssh/http v0.9.2 + github.com/containerssh/log v0.9.7 github.com/containerssh/service v0.9.0 github.com/stretchr/testify v1.6.1 ) diff --git a/go.sum b/go.sum index 3bd27e7..e33937f 100644 --- a/go.sum +++ b/go.sum @@ -2,22 +2,36 @@ github.com/containerssh/geoip v0.9.3 h1:RDj46uxB3Ew7qoTHCUClBDkqULRZJOLxYf/VqPgE github.com/containerssh/geoip v0.9.3/go.mod h1:8fyg4eUrKKY0FDyS7GofI6OO7UXDoG1icRtVAHb510k= github.com/containerssh/http v0.9.1 h1:n7fNwRoZR+HsuAinuIdDbSl28O/d9O7raoUUYyeQkgA= github.com/containerssh/http v0.9.1/go.mod h1:rKeRa8glbNsA08etU2HkRAsQ5Wn9J6Ht8Ly/aSNXC/I= +github.com/containerssh/http v0.9.2 h1:ZvGaQy/xxNE+UGavsyp2bYwagz9/s+c11wVQoCQXRY4= +github.com/containerssh/http v0.9.2/go.mod h1:ZuPYt0qDhZ4FRWRf/gUyG/VChNRIMd4N7gSA3Qs3SCI= github.com/containerssh/log v0.9.2 h1:QngZdg3EFvFxthpJY4X2qgVwqSHFTzHJ2kCiQkR7FLQ= github.com/containerssh/log v0.9.2/go.mod h1:05pgNm7IgFKt+qbZiUhtuJw2B4j3ynn2vSv5j2JA7hA= github.com/containerssh/log v0.9.5 h1:uXBtOr0ao/0plopnIz0trAH7VmIYQUDhKwoZsZ20ufM= github.com/containerssh/log v0.9.5/go.mod h1:P/tea/If6kzzfqlZdmqNp5SOpD6CD/OvmvcOK+vbweI= +github.com/containerssh/log v0.9.6/go.mod h1:P/tea/If6kzzfqlZdmqNp5SOpD6CD/OvmvcOK+vbweI= +github.com/containerssh/log v0.9.7 h1:T1FxnEtGMbyJJ7C66dDr6k2RwTnSpVGE5U5apr5pB78= +github.com/containerssh/log v0.9.7/go.mod h1:NBMzkhOLZ4z45ShSBKQ/Ij6Hqqg15DgOKy6HlSITx0s= github.com/containerssh/service v0.9.0 h1:JUHqiK12tclq7EWQYGRTfgKKw6fhHs0gxlKWTvVwFlQ= github.com/containerssh/service v0.9.0/go.mod h1:otAKYF1MWy2eB0K7Sk7YQIECQMTHR3yikbyS1UstGpY= +github.com/containerssh/structutils v0.9.0/go.mod h1:zirdwNXan3kuTpsJp9Gl3W6VQz0fexqMySqxmfviSjw= +github.com/creasty/defaults v1.5.1/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= github.com/gordonklaus/ineffassign v0.0.0-20200809085317-e36bfde3bb78/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oschwald/geoip2-golang v1.4.0 h1:5RlrjCgRyIGDz/mBmPfnAF4h8k0IAcRv9PvrpOfz+Ug= github.com/oschwald/geoip2-golang v1.4.0/go.mod h1:8QwxJvRImBH+Zl6Aa6MaIcs5YdlZSTKtzmPGzQqi9ng= github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls= github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= @@ -49,6 +63,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=