promhermes

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 21, 2020 License: MIT Imports: 15 Imported by: 1

Documentation

Overview

Package promhermes provides tooling around HTTP servers.

First, the package allows the creation of hermes.Handler instances to expose Prometheus metrics via HTTP. promhermes.Handler acts on the prometheus.DefaultGatherer. With HandlerFor, you can create a handler for a custom registry or anything that implements the Gatherer interface. It also allows the creation of handlers that act differently on errors or allow to log errors.

Second, the package provides tooling to instrument instances of hermes.Handler via middleware. Middleware wrappers follow the naming scheme InstrumentHandlerX, where X describes the intended use of the middleware. See each function's doc comment for specific details.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultHandler

func DefaultHandler() hermes.Handler

DefaultHandler returns an hermes.Handler for the prometheus.DefaultGatherer, using default HandlerOpts, i.e. it reports the first error as an HTTP error, it has no error logging, and it applies compression if requested by the client.

The returned hermes.Handler is already instrumented using the InstrumentMetricHandler function and the prometheus.DefaultRegisterer. If you create multiple hermes.Handlers by separate calls of the Handler function, the metrics used for instrumentation will be shared between them, providing global scrape counts.

This function is meant to cover the bulk of basic use cases. If you are doing anything that requires more customization (including using a non-default Gatherer, different instrumentation, and non-default HandlerOpts), use the HandlerFor function. See there for details.

func Handler

func Handler(srv interface {
	prometheus.Gatherer
	prometheus.Registerer
}) hermes.Handler

Handler returns an hermes.Handler for the promsrv.Service, using default HandlerOpts, i.e. it reports the first error as an HTTP error, it has no error logging, and it applies compression if requested by the client.

The returned hermes.Handler is already instrumented using the InstrumentMetricHandler function and the prometheus.DefaultRegisterer. If you create multiple hermes.Handlers by separate calls of the Handler function, the metrics used for instrumentation will be shared between them, providing global scrape counts.

This function is meant to cover the bulk of basic use cases. If you are doing anything that requires more customization (including using a non-default Gatherer, different instrumentation, and non-default HandlerOpts), use the HandlerFor function. See there for details.

func HandlerFor

func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) hermes.Handler

HandlerFor returns an uninstrumented hermes.Handler for the provided Gatherer. The behavior of the Handler is defined by the provided HandlerOpts. Thus, HandlerFor is useful to create hermes.Handlers for custom Gatherers, with non-default HandlerOpts, and/or with custom (or no) instrumentation. Use the InstrumentMetricHandler function to apply the same kind of instrumentation as it is used by the Handler function.

func InstrumentHandlerCounter

func InstrumentHandlerCounter(counter *prometheus.CounterVec, next hermes.Handler) hermes.Handler

InstrumentHandlerCounter is a middleware that wraps the provided hermes.Handler to observe the request result with the provided CounterVec. The CounterVec must have zero, one, or two non-const non-curried labels. For those, the only allowed label names are "code" and "method". The function panics otherwise. Partitioning of the CounterVec happens by HTTP status code and/or HTTP method if the respective instance label names are present in the CounterVec. For unpartitioned counting, use a CounterVec with zero labels.

If the wrapped Handler does not set a status code, a status code of 200 is assumed.

If the wrapped Handler panics, the Counter is not incremented.

See the example for InstrumentHandlerDuration for example usage.

func InstrumentHandlerDuration

func InstrumentHandlerDuration(obs prometheus.ObserverVec, next hermes.Handler) hermes.Handler

InstrumentHandlerDuration is a middleware that wraps the provided hermes.Handler to observe the request duration with the provided ObserverVec. The ObserverVec must have zero, one, or two non-const non-curried labels. For those, the only allowed label names are "code" and "method". The function panics otherwise. The Observe method of the Observer in the ObserverVec is called with the request duration in seconds. Partitioning happens by HTTP status code and/or HTTP method if the respective instance label names are present in the ObserverVec. For unpartitioned observations, use an ObserverVec with zero labels. Note that partitioning of Histograms is expensive and should be used judiciously.

If the wrapped Handler does not set a status code, a status code of 200 is assumed.

If the wrapped Handler panics, no values are reported.

Note that this method is only guaranteed to never observe negative durations if used with Go1.9+.

Example
package main

import (
	"fmt"
	"log"

	"github.com/valyala/fasthttp"

	"github.com/lab259/hermes"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/prometheus/client_golang/prometheus"
)

func createRequestCtx(method, path string) *fasthttp.RequestCtx {
	ctx := &fasthttp.RequestCtx{}
	ctx.Request.Header.SetMethod(method)
	ctx.Request.URI().SetPath(path)
	return ctx
}

var _ = Describe("Instrument Server", func() {
	When("validating labels", func() {
		scenarios := map[string]struct {
			varLabels     []string
			constLabels   []string
			curriedLabels []string
			ok            bool
		}{
			"empty": {
				varLabels:     []string{},
				constLabels:   []string{},
				curriedLabels: []string{},
				ok:            true,
			},
			"code as single var label": {
				varLabels:     []string{"code"},
				constLabels:   []string{},
				curriedLabels: []string{},
				ok:            true,
			},
			"method as single var label": {
				varLabels:     []string{"method"},
				constLabels:   []string{},
				curriedLabels: []string{},
				ok:            true,
			},
			"cade and method as var labels": {
				varLabels:     []string{"method", "code"},
				constLabels:   []string{},
				curriedLabels: []string{},
				ok:            true,
			},
			"valid case with all labels used": {
				varLabels:     []string{"code", "method"},
				constLabels:   []string{"foo", "bar"},
				curriedLabels: []string{"dings", "bums"},
				ok:            true,
			},
			"unsupported var label": {
				varLabels:     []string{"foo"},
				constLabels:   []string{},
				curriedLabels: []string{},
				ok:            false,
			},
			"mixed var labels": {
				varLabels:     []string{"method", "foo", "code"},
				constLabels:   []string{},
				curriedLabels: []string{},
				ok:            false,
			},
			"unsupported var label but curried": {
				varLabels:     []string{},
				constLabels:   []string{},
				curriedLabels: []string{"foo"},
				ok:            true,
			},
			"mixed var labels but unsupported curried": {
				varLabels:     []string{"code", "method"},
				constLabels:   []string{},
				curriedLabels: []string{"foo"},
				ok:            true,
			},
			"supported label as const and curry": {
				varLabels:     []string{},
				constLabels:   []string{"code"},
				curriedLabels: []string{"method"},
				ok:            true,
			},
			"supported label as const and curry with unsupported as var": {
				varLabels:     []string{"foo"},
				constLabels:   []string{"code"},
				curriedLabels: []string{"method"},
				ok:            false,
			},
		}

		for name, sc := range scenarios {
			It(name, func() {
				constLabels := prometheus.Labels{}
				for _, l := range sc.constLabels {
					constLabels[l] = "dummy"
				}
				c := prometheus.NewCounterVec(
					prometheus.CounterOpts{
						Name:        "c",
						Help:        "c help",
						ConstLabels: constLabels,
					},
					append(sc.varLabels, sc.curriedLabels...),
				)
				o := prometheus.ObserverVec(prometheus.NewHistogramVec(
					prometheus.HistogramOpts{
						Name:        "c",
						Help:        "c help",
						ConstLabels: constLabels,
					},
					append(sc.varLabels, sc.curriedLabels...),
				))
				for _, l := range sc.curriedLabels {
					c = c.MustCurryWith(prometheus.Labels{l: "dummy"})
					o = o.MustCurryWith(prometheus.Labels{l: "dummy"})
				}

				func() {
					defer func() {
						if err := recover(); err != nil {
							if sc.ok {
								Fail(fmt.Sprintf("unexpected panic: %s", err))
							}
						} else if !sc.ok {
							Fail("expected panic")
						}
					}()
					InstrumentHandlerCounter(c, nil)
				}()
				func() {
					defer func() {
						if err := recover(); err != nil {
							if sc.ok {
								Fail(fmt.Sprintf("unexpected panic: %s", err))
							}
						} else if !sc.ok {
							Fail("expected panic")
						}
					}()
					InstrumentHandlerDuration(o, nil)
				}()
				if sc.ok {
					// Test if wantCode and wantMethod were detected correctly.
					var wantCode, wantMethod bool
					for _, l := range sc.varLabels {
						if l == "code" {
							wantCode = true
						}
						if l == "method" {
							wantMethod = true
						}
					}
					gotCode, gotMethod := checkLabels(c)
					Expect(gotCode).To(Equal(wantCode))
					Expect(gotMethod).To(Equal(wantMethod))

					gotCode, gotMethod = checkLabels(o)
					Expect(gotCode).To(Equal(wantCode))
					Expect(gotMethod).To(Equal(wantMethod))
				}
			})
		}
	})

	It("should chain handlers", func() {
		reg := prometheus.NewRegistry()

		inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{
			Name: "in_flight_requests",
			Help: "A gauge of requests currently being served by the wrapped handler.",
		})

		counter := prometheus.NewCounterVec(
			prometheus.CounterOpts{
				Name: "api_requests_total",
				Help: "A counter for requests to the wrapped handler.",
			},
			[]string{"code", "method"},
		)

		histVec := prometheus.NewHistogramVec(
			prometheus.HistogramOpts{
				Name:        "response_duration_seconds",
				Help:        "A histogram of request latencies.",
				Buckets:     prometheus.DefBuckets,
				ConstLabels: prometheus.Labels{"handler": "api"},
			},
			[]string{"method"},
		)

		writeHeaderVec := prometheus.NewHistogramVec(
			prometheus.HistogramOpts{
				Name:        "write_header_duration_seconds",
				Help:        "A histogram of time to first write latencies.",
				Buckets:     prometheus.DefBuckets,
				ConstLabels: prometheus.Labels{"handler": "api"},
			},
			[]string{},
		)

		responseSize := prometheus.NewHistogramVec(
			prometheus.HistogramOpts{
				Name:    "push_request_size_bytes",
				Help:    "A histogram of request sizes for requests.",
				Buckets: []float64{200, 500, 900, 1500},
			},
			[]string{},
		)

		handler := hermes.Handler(func(req hermes.Request, res hermes.Response) hermes.Result {
			return res.Data([]byte("OK"))
		})

		reg.MustRegister(inFlightGauge, counter, histVec, responseSize, writeHeaderVec)

		chain := InstrumentHandlerInFlight(inFlightGauge,
			InstrumentHandlerCounter(counter,
				InstrumentHandlerDuration(histVec,
					// InstrumentHandlerTimeToWriteHeader(writeHeaderVec,
					InstrumentHandlerResponseSize(responseSize, handler),
					// ),
				),
			),
		)

		router := hermes.DefaultRouter()
		router.Get("/", chain)

		ctx := createRequestCtx("GET", "/")
		router.Handler()(ctx)
	})
})

func main() {
	inFlightGauge := prometheus.NewGauge(prometheus.GaugeOpts{
		Name: "in_flight_requests",
		Help: "A gauge of requests currently being served by the wrapped handler.",
	})

	counter := prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "api_requests_total",
			Help: "A counter for requests to the wrapped handler.",
		},
		[]string{"code", "method"},
	)

	// duration is partitioned by the HTTP method and handler. It uses custom
	// buckets based on the expected request duration.
	duration := prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "request_duration_seconds",
			Help:    "A histogram of latencies for requests.",
			Buckets: []float64{.25, .5, 1, 2.5, 5, 10},
		},
		[]string{"handler", "method"},
	)

	// responseSize has no labels, making it a zero-dimensional
	// ObserverVec.
	responseSize := prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "response_size_bytes",
			Help:    "A histogram of response sizes for requests.",
			Buckets: []float64{200, 500, 900, 1500},
		},
		[]string{},
	)

	// Create the handlers that will be wrapped by the middleware.
	pushHandler := hermes.Handler(func(req hermes.Request, res hermes.Response) hermes.Result {
		return res.Data([]byte("Push"))
	})
	pullHandler := hermes.Handler(func(req hermes.Request, res hermes.Response) hermes.Result {
		return res.Data([]byte("Pull"))
	})

	// Register all of the metrics in the standard registry.
	prometheus.MustRegister(inFlightGauge, counter, duration, responseSize)

	// Instrument the handlers with all the metrics, injecting the "handler"
	// label by currying.
	pushChain := InstrumentHandlerInFlight(inFlightGauge,
		InstrumentHandlerDuration(duration.MustCurryWith(prometheus.Labels{"handler": "push"}),
			InstrumentHandlerCounter(counter,
				InstrumentHandlerResponseSize(responseSize, pushHandler),
			),
		),
	)
	pullChain := InstrumentHandlerInFlight(inFlightGauge,
		InstrumentHandlerDuration(duration.MustCurryWith(prometheus.Labels{"handler": "pull"}),
			InstrumentHandlerCounter(counter,
				InstrumentHandlerResponseSize(responseSize, pullHandler),
			),
		),
	)

	router := hermes.DefaultRouter()
	router.Get("/metrics", DefaultHandler())
	router.Get("/push", pushChain)
	router.Get("/pull", pullChain)

	app := hermes.NewApplication(hermes.ApplicationConfig{
		HTTP: hermes.FasthttpServiceConfiguration{
			Bind: ":3000",
		},
	}, router)

	if err := app.Start(); err != nil {
		log.Fatal(err)
	}
}
Output:

func InstrumentHandlerInFlight

func InstrumentHandlerInFlight(g prometheus.Gauge, next hermes.Handler) hermes.Handler

InstrumentHandlerInFlight is a middleware that wraps the provided hermes.Handler. It sets the provided prometheus.Gauge to the number of requests currently handled by the wrapped hermes.Handler.

See the example for InstrumentHandlerDuration for example usage.

func InstrumentHandlerRequestSize

func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next hermes.Handler) hermes.Handler

InstrumentHandlerRequestSize is a middleware that wraps the provided hermes.Handler to observe the request size with the provided ObserverVec. The ObserverVec must have zero, one, or two non-const non-curried labels. For those, the only allowed label names are "code" and "method". The function panics otherwise. The Observe method of the Observer in the ObserverVec is called with the request size in bytes. Partitioning happens by HTTP status code and/or HTTP method if the respective instance label names are present in the ObserverVec. For unpartitioned observations, use an ObserverVec with zero labels. Note that partitioning of Histograms is expensive and should be used judiciously.

If the wrapped Handler does not set a status code, a status code of 200 is assumed.

If the wrapped Handler panics, no values are reported.

See the example for InstrumentHandlerDuration for example usage.

func InstrumentHandlerResponseSize

func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next hermes.Handler) hermes.Handler

InstrumentHandlerResponseSize is a middleware that wraps the provided hermes.Handler to observe the response size with the provided ObserverVec. The ObserverVec must have zero, one, or two non-const non-curried labels. For those, the only allowed label names are "code" and "method". The function panics otherwise. The Observe method of the Observer in the ObserverVec is called with the response size in bytes. Partitioning happens by HTTP status code and/or HTTP method if the respective instance label names are present in the ObserverVec. For unpartitioned observations, use an ObserverVec with zero labels. Note that partitioning of Histograms is expensive and should be used judiciously.

If the wrapped Handler does not set a status code, a status code of 200 is assumed.

If the wrapped Handler panics, no values are reported.

See the example for InstrumentHandlerDuration for example usage.

func InstrumentMetricHandler

func InstrumentMetricHandler(reg prometheus.Registerer, handler hermes.Handler) hermes.Handler

InstrumentMetricHandler is usually used with an hermes.Handler returned by the HandlerFor function. It instruments the provided hermes.Handler with two metrics: A counter vector "promhermes_metric_handler_requests_total" to count scrapes partitioned by HTTP status code, and a gauge "promhermes_metric_handler_requests_in_flight" to track the number of simultaneous scrapes. This function idempotently registers collectors for both metrics with the provided Registerer. It panics if the registration fails. The provided metrics are useful to see how many scrapes hit the monitored target (which could be from different Prometheus servers or other scrapers), and how often they overlap (which would result in more than one scrape in flight at the same time). Note that the scrapes-in-flight gauge will contain the scrape by which it is exposed, while the scrape counter will only get incremented after the scrape is complete (as only then the status code is known). For tracking scrape durations, use the "scrape_duration_seconds" gauge created by the Prometheus server upon each scrape.

Types

type HandlerErrorHandling

type HandlerErrorHandling int

HandlerErrorHandling defines how a Handler serving metrics will handle errors.

const (
	// Serve an HTTP status code 500 upon the first error
	// encountered. Report the error message in the body.
	HTTPErrorOnError HandlerErrorHandling = iota
	// Ignore errors and try to serve as many metrics as possible.  However,
	// if no metrics can be served, serve an HTTP status code 500 and the
	// last error message in the body. Only use this in deliberate "best
	// effort" metrics collection scenarios. In this case, it is highly
	// recommended to provide other means of detecting errors: By setting an
	// ErrorLog in HandlerOpts, the errors are logged. By providing a
	// Registry in HandlerOpts, the exposed metrics include an error counter
	// "promhermes_metric_handler_errors_total", which can be used for
	// alerts.
	ContinueOnError
	// Panic upon the first error encountered (useful for "crash only" apps).
	PanicOnError
)

These constants cause handlers serving metrics to behave as described if errors are encountered.

type HandlerOpts

type HandlerOpts struct {
	// ErrorLog specifies an optional logger for errors collecting and
	// serving metrics. If nil, errors are not logged at all.
	ErrorLog Logger
	// ErrorHandling defines how errors are handled. Note that errors are
	// logged regardless of the configured ErrorHandling provided ErrorLog
	// is not nil.
	ErrorHandling HandlerErrorHandling
	// If Registry is not nil, it is used to register a metric
	// "promhermes_metric_handler_errors_total", partitioned by "cause". A
	// failed registration causes a panic. Note that this error counter is
	// different from the instrumentation you get from the various
	// InstrumentHandler... helpers. It counts errors that don't necessarily
	// result in a non-2xx HTTP status code. There are two typical cases:
	// (1) Encoding errors that only happen after streaming of the HTTP body
	// has already started (and the status code 200 has been sent). This
	// should only happen with custom collectors. (2) Collection errors with
	// no effect on the HTTP status code because ErrorHandling is set to
	// ContinueOnError.
	Registry prometheus.Registerer
	// If DisableCompression is true, the handler will never compress the
	// response, even if requested by the client.
	DisableCompression bool
	// The number of concurrent HTTP requests is limited to
	// MaxRequestsInFlight. Additional requests are responded to with 503
	// Service Unavailable and a suitable message in the body. If
	// MaxRequestsInFlight is 0 or negative, no limit is applied.
	MaxRequestsInFlight int
	// If handling a request takes longer than Timeout, it is responded to
	// with 503 ServiceUnavailable and a suitable Message. No timeout is
	// applied if Timeout is 0 or negative. Note that with the current
	// implementation, reaching the timeout simply ends the HTTP requests as
	// described above (and even that only if sending of the body hasn't
	// started yet), while the bulk work of gathering all the metrics keeps
	// running in the background (with the eventual result to be thrown
	// away). Until the implementation is improved, it is recommended to
	// implement a separate timeout in potentially slow Collectors.
	Timeout time.Duration
}

HandlerOpts specifies options how to serve metrics via an hermes.Handler. The zero value of HandlerOpts is a reasonable default.

type Logger

type Logger interface {
	Println(v ...interface{})
}

Logger is the minimal interface HandlerOpts needs for logging. Note that log.Logger from the standard library implements this interface, and it is easy to implement by custom loggers, if they don't do so already anyway.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL