Czym jest Prometheus?

Prometheus jest to ekosystem do monitorowania napisany przez programistów z SoundCloud. Jak możecie się przekonać przeglądając oficjalne konto na githubie, większość środowiska jest napisana w Go. Od 2016 roku projekt jest też częścią Cloud Native Computing Foundation obok takich rozwiązań jak kubernetes, gRPC czy OpenTracing. Daje nam to pewność, że projekt będzie rozwijany przez długie lata, będzie ewoluował razem z resztą środowiska, a także wsparcie dla Go będzie stało na najwyższym poziomie.

Skupię się tutaj na ostatniej wersji, oznaczonej tagiem v0.8.0. W tej wersji wiele funkcji zostało oznaczonych jako DEPRECATED i zostaną one przeze mnie pominięte. Zalecana wersja Go to 1.9+.

Biblioteka

Zasadniczo Prometheus jako serwer centralny musi być świadomy istnienia aplikacji która jest monitorowana. Tylko wtedy jest on w stanie pobrać metryki ze wskazanego endpointu. Z pomocą przychodzi nam biblioteka promhttp, która jest częścią składową oficjalnej paczki.

promhttp.HandlerFor pozwala utworzyć endpoint dla danego prometheus.Gatherer’a. Interfejs ten jest na przykład implementowany przez prometheus.DefaultRegisterer.

Ponadto biblioteka ta zawiera garść dekoratorów które, pozwolą nam zbierać informacje na temat naszej aplikacji:

Implementacja

Jak widać, żeby zacząć nie trzeba się wiele napracować. Większość potrzebnych nam składników jest już dostępna. Brakujący element to kolektory które musimy zainicjować własnoręcznie.

duration := prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Namespace: "acme",
        Subsystem: "your_app",
        Name:      "http_durations_histogram_seconds",
        Help:      "Request time duration.",
    },
    []string{"code", "method"},
)
requests := prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Namespace: "acme",
        Subsystem: "your_app",
        Name:      "http_requests_total",
        Help:      "Total number of requests received.",
    },
    []string{"code", "method"},
)

Oba one muszą zostać zarejestrowane, a następnie przekazane jako argument do wyżej wymienionych dekoratorów. Możemy trochę usprawnić ten proces poprzez wprowadzenie dodatkowej struktury.

type decorator struct {
	duration *prometheus.HistogramVec
	requests *prometheus.CounterVec
}

Aby spełniać swoje zadanie, struktura ta powinna implementować interfejs prometheus.Collector.

// Describe implements prometheus Collector interface.
func (d *decorator) Describe(in chan<- *prometheus.Desc) {
	d.duration.Describe(in)
	d.requests.Describe(in)
}

// Collect implements prometheus Collector interface.
func (d *decorator) Collect(in chan<- prometheus.Metric) {
	d.duration.Collect(in)
	d.requests.Collect(in)
}

Dodatkowo, możemy zredukować duplikację kodu implementując dodatkową metodę. Jej zadaniem będzie dekorowanie danego handlera szeregiem funkcji.

func (d *decorator) instrument(handler http.Handler) http.Handler {
	return promhttp.InstrumentHandlerDuration(
		d.duration,
		promhttp.InstrumentHandlerCounter(
			d.requests,
			handler,
		),
	)
}

Naszym ostatnim krokiem będzie połączenie wszystkiego ze sobą i udostępnienie metryk.

func main() {
    dec := &decorator{
        // inicjalizacja
    }

    prometheus.DefaultRegisterer.Register(dec)

    go func() {
        dbg := http.NewServeMux()
        dbg.Handle("/metrics", promhttp.HandlerFor(
            prometheus.DefaultGatherer,
            promhttp.HandlerOpts{},
        ))
        http.ListenAndServe("0.0.0.0:8081", dbg)
    }()

    app := http.NewServeMux()
    app.Handle("/", dec.instrument(&handler{}))
    http.ListenAndServe("0.0.0.0:8080", app)
}

Aplikacja przez nas napisana będzie nasłuchiwać na dwóch portach. Pierwszy 8080, zarezerowany dla aplikacji właściwej. Drugi 8081, na którym prometheus będzie miał dostęp do metryk. Chciałbym zwrócić uwagę, że router został w drugim przypadku zastosowany nie bez powodu. Pozwoli on w przyszłości udostępnić na tym samym porcie healthcheck, czy też endpointy pprof.

Weryfikacja

Aby sprawdzić, czy nasze demo zwraca poprawny wynik, posłużymy się aplikacją powłoki systemowej curl.

$ curl http://localhost:8080
It works!
$ curl -s localhost:8081/metrics | grep 'acme_your_app_http_requests_total{code="200",method="get"}'
acme_your_app_http_requests_total{code="200",method="get"} 1
$ curl http://localhost:8080
It works!
$ curl -s localhost:8081/metrics | grep 'acme_your_app_http_requests_total{code="200",method="get"}'
acme_your_app_http_requests_total{code="200",method="get"} 2

Zwracana wartość odpowiada ilości wysłanych żądań. Monitoring działa bez zarzutu.

Podsumowanie

Aby utrzymać przejrzystość, przykłady nie zawierają wszystkich wspieranych metryk. Podczas ich implementacji warto zapoznać się z dokumentacją dekoratorów. Znajdują się tam informacje o wspieranych etykietach.

Pełny kod źródłowy aplikacji można znaleźć tutaj.