Skip to content

Commit

Permalink
Create a "merge gatherer"
Browse files Browse the repository at this point in the history
This allows to finally get rid of the infamous injection hook in the
interface. The old SetMetricFamilyInjectionHook still exist as a
deprecated function but is now implemented with the new plumbing under
the hood.

Now that we have multiple Gatherer implementation, I renamed
push.Registry to push.FromGatherer.

This commit also improves the consistency checks, which happened as a
byproduct of the refactoring to allow checking in both the "merge
gatherer" Gatherers as well as in the normal Registry.
  • Loading branch information
beorn7 committed Aug 12, 2016
1 parent 1dc03a7 commit a6321dd
Show file tree
Hide file tree
Showing 8 changed files with 465 additions and 215 deletions.
111 changes: 111 additions & 0 deletions prometheus/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
package prometheus_test

import (
"bytes"
"fmt"
"math"
"net/http"
"runtime"
"sort"
"strings"

dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"

"github.com/golang/protobuf/proto"

Expand Down Expand Up @@ -638,3 +641,111 @@ func ExampleAlreadyRegisteredError() {
}
}
}

func ExampleGatherers() {
reg := prometheus.NewRegistry()
temp := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "temperature_kelvin",
Help: "Temperature in Kelvin.",
},
[]string{"location"},
)
reg.MustRegister(temp)
temp.WithLabelValues("outside").Set(273.14)
temp.WithLabelValues("inside").Set(298.44)

var parser expfmt.TextParser

text := `
# TYPE humidity_percent gauge
# HELP humidity_percent Humidity in %.
humidity_percent{location="outside"} 45.4
humidity_percent{location="inside"} 33.2
# TYPE temperature_kelvin gauge
# HELP temperature_kelvin Temperature in Kelvin.
temperature_kelvin{location="somewhere else"} 4.5
`

parseText := func() ([]*dto.MetricFamily, error) {
parsed, err := parser.TextToMetricFamilies(strings.NewReader(text))
if err != nil {
return nil, err
}
var result []*dto.MetricFamily
for _, mf := range parsed {
result = append(result, mf)
}
return result, nil
}

gatherers := prometheus.Gatherers{
reg,
prometheus.GathererFunc(parseText),
}

gathering, err := gatherers.Gather()
if err != nil {
fmt.Println(err)
}

out := &bytes.Buffer{}
for _, mf := range gathering {
if _, err := expfmt.MetricFamilyToText(out, mf); err != nil {
panic(err)
}
}
fmt.Print(out.String())
fmt.Println("----------")

// Note how the temperature_kelvin metric family has been merged from
// different sources. Now try
text = `
# TYPE humidity_percent gauge
# HELP humidity_percent Humidity in %.
humidity_percent{location="outside"} 45.4
humidity_percent{location="inside"} 33.2
# TYPE temperature_kelvin gauge
# HELP temperature_kelvin Temperature in Kelvin.
# Duplicate metric:
temperature_kelvin{location="outside"} 265.3
# Wrong labels:
temperature_kelvin 4.5
`

gathering, err = gatherers.Gather()
if err != nil {
fmt.Println(err)
}
// Note that still as many metrics as possible are returned:
out.Reset()
for _, mf := range gathering {
if _, err := expfmt.MetricFamilyToText(out, mf); err != nil {
panic(err)
}
}
fmt.Print(out.String())

// Output:
// # HELP humidity_percent Humidity in %.
// # TYPE humidity_percent gauge
// humidity_percent{location="inside"} 33.2
// humidity_percent{location="outside"} 45.4
// # HELP temperature_kelvin Temperature in Kelvin.
// # TYPE temperature_kelvin gauge
// temperature_kelvin{location="inside"} 298.44
// temperature_kelvin{location="outside"} 273.14
// temperature_kelvin{location="somewhere else"} 4.5
// ----------
// 2 error(s) occurred:
// * collected metric temperature_kelvin label:<name:"location" value:"outside" > gauge:<value:265.3 > was collected before with the same name and label values
// * collected metric temperature_kelvin gauge:<value:4.5 > has label dimensions inconsistent with previously collected metrics in the same metric family
// # HELP humidity_percent Humidity in %.
// # TYPE humidity_percent gauge
// humidity_percent{location="inside"} 33.2
// humidity_percent{location="outside"} 45.4
// # HELP temperature_kelvin Temperature in Kelvin.
// # TYPE temperature_kelvin gauge
// temperature_kelvin{location="inside"} 298.44
// temperature_kelvin{location="outside"} 273.14
}
26 changes: 13 additions & 13 deletions prometheus/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ import (
const separatorByte byte = 255

// A Metric models a single sample value with its meta data being exported to
// Prometheus. Implementers of Metric in this package inclued Gauge, Counter,
// Untyped, and Summary. Users can implement their own Metric types, but that
// should be rarely needed. See the example for SelfCollector, which is also an
// example for a user-implemented Metric.
// Prometheus. Implementations of Metric in this package are Gauge, Counter,
// Histogram, Summary, and Untyped.
type Metric interface {
// Desc returns the descriptor for the Metric. This method idempotently
// returns the same descriptor throughout the lifetime of the
Expand All @@ -36,16 +34,18 @@ type Metric interface {
// Write encodes the Metric into a "Metric" Protocol Buffer data
// transmission object.
//
// Implementers of custom Metric types must observe concurrency safety
// as reads of this metric may occur at any time, and any blocking
// occurs at the expense of total performance of rendering all
// registered metrics. Ideally Metric implementations should support
// concurrent readers.
// Metric implementations must observe concurrency safety as reads of
// this metric may occur at any time, and any blocking occurs at the
// expense of total performance of rendering all registered
// metrics. Ideally, Metric implementations should support concurrent
// readers.
//
// While populating dto.Metric, it is recommended to sort labels
// lexicographically. (Implementers may find LabelPairSorter useful for
// that.) Callers of Write should still make sure of sorting if they
// depend on it.
// While populating dto.Metric, it is the responsibility of the
// implementation to ensure validity of the Metric protobuf (like valid
// UTF-8 strings or syntactically valid metric and label names). It is
// recommended to sort labels lexicographically. (Implementers may find
// LabelPairSorter useful for that.) Callers of Write should still make
// sure of sorting if they depend on it.
Write(*dto.Metric) error
// TODO(beorn7): The original rationale of passing in a pre-allocated
// dto.Metric protobuf to save allocations has disappeared. The
Expand Down
10 changes: 4 additions & 6 deletions prometheus/promhttp/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,11 @@ func TestHandlerErrorHandling(t *testing.T) {
ErrorLog: logger,
ErrorHandling: PanicOnError,
})
wantMsg := `error gathering metrics: 1 error(s) occurred:
* error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", constLabels: {}, variableLabels: []}: collect error
wantMsg := `error gathering metrics: error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", constLabels: {}, variableLabels: []}: collect error
`
wantErrorBody := `An error has occurred during metrics gathering:
1 error(s) occurred:
* error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", constLabels: {}, variableLabels: []}: collect error
error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", constLabels: {}, variableLabels: []}: collect error
`
wantOKBody := `# HELP name docstring
# TYPE name counter
Expand All @@ -110,10 +108,10 @@ the_count 0
t.Errorf("got HTTP status code %d, want %d", got, want)
}
if got := logBuf.String(); got != wantMsg {
t.Errorf("got log message %q, want %q", got, wantMsg)
t.Errorf("got log message:\n%s\nwant log mesage:\n%s\n", got, wantMsg)
}
if got := writer.Body.String(); got != wantErrorBody {
t.Errorf("got body %q, want %q", got, wantErrorBody)
t.Errorf("got body:\n%s\nwant body:\n%s\n", got, wantErrorBody)
}
logBuf.Reset()
writer.Body.Reset()
Expand Down
2 changes: 1 addition & 1 deletion prometheus/push/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func ExampleRegistry() {
registry.MustRegister(completionTime)

completionTime.Set(float64(time.Now().Unix()))
if err := push.Registry(
if err := push.FromGatherer(
"db_backup", push.HostnameGroupingKey(),
"http:https://pushgateway:9091",
registry,
Expand Down
40 changes: 20 additions & 20 deletions prometheus/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ import (

const contentTypeHeader = "Content-Type"

// Registry triggers a metric collection by the provided Gatherer (which is
// usually implemented by a prometheus.Registry, thus the name of the function)
// and pushes all gathered metrics to the Pushgateway specified by url, using
// the provided job name and the (optional) further grouping labels (the
// grouping map may be nil). See the Pushgateway documentation for detailed
// implications of the job and other grouping labels. Neither the job name nor
// any grouping label value may contain a "/". The metrics pushed must not
// contain a job label of their own nor any of the grouping labels.
// FromGatherer triggers a metric collection by the provided Gatherer (which is
// usually implemented by a prometheus.Registry) and pushes all gathered metrics
// to the Pushgateway specified by url, using the provided job name and the
// (optional) further grouping labels (the grouping map may be nil). See the
// Pushgateway documentation for detailed implications of the job and other
// grouping labels. Neither the job name nor any grouping label value may
// contain a "/". The metrics pushed must not contain a job label of their own
// nor any of the grouping labels.
//
// You can use just host:port or ip:port as url, in which case 'http:https://' is
// added automatically. You can also include the schema in the URL. However, do
Expand All @@ -60,18 +60,18 @@ const contentTypeHeader = "Content-Type"
// Note that all previously pushed metrics with the same job and other grouping
// labels will be replaced with the metrics pushed by this call. (It uses HTTP
// method 'PUT' to push to the Pushgateway.)
func Registry(job string, grouping map[string]string, url string, reg prometheus.Gatherer) error {
return push(job, grouping, url, reg, "PUT")
func FromGatherer(job string, grouping map[string]string, url string, g prometheus.Gatherer) error {
return push(job, grouping, url, g, "PUT")
}

// RegistryAdd works like Registry, but only previously pushed metrics with the
// same name (and the same job and other grouping labels) will be replaced. (It
// uses HTTP method 'POST' to push to the Pushgateway.)
func RegistryAdd(job string, grouping map[string]string, url string, reg prometheus.Gatherer) error {
return push(job, grouping, url, reg, "POST")
// AddFromGatherer works like FromGatherer, but only previously pushed metrics
// with the same name (and the same job and other grouping labels) will be
// replaced. (It uses HTTP method 'POST' to push to the Pushgateway.)
func AddFromGatherer(job string, grouping map[string]string, url string, g prometheus.Gatherer) error {
return push(job, grouping, url, g, "POST")
}

func push(job string, grouping map[string]string, pushURL string, reg prometheus.Gatherer, method string) error {
func push(job string, grouping map[string]string, pushURL string, g prometheus.Gatherer, method string) error {
if !strings.Contains(pushURL, ":https://") {
pushURL = "http:https://" + pushURL
}
Expand All @@ -94,7 +94,7 @@ func push(job string, grouping map[string]string, pushURL string, reg prometheus
}
pushURL = fmt.Sprintf("%s/metrics/job/%s", pushURL, strings.Join(urlComponents, "/"))

mfs, err := reg.Gather()
mfs, err := g.Gather()
if err != nil {
return err
}
Expand Down Expand Up @@ -134,14 +134,14 @@ func push(job string, grouping map[string]string, pushURL string, reg prometheus
return nil
}

// Collectors works like Registry, but it does not use a Gatherer. Instead, it
// collects from the provided collectors directly. It is a convenient way to
// Collectors works like FromGatherer, but it does not use a Gatherer. Instead,
// it collects from the provided collectors directly. It is a convenient way to
// push only a few metrics.
func Collectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error {
return pushCollectors(job, grouping, url, "PUT", collectors...)
}

// AddCollectors works like RegistryAdd, but it does not use a Gatherer.
// AddCollectors works like AddFromGatherer, but it does not use a Gatherer.
// Instead, it collects from the provided collectors directly. It is a
// convenient way to push only a few metrics.
func AddCollectors(job string, grouping map[string]string, url string, collectors ...prometheus.Collector) error {
Expand Down
4 changes: 2 additions & 2 deletions prometheus/push/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func TestPush(t *testing.T) {
}

// Push registry, all good.
if err := Registry("testjob", HostnameGroupingKey(), pgwOK.URL, reg); err != nil {
if err := FromGatherer("testjob", HostnameGroupingKey(), pgwOK.URL, reg); err != nil {
t.Fatal(err)
}
if lastMethod != "PUT" {
Expand All @@ -161,7 +161,7 @@ func TestPush(t *testing.T) {
}

// PushAdd registry, all good.
if err := RegistryAdd("testjob", map[string]string{"a": "x", "b": "y"}, pgwOK.URL, reg); err != nil {
if err := AddFromGatherer("testjob", map[string]string{"a": "x", "b": "y"}, pgwOK.URL, reg); err != nil {
t.Fatal(err)
}
if lastMethod != "POST" {
Expand Down
Loading

0 comments on commit a6321dd

Please sign in to comment.