Go Services mit Prometheus überwachen

Go Services mit Prometheus überwachen

Spätestens nach der Produktivnahme des eigenen Go-basierten Microservice ist es sinnvoll den Service kontinuierlich zu überwachen. Eine einfache und effektive Möglichkeit besteht darin Monitoringdaten periodisch in eine Timeseries Datenbank (TSDB) zu speichern und anschließend die Daten zu visualisieren.

Klassische Monitoringwerkzeuge wie Nagios oder Icinga stoßen bei der Überwachung von sehr dynamischen, kontainerbasierten Cloud-Services schnell an ihre Grenzen. In Cloud-native-Umgebungen breiten sich aus diesem Grund immer mehr Monitoringsysteme mit TSDBs durch.

Ein Beispiel eines solchen Systems ist das Open-Source Projekt Prometheus, das selbst in Go implementiert ist und eine Datenbank sowie ein schlankes Frontend zur Anzeige der Daten bereitstellt.

Im Artikel soll ein überschaubares, aber vollständiges, Beispiel für die Überwachung einer Go Applikation gezeigt werden. Ein HTTP basierter Web-Service soll mittels Prometheus überwacht werden und die Ergebnisse sollen grafisch in Grafana angezeigt werden.

Das Beispiel wird aus 3 Docker Containern bestehen:

  • Container für die eigentliche Anwednung
  • Container für die Überwachung mit Prometheus
  • Container für die Auswertung mittels Grafana

Die folgende Grafik stellt den Aufbau der Docker Container dar:

Docker Container für Go Applikation, Prometheus und Grafana

Docker Container für Go Applikation, Prometheus und Grafana

Die komplette Umgebung soll über docker-compose gestartet werden.

Go Applikation

Die zu überwachende Applikation soll recht einfach aufgebaut sein. Es handelt sich um einen HTTP Web-Service, der einen einfachen String liefert. Genutzt wird die Go Standardbibliothek.

package main

import (
	"fmt"
	"log"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hi there!")
}

func main() {

	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":8080", nil))

}

Nach dem Start mittels go run main.go kann unter http://localhost:8080 das Ergebnis betrachtet werden.

Da später noch weitere Abhängikgeiten hinzugefügt werden sollen wird in diesem Schritt direkt eine Go-Modul Definition für das Beispiel erstellt. Dies geschieht über go mod init github.com/kkoehler/golang/prometheus und resultuiert in einer go.mod sowie einer go.sum Datei mit Meta-Informationen zum Modul.

In obigen Beispiel ist github.com/kkoehler/golang/prometheus der Modulname - Vorteil der Moduldateien ist natürlich auch, dass die Anwendung außerhalb des GO_PATH gebaut und ausgeführt werden kann.

Docker Image für Golang

Wie bereits oben dargestellt soll die Anwendung und deren Überwachung über Docker ausgeführt werden. Es wird als auch ein Docker Container für die Anwendung benötigt. Im Beispiel nutzen wir das Standard Go Dockerfile als Ausgangsbasis (hier Version 1.12.0).

Das Dockerfile liegt im Root Verzeichnis der Anwendung.

from golang:1.12.0

RUN mkdir /application
ADD . /application
WORKDIR /application
RUN go build -o application .

CMD ["/application/application"]

Die Anwendung lässt sich nun über folgende Kommandos starten:

docker build -t gotest .
docker run --rm -p 8080:8080 gotest

Instrumentierung der Anwendung

Nachdem die Grundlagen der Anwendung geschaffen wurden, kann die eigentliche Instrumentierung erfolgen.

Prometheus bietet zwei Client-Bibliotheken an, die in eigene Projekte eingebunden werden können. Es handelt sich um zwei grundsätzlich verschiedene Bibliotheken. Die eine dient dem Aufruf der HTTP API Schnittstelle von Prometheus, die andere ist für die Instrumentierung eigener Anwendungen gedacht.

In diesem Beispiel wird die zweite Variante zur Instrumentierung genutzt. Sie kann über folgende Kommandos installiert werden.

go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promauto
go get github.com/prometheus/client_golang/prometheus/promhttp

Der einfachste Anwendungsfall besteht darin die Go Runtime selbst zu überwachen. Das lässt sich mit der Prometheus Bibliothek sehr einfach abbilden. Prometheus bietet direkt einen eigenen Handler, der direkt über http.Handle("/metrics", promhttp.Handler()) eingebunden werden kann.

In einem kompletten Beispiel sieht der Beispiel-Code nun so aus:

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/prometheus/client_golang/prometheus/promhttp"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hi there!")
}

func main() {

	http.HandleFunc("/", handler)
	http.Handle("/metrics", promhttp.Handler())
	log.Fatal(http.ListenAndServe(":8080", nil))

}

Unter http://localhost:8080/metrics lassen sich nun Metriken zur Go Runtime (und Prometheus) abfragen. Eine Beispielabfrage sieht wie folgt aus:

# HELP go_gc_duration_seconds A summary of the GC invocation durations.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
...

Bei der Ausgabe handlet es handelt sich um das Standard-Ausgabeformat von Prometheus, das ohne Probleme wieder verarbeitet werden kann.

Docker für Prometheus

Der Docker Container für Prometheus ist ebenfalls schnell erstellt, da es bereits Images bzw. Dockerfiles von Prometheus selbst gibt. Hier soll die Version prom/prometheus:v2.8.0 genutzt werden.

Jetzt fehlt noch die Prometheus Scrape Konfiguration, die die Metriken von der Applikation periodisch abruft und dann in der TSDB speichert. Diese kann z. B. als Docker Volume Mapping über den Parameter -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml mitgegeben werden.

Im Beispiel wird allerdings ein separates Image mit einer passenden Konfiguration erstellt. Im Dockerfile wird dementsprechend die Konfigurationsdatei mitgegeben. Mehr muss nicht gemacht werden.

Im Produktivbetrieb sollte man natürlich das Datenverzeichnis “nach außen” mappen, damit es bei Neustarts des Containers nicht gelöscht wird!

from prom/prometheus:v2.8.0

ADD prometheus.yml /etc/prometheus/

Die prometheus.yml liegt lokal und enthält eine Konfiguration, die alle 5 Sekunden die Adresse http://application:8080/metrics aufruft um dort die aktuellen Werte zu erhalten.

Die Adresse http://application:8080 ist aktuell noch nicht erreichbar. Das wird sich mit der weiter unten beschriebenen Docker-Compose Konfiguration ändern.

global:
  scrape_interval:     15s
  external_labels:
    monitor: 'codelab-monitor'

scrape_configs:
  - job_name: 'application'
    scrape_interval: 5s
    static_configs:
      - targets: ['application:8080']

Docker für Grafana

Auch die Ausführung des Grafana Servers soll innerhalb eines Docker Images erfolgen. Hierzu wird wieder ein vorgefertigtes Images, diesmal von Grafana, benutzt. Das Dockerfile des Beispiels ist einfach aufgebaut:

from grafana/grafana:6.0.2

Das Grafana Backend ist übrigens auch in Go implementiert.

Docker Compose hält alles zusammen

Nachdem die Einzelteile (die drei Docker Images) vorbereitet sind, kann die komplette Umgebung gestartet werden. Hierzu wird eine docker-compose Datei mit folgendem Inhalt benötigt:

version: '2'
services:
  prometheus:
    build: prometheus
    ports:
      - "9090:9090"
  grafana:
    build: grafana
    ports:
      - "3000:3000"
  application:
    build: application
    ports:
      - "8080:8080"

Gestartet wird die Umgebung über:

docker-compose up --build

Prometheus testen

Wenn alle drei Container erfolgreich gestartet wurden kann in der Prometheus GUI unter http://localhost:9090 nach einer Metrik gesucht werden. So kann man z. B. die Garbage Collector Laufzeiten anschauen: go_gc_duration_seconds_sum.

Die Metriken stammen von der Go Runtime der Anwendung. Importiert werden sie über den Scrape-Job, der in der prometheus.yml angegeben wurde.

Prometheus Web GUI

Prometheus Web GUI

Grafana einrichten

Um das ganze noch grafisch darzustellen kann die Grafana GUI geöffnet werden. Diese befindet sich unter http://localhost:3000.

Beim ersten Aufruf muss das Passwort geändert werden. Startbenutzername und Passwort ist admin/admin. Im zweiten Schritt muss in Grafana eine neue DataSource eingerichtet werden. Prometheus wird ohne Umwege unterstützt.

Über die Grafana Dialoge lässt sich recht einfach eine neue DataSource anlegen (siehe Screenshot). Als Http-Url muss im Beispiel http://prometheus:9090 angegeben werden. Dies entspricht dem Namen des Containers in der docker-compose Datei.

Mit Grafana Prometheus anbinden

Mit Grafana Prometheus anbinden

Nach dem Speichern sollte die Meldung Data source is working erscheinen.

Dashboards

Im letzten Schritt können nun Dashboards zur Visualisierung der Werte aufgebaut werden. Um das Beispiel mit der Garbage Collection nocheinmal aufzunehmen kann hier folgender Query-String in das Dashboard eingetragen werden. Wie im Screenshot zu sehen werden direkt Werte angezeigt.

go_gc_duration_seconds_sum

Der Aufbau schöner und aussagekräftige Dashboards kann an vielen Stellen im Internet gefunden werden. Hier geht es um ein “Big Picture”.

Grafana Dashboard für Go Runtime

Grafana Dashboard für Go Runtime

Beispiel auf Github

Das komplette Beispiel kann auch in GitHub gefunden werden.

Eigene Metriken aufnehmen

Im bisherigen Beispiel wurde “nur” die Go Runtime mittels Prometheus überwacht. Möchte man auch eigene Messpunkte aufnehmen lässt sich das auch recht einfach über die Prometheus Bibliothek erreichen.

Der Aufruf von promauto.NewCounter erzeugt z. B. einen neuen Counter, den man dann aufrufen kann um z. B. die Anzahl der Aufrufe einer Funktion zu überwachen. Hier ein “globaler” Counter:

var (
	opsProcessed = promauto.NewCounter(prometheus.CounterOpts{
		Name: "myapp_processed_ops_total",
		Help: "The total number of processed events",
	})
)

In der Folge führt der Aufruf von opsProcessed.Inc() zu einer Erhöhung des Wertes des Counters.

Aus dem Beispiel der Prometheus Doku ist folgender Code-Schnippsel übernommen, der alle 2 Sekunden den Wert automatisch erhöht.

func generateDummyCalls() {
	go func() {
		for {
			opsProcessed.Inc()
			time.Sleep(2 * time.Second)
		}
	}()
}

In die Anwendung eingebaut sieht das wie folgt aus:

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

func generateDummyCalls() {
	go func() {
		for {
			opsProcessed.Inc()
			time.Sleep(2 * time.Second)
		}
	}()
}

var (
	opsProcessed = promauto.NewCounter(prometheus.CounterOpts{
		Name: "myapp_processed_ops_total",
		Help: "The total number of processed events",
	})
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hi there")
}

func main() {

	generateDummyCalls()

	fmt.Println("Starting server...")
	http.HandleFunc("/", handler)
	http.Handle("/metrics", promhttp.Handler())
	log.Fatal(http.ListenAndServe(":8080", nil))

}

Natürlich lässt sich auch hierfür ein Grafana Dashboard dazu anlegen. ;-)

Mehr?

Bis hierher war es ein Einstieg in die Überwachung von Go Anwednungen mit Prometheus. An dieser Stelle sei allerdings auch auf die offizielle Dokumentation verwiesen.

Viel Erfolg!

25.03.2019

 

Kennen Sie schon das Buch zum Thema?

Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.

  • Von den Sprachgrundlagen bis zur Qualitätssicherung
  • Architekturstil verstehen und direkt anwenden
  • Idiomatic Go, gRPC, Go Cloud Development Kit
  • Cloud-native Anwendungen erstellen
Microservices mit Go Buch

zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)

Kontakt

Source Fellows GmbH

Source Fellows GmbH Logo

Lerchenstraße 31

72762 Reutlingen

Telefon: (0049) 07121 6969 802

E-Mail: info@source-fellows.com