-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add a metric exposition utility package
[#166889819] Signed-off-by: Travis Patterson <tpatterson@pivotal.io>
- Loading branch information
Showing
3 changed files
with
305 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package metrics_test | ||
|
||
import ( | ||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
"log" | ||
|
||
"testing" | ||
) | ||
|
||
func TestMetrics(t *testing.T) { | ||
log.SetOutput(GinkgoWriter) | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "Metrics Suite") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package metrics | ||
|
||
import ( | ||
"fmt" | ||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/prometheus/client_golang/prometheus/promhttp" | ||
"log" | ||
"net" | ||
"net/http" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// The Registry keeps track of registered counters and gauges. Optionally, it can | ||
// provide a server on a Prometheus-formatted endpoint. | ||
type Registry struct { | ||
port string | ||
defaultTags map[string]string | ||
loggr *log.Logger | ||
} | ||
|
||
// A cumulative metric that represents a single monotonically increasing counter | ||
// whose value can only increase or be reset to zero on restart | ||
type Counter interface { | ||
Add(float64) | ||
} | ||
|
||
// A single numerical value that can arbitrarily go up and down. | ||
type Gauge interface { | ||
Add(float64) | ||
Set(float64) | ||
} | ||
|
||
// Registry will register the metrics route with the default http mux but will not | ||
// start an http server. This is intentional so that we can combine metrics with | ||
// other things like pprof into one server. To start a server | ||
// just for metrics, use the WithServer RegistryOption | ||
func NewRegistry(logger *log.Logger, opts ...RegistryOption) *Registry { | ||
pr := &Registry{ | ||
loggr: logger, | ||
defaultTags: make(map[string]string), | ||
} | ||
|
||
for _, o := range opts { | ||
o(pr) | ||
} | ||
|
||
http.Handle("/metrics", promhttp.Handler()) | ||
return pr | ||
} | ||
|
||
// Creates new counter. When a duplicate is registered, the Registry will return | ||
// the previously created metric. | ||
func (p *Registry) NewCounter(name string, opts ...MetricOption) Counter { | ||
opt := p.toPromOpt(name, "counter metric", opts...) | ||
c := prometheus.NewCounter(prometheus.CounterOpts(opt)) | ||
return p.registerCollector(name, c).(Counter) | ||
} | ||
|
||
// Creates new gauge. When a duplicate is registered, the Registry will return | ||
// the previously created metric. | ||
func (p *Registry) NewGauge(name string, opts ...MetricOption) Gauge { | ||
opt := p.toPromOpt(name, "gauge metric", opts...) | ||
g := prometheus.NewGauge(prometheus.GaugeOpts(opt)) | ||
return p.registerCollector(name, g).(Gauge) | ||
} | ||
|
||
func (p *Registry) registerCollector(name string, c prometheus.Collector) prometheus.Collector { | ||
err := prometheus.DefaultRegisterer.Register(c) | ||
if err != nil { | ||
typ, ok := err.(prometheus.AlreadyRegisteredError) | ||
if !ok { | ||
p.loggr.Panicf("unable to create %s: %s", name, err) | ||
} | ||
|
||
return typ.ExistingCollector | ||
} | ||
|
||
return c | ||
} | ||
|
||
// Get the port of the running metrics server | ||
func (p *Registry) Port() string { | ||
return fmt.Sprint(p.port) | ||
} | ||
|
||
func (p *Registry) toPromOpt(name, helpText string, mOpts ...MetricOption) prometheus.Opts { | ||
opt := prometheus.Opts{ | ||
Name: name, | ||
Help: helpText, | ||
ConstLabels: make(map[string]string), | ||
} | ||
|
||
for _, o := range mOpts { | ||
o(&opt) | ||
} | ||
|
||
for k, v := range p.defaultTags { | ||
opt.ConstLabels[k] = v | ||
} | ||
|
||
return opt | ||
} | ||
|
||
// Options for registry initialization | ||
type RegistryOption func(r *Registry) | ||
|
||
// Add Default tags to all gauges and counters created from this registry | ||
func WithDefaultTags(tags map[string]string) RegistryOption { | ||
return func(r *Registry) { | ||
for k, v := range tags { | ||
r.defaultTags[k] = v | ||
} | ||
} | ||
} | ||
|
||
// Starts an http server on the given port to host metrics. | ||
func WithServer(port int) RegistryOption { | ||
return func(r *Registry) { | ||
r.start(port) | ||
} | ||
} | ||
|
||
func (p *Registry) start(port int) { | ||
addr := fmt.Sprintf("127.0.0.1:%d", port) | ||
s := http.Server{ | ||
Addr: addr, | ||
ReadTimeout: 5 * time.Second, | ||
WriteTimeout: 5 * time.Second, | ||
} | ||
|
||
lis, err := net.Listen("tcp", addr) | ||
if err != nil { | ||
p.loggr.Fatalf("Unable to setup metrics endpoint (%s): %s", addr, err) | ||
} | ||
p.loggr.Printf("Metrics endpoint is listening on %s", lis.Addr().String()) | ||
|
||
parts := strings.Split(lis.Addr().String(), ":") | ||
p.port = parts[len(parts)-1] | ||
|
||
go s.Serve(lis) | ||
} | ||
|
||
// Options applied to metrics on creation | ||
type MetricOption func(o *prometheus.Opts) | ||
|
||
// Add these tags to the metrics | ||
func WithMetricTags(tags map[string]string) MetricOption { | ||
return func(o *prometheus.Opts) { | ||
for k, v := range tags { | ||
o.ConstLabels[k] = v | ||
} | ||
} | ||
} | ||
|
||
// Add the passed help text to the created metric | ||
func WithHelpText(helpText string) MetricOption { | ||
return func(o *prometheus.Opts) { | ||
o.Help = helpText | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package metrics_test | ||
|
||
import ( | ||
"code.cloudfoundry.org/go-loggregator/metrics" | ||
"fmt" | ||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
"github.com/prometheus/client_golang/prometheus" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
) | ||
|
||
var _ = Describe("PrometheusMetrics", func() { | ||
|
||
var ( | ||
l *log.Logger | ||
) | ||
|
||
BeforeEach(func() { | ||
l = log.New(GinkgoWriter, "", log.LstdFlags) | ||
|
||
// This is needed because the prom registry will register | ||
// the /metrics route with the default http mux which is | ||
// global | ||
http.DefaultServeMux = new(http.ServeMux) | ||
|
||
// Resetting prometheus registry because we use the global | ||
// DefaultRegisterer for the default instrumentation | ||
r := prometheus.NewRegistry() | ||
prometheus.DefaultRegisterer = r | ||
prometheus.DefaultGatherer = r | ||
}) | ||
|
||
It("serves metrics on a prometheus endpoint", func() { | ||
r := metrics.NewRegistry(l, metrics.WithServer(0)) | ||
|
||
c := r.NewCounter( | ||
"test_counter", | ||
metrics.WithMetricTags(map[string]string{"foo": "bar"}), | ||
metrics.WithHelpText("a counter help text for test_counter"), | ||
) | ||
|
||
g := r.NewGauge( | ||
"test_gauge", | ||
metrics.WithHelpText("a gauge help text for test_gauge"), | ||
metrics.WithMetricTags(map[string]string{"bar": "baz"}), | ||
) | ||
|
||
c.Add(10) | ||
g.Set(10) | ||
g.Add(1) | ||
|
||
Eventually(func() string { return getMetrics(r.Port()) }).Should(ContainSubstring(`test_counter{foo="bar"} 10`)) | ||
Eventually(func() string { return getMetrics(r.Port()) }).Should(ContainSubstring("a counter help text for test_counter")) | ||
Eventually(func() string { return getMetrics(r.Port()) }).Should(ContainSubstring(`test_gauge{bar="baz"} 11`)) | ||
Eventually(func() string { return getMetrics(r.Port()) }).Should(ContainSubstring("a gauge help text for test_gauge")) | ||
}) | ||
|
||
It("accepts custom default tags", func() { | ||
ct := map[string]string{ | ||
"tag": "custom", | ||
} | ||
|
||
r := metrics.NewRegistry(l, metrics.WithDefaultTags(ct), metrics.WithServer(0)) | ||
|
||
r.NewCounter( | ||
"test_counter", | ||
metrics.WithHelpText("a counter help text for test_counter"), | ||
) | ||
|
||
r.NewGauge( | ||
"test_gauge", | ||
metrics.WithHelpText("a gauge help text for test_gauge"), | ||
) | ||
|
||
Eventually(func() string { return getMetrics(r.Port()) }).Should(ContainSubstring(`test_counter{tag="custom"} 0`)) | ||
Eventually(func() string { return getMetrics(r.Port()) }).Should(ContainSubstring(`test_gauge{tag="custom"} 0`)) | ||
}) | ||
|
||
It("returns the metric when duplicate is created", func() { | ||
r := metrics.NewRegistry(l, metrics.WithServer(0)) | ||
|
||
c := r.NewCounter("test_counter") | ||
c2 := r.NewCounter("test_counter") | ||
|
||
c.Add(1) | ||
c2.Add(2) | ||
|
||
Eventually(func() string { | ||
return getMetrics(r.Port()) | ||
}).Should(ContainSubstring(`test_counter 3`)) | ||
|
||
g := r.NewGauge("test_gauge") | ||
g2 := r.NewGauge("test_gauge") | ||
|
||
g.Add(1) | ||
g2.Add(2) | ||
|
||
Eventually(func() string { | ||
return getMetrics(r.Port()) | ||
}).Should(ContainSubstring(`test_gauge 3`)) | ||
}) | ||
|
||
It("panics if the metric is invalid", func() { | ||
r := metrics.NewRegistry(l) | ||
|
||
Expect(func() { | ||
r.NewCounter("test-counter") | ||
}).To(Panic()) | ||
|
||
Expect(func() { | ||
r.NewGauge("test-counter") | ||
}).To(Panic()) | ||
}) | ||
}) | ||
|
||
func getMetrics(port string) string { | ||
addr := fmt.Sprintf("http://127.0.0.1:%s/metrics", port) | ||
resp, err := http.Get(addr) | ||
if err != nil { | ||
return "" | ||
} | ||
|
||
respBytes, err := ioutil.ReadAll(resp.Body) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
return string(respBytes) | ||
} |