diff options
-rw-r--r-- | cmd/rr/http/metrics.go | 86 | ||||
-rw-r--r-- | cmd/rr/main.go | 4 | ||||
-rw-r--r-- | go.mod | 5 | ||||
-rw-r--r-- | service/metrics/config.go | 23 | ||||
-rw-r--r-- | service/metrics/service.go | 53 |
5 files changed, 168 insertions, 3 deletions
diff --git a/cmd/rr/http/metrics.go b/cmd/rr/http/metrics.go new file mode 100644 index 00000000..f9d3c079 --- /dev/null +++ b/cmd/rr/http/metrics.go @@ -0,0 +1,86 @@ +package http + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cobra" + rr "github.com/spiral/roadrunner/cmd/rr/cmd" + rrhttp "github.com/spiral/roadrunner/service/http" + "github.com/spiral/roadrunner/service/metrics" + "strconv" +) + +func init() { + cobra.OnInitialize(func() { + svc, _ := rr.Container.Get(metrics.ID) + mtr, ok := svc.(*metrics.Service) + if !ok || !mtr.Enabled() { + return + } + + ht, _ := rr.Container.Get(rrhttp.ID) + if ht, ok := ht.(*rrhttp.Service); ok { + collector := newCollector() + + // register metrics + mtr.MustRegister(collector.requestCounter) + mtr.MustRegister(collector.requestDuration) + + // collect events + ht.AddListener(collector.listener) + } + }) +} + +// listener provide debug callback for system events. With colors! +type metricCollector struct { + requestCounter *prometheus.CounterVec + requestDuration *prometheus.HistogramVec +} + +func newCollector() *metricCollector { + return &metricCollector{ + requestCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "rr_http_total", + Help: "Total number of handled http requests after server restart.", + }, + []string{"status"}, + ), + requestDuration: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "rr_http_request_duration", + Help: "HTTP request duration.", + Buckets: []float64{0.25, 0.5, 1, 10, 20, 60}, + }, + []string{"status"}, + ), + } +} + +// listener listens to http events and generates nice looking output. +func (c *metricCollector) listener(event int, ctx interface{}) { + // http events + switch event { + case rrhttp.EventResponse: + e := ctx.(*rrhttp.ResponseEvent) + + c.requestCounter.With(prometheus.Labels{ + "status": strconv.Itoa(e.Response.Status), + }).Inc() + + c.requestDuration.With(prometheus.Labels{ + "status": strconv.Itoa(e.Response.Status), + }).Observe(e.Elapsed().Seconds()) + + case rrhttp.EventError: + e := ctx.(*rrhttp.ErrorEvent) + + c.requestCounter.With(prometheus.Labels{ + "status": "500", + }).Inc() + + c.requestDuration.With(prometheus.Labels{ + "status": "500", + }).Observe(e.Elapsed().Seconds()) + } +} diff --git a/cmd/rr/main.go b/cmd/rr/main.go index 6f325fb4..6fb10ba6 100644 --- a/cmd/rr/main.go +++ b/cmd/rr/main.go @@ -24,12 +24,13 @@ package main import ( rr "github.com/spiral/roadrunner/cmd/rr/cmd" - "github.com/spiral/roadrunner/service/headers" // services (plugins) "github.com/spiral/roadrunner/service/env" + "github.com/spiral/roadrunner/service/headers" "github.com/spiral/roadrunner/service/http" "github.com/spiral/roadrunner/service/limit" + "github.com/spiral/roadrunner/service/metrics" "github.com/spiral/roadrunner/service/rpc" "github.com/spiral/roadrunner/service/static" @@ -42,6 +43,7 @@ func main() { rr.Container.Register(env.ID, &env.Service{}) rr.Container.Register(rpc.ID, &rpc.Service{}) rr.Container.Register(http.ID, &http.Service{}) + rr.Container.Register(metrics.ID, &metrics.Service{}) rr.Container.Register(headers.ID, &headers.Service{}) rr.Container.Register(static.ID, &static.Service{}) rr.Container.Register(limit.ID, &limit.Service{}) @@ -13,15 +13,16 @@ require ( github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b github.com/olekukonko/tablewriter v0.0.1 github.com/pkg/errors v0.8.1 + github.com/prometheus/client_golang v1.0.0 github.com/shirou/gopsutil v2.17.12+incompatible github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect github.com/sirupsen/logrus v1.3.0 github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.3.1 github.com/spiral/goridge v2.1.3+incompatible - github.com/stretchr/testify v1.2.2 + github.com/stretchr/testify v1.3.0 github.com/yookoala/gofast v0.3.0 - golang.org/x/net v0.0.0-20181017193950-04a2e542c03f + golang.org/x/net v0.0.0-20181114220301-adae6a3d119a golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/service/metrics/config.go b/service/metrics/config.go new file mode 100644 index 00000000..799ba2d2 --- /dev/null +++ b/service/metrics/config.go @@ -0,0 +1,23 @@ +package metrics + +import "github.com/spiral/roadrunner/service" + +type Config struct { + // Address to listen + Address string + + // Metrics define application specific metrics. + Metrics map[string]Metric +} + +// Metric describes single application specific metric. +type Metric struct { + Type string + Description string + Labels []string +} + +// Hydrate configuration. +func (c *Config) Hydrate(cfg service.Config) error { + return cfg.Unmarshal(c) +} diff --git a/service/metrics/service.go b/service/metrics/service.go new file mode 100644 index 00000000..50b533f8 --- /dev/null +++ b/service/metrics/service.go @@ -0,0 +1,53 @@ +package metrics + +import ( + "context" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "net/http" +) + +// ID declares public service name. +const ID = "metrics" + +// Service to manage application metrics using Prometheus. +type Service struct { + cfg *Config + http *http.Server +} + +// Init service. +func (s *Service) Init(cfg *Config) (bool, error) { + s.cfg = cfg + return true, nil +} + +// Enabled indicates that server is able to collect metrics. +func (s *Service) Enabled() bool { + return s.cfg != nil +} + +// Register new prometheus collector. +func (s *Service) Register(c prometheus.Collector) error { + return prometheus.Register(c) +} + +// MustRegister registers new collector or fails with panic. +func (s *Service) MustRegister(c prometheus.Collector) { + if err := prometheus.Register(c); err != nil { + panic(err) + } +} + +// Serve prometheus metrics service. +func (s *Service) Serve() error { + s.http = &http.Server{Addr: s.cfg.Address, Handler: promhttp.Handler()} + + return s.http.ListenAndServe() +} + +// Stop prometheus metrics service. +func (s *Service) Stop() { + // gracefully stop server + s.http.Shutdown(context.Background()) +} |