Kapitel 4. Exposition

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

In Kapitel 3 haben wir uns hauptsächlich darauf konzentriert, deinen Code zu instrumentieren. Aber alle Instrumentierung der Welt nützt nichts, wenn die erzeugten Metriken nicht in deinem Überwachungssystem landen. Der Prozess, bei dem Metriken für Prometheus verfügbar gemacht werden, wird als Exposition bezeichnet.

Die Übergabe an Prometheus erfolgt über HTTP. Normalerweise stellst du Metriken unter dem Pfad /metrics zur Verfügung und die Anfrage wird von einer Client-Bibliothek für dich bearbeitet. Prometheus unterstützt zwei für Menschen lesbare Textformate: das Prometheus-Textformat und OpenMetrics. Du hast die Möglichkeit, das Expositionsformat von Hand zu erstellen. In diesem Fall ist es einfacher, das Prometheus-Textformat zu verwenden, das weniger streng ist. Das ist einfacher, wenn du das Prometheus-Textformat verwendest, das weniger streng ist. Du kannst das auch tun, wenn es keine geeignete Bibliothek für deine Sprache gibt, aber es ist empfehlenswert, eine Bibliothek zu verwenden, da sie alle kleinen Details wie das Escaping richtig hinbekommt. Die meisten Bibliotheken bieten auch die Möglichkeit, Metriken sowohl im OpenMetrics- als auch im Prometheus-Textformat zu erstellen.

Die Exposition erfolgt in der Regel entweder in deiner Hauptfunktion oder einer anderen Top-Level-Funktion und muss nur einmal pro Anwendung konfiguriert werden.

Metriken werden normalerweise in der Standardregistrierung registriert, wenn du sie definierst. Wenn eine der Bibliotheken, von denen du abhängst, über Prometheus-Instrumente verfügt, werden die Metriken in der Standardregistrierung eingetragen und du profitierst von dieser zusätzlichen Instrumentierung, ohne etwas tun zu müssen. Manche Benutzer ziehen es vor, die Registry explizit von der Hauptfunktion aus weiterzugeben. Du müsstest dich also darauf verlassen, dass jede Bibliothek zwischen der Hauptfunktion deiner Anwendung und der Prometheus-Instrumentierung von der Instrumentierung weiß. Das setzt voraus, dass sich jede Bibliothek in der Abhängigkeitskette um die Instrumentierung kümmert und sich über die Wahl derInstrumentierungsbibliotheken einig ist.

Dieses Design ermöglicht die Instrumentierung für Prometheus-Metriken ohne jegliche Exposition.1 Abgesehen von den (geringen) Ressourcenkosten für die Instrumentierung gibt es in diesem Fall keine Auswirkungen auf deine Anwendung. Wenn du eine Bibliothek schreibst, kannst du die Instrumentierung für deine Benutzer mit Prometheus hinzufügen, ohne dass deine Benutzer, die nicht überwachen, zusätzlichen Aufwand haben. Um diesen Anwendungsfall besser zu unterstützen, versuchen die Instrumentierungsteile der Client-Bibliotheken, ihre Abhängigkeiten zu minimieren.

Werfen wir einen Blick auf die Darstellung in einigen der gängigen Client-Bibliotheken. Wir gehen hier davon aus, dass du weißt, wie du die Client-Bibliotheken und alle anderen erforderlichen Abhängigkeiten installierst.

Python

Du hast start_http_server bereits in Kapitel 3 kennengelernt. Er startet einen Hintergrund-Thread mit einem HTTP-Server, der nur Prometheus-Metriken liefert, wie folgt:

from prometheus_client import start_http_server

if __name__ == '__main__':
    start_http_server(8000)
    // Your code goes here.

start_http_server ist sehr praktisch, um schnell loslegen zu können. Wahrscheinlich hast du aber bereits einen HTTP-Server in deiner Anwendung, von dem du deine Metriken abrufen möchtest.

In Python gibt es verschiedene Möglichkeiten, dies zu tun, je nachdem, welche Frameworks du verwendest.

WSGI

Web Server Gateway Interface (WSGI) ist ein Python-Standard für Webanwendungen. Der Python-Client bietet eine WSGI-App, die du mit deinem bestehenden WSGI-Code verwenden kannst. In Beispiel 4-1 wird metrics_app von my_app delegiert, wenn der Pfad /metrics angefordert wird; andernfalls führt sie ihre übliche Logik aus. Durch die Verkettung von WSGI-Anwendungen kannst du Middleware wie z. B. Authentifizierung hinzufügen, die Client-Bibliotheken nicht von Haus aus bieten.

Beispiel 4-1. Exposition mit WSGI in Python
from prometheus_client import make_wsgi_app
from wsgiref.simple_server import make_server

metrics_app = make_wsgi_app()

def my_app(environ, start_fn):
    if environ['PATH_INFO'] == '/metrics':
        return metrics_app(environ, start_fn)
    start_fn('200 OK', [])
    return [b'Hello World']

if __name__ == '__main__':
    httpd = make_server('', 8000, my_app)
    httpd.serve_forever()

Verdreht

Twisted ist eine ereignisgesteuerte Python-Netzwerk-Engine. Sie unterstützt WSGI, sodass du make_wsgi_app einbinden kannst, wie in Beispiel 4-2 gezeigt.

Beispiel 4-2. Exposition mit Twisted in Python
from prometheus_client import make_wsgi_app
from twisted.web.server import Site
from twisted.web.wsgi import WSGIResource
from twisted.web.resource import Resource
from twisted.internet import reactor

metrics_resource = WSGIResource(
        reactor, reactor.getThreadPool(), make_wsgi_app())

class HelloWorld(Resource):
      isLeaf = False
      def render_GET(self, request):
          return b"Hello World"

root = HelloWorld()
root.putChild(b'metrics', metrics_resource)

reactor.listenTCP(8000, Site(root))
reactor.run()

Multiprozess mit Gunicorn

Prometheus geht davon aus, dass die Anwendungen, die es überwacht, langlebig und multithreaded sind. Aber das kann bei Laufzeiten wie CPython ein wenig in die Hose gehen.2 CPython ist aufgrund des Global Interpreter Lock (GIL) effektiv auf einen Prozessorkern beschränkt. Um dies zu umgehen, verteilen manche Nutzer die Arbeitslast mit einem Tool wie Gunicorn auf mehrere Prozesse.

Wenn du die Python-Client-Bibliothek auf die übliche Weise verwenden würdest, würde jeder Worker seine eigenen Metriken verfolgen. Jedes Mal, wenn Prometheus die Anwendung scrapen würde, würde es zufällig die Metriken von nur einem der Worker abrufen, was nur einen Bruchteil der Informationen ausmachen würde und außerdem zu Problemen führen würde, z. B. dass die Zähler scheinbar rückwärts laufen. Worker können auch relativ kurzlebig sein.

Die Lösung für dieses Problem, die der Python-Client bietet, ist, dass jeder Worker seine eigenen Metriken verfolgt. Zum Zeitpunkt der Veröffentlichung werden alle Metriken aller Worker so kombiniert, dass die Semantik einer Multithreading-Anwendung entsteht. Es gibt einige Einschränkungen bei diesem Ansatz: Die Metriken vonprocess_ und die benutzerdefinierten Collectors werden nicht angezeigt, und das Pushgateway kann nicht verwendet werden.3

Wenn du Gunicorn verwendest, musst du der Client-Bibliothek mitteilen, wenn ein Worker-Prozess beendet wird.4 Das machst du in einer Konfigurationsdatei wie in Beispiel 4-3.

Beispiel 4-3. Gunicorn config.py zum Umgang mit dem Beenden von Arbeitsprozessen
from prometheus_client import multiprocess

def child_exit(server, worker):
    multiprocess.mark_process_dead(worker.pid)

Du brauchst auch eine Anwendung, die die Metriken liefert. Gunicorn verwendet WSGI, du kannst alsomake_wsgi_app verwenden. Du musst eine benutzerdefinierte Registry erstellen, die nur eineMultiProcessCollector für die Darstellung enthält, damit sie nicht sowohl die Multiprozess-Metriken als auch die Metriken aus der lokalen Standard-Registry enthält(Beispiel 4-4).

Beispiel 4-4. Gunicorn-Anwendung in app.py
from prometheus_client import multiprocess, make_wsgi_app, CollectorRegistry
from prometheus_client import Counter, Gauge

REQUESTS = Counter("http_requests_total", "HTTP requests")
IN_PROGRESS = Gauge("http_requests_inprogress", "Inprogress HTTP requests",
        multiprocess_mode='livesum')

@IN_PROGRESS.track_inprogress()
def app(environ, start_fn):
    REQUESTS.inc()
    if environ['PATH_INFO'] == '/metrics':
        registry = CollectorRegistry()
        multiprocess.MultiProcessCollector(registry)
        metrics_app = make_wsgi_app(registry)
        return metrics_app(environ, start_fn)
    start_fn('200 OK', [])
    return [b'Hello World']

Wie du in Beispiel 4-4 sehen kannst, funktionieren Zähler normal, ebenso wie Zusammenfassungen und Histogramme. Für Messgeräte gibt es eine zusätzliche optionale Konfiguration mit multiprocess_mode. Du kannst das Messgerät wie folgt konfigurieren, je nachdem, wie du es verwenden möchtest:

all

Die Standardeinstellung, die von jedem Prozess eine Zeitreihe zurückgibt, unabhängig davon, ob er lebendig oder tot ist. So kannst du die Reihen in PromQL nach Belieben aggregieren. Sie werden durch ein pid Label unterschieden.

liveall

Gibt eine Zeitreihe von jedem lebenden Prozess zurück.

livesum

Gibt eine einzelne Zeitreihe zurück, die die Summe der Werte jedes aktiven Prozesses ist. Du würdest dies für Dinge wie laufende Anfragen oder Ressourcennutzung über alle Prozesse hinweg verwenden. Ein Prozess könnte mit einem Wert ungleich Null abgebrochen worden sein, daher werden tote Prozesse ausgeschlossen.

max

Gibt eine einzelne Zeitreihe zurück, die das Maximum des Wertes jedes lebenden oder toten Prozesses ist. Das ist nützlich, wenn du das letzte Mal verfolgen willst, wann etwas passiert ist, z. B. die Bearbeitung einer Anfrage, die in einem Prozess gewesen sein könnte, der jetzt tot ist.

min

Gibt eine einzelne Zeitreihe zurück, die das Minimum des Wertes von jedem lebenden oder toten Prozess ist.

Bevor du Gunicorn ausführen kannst, musst du einige Einstellungen vornehmen, wie inBeispiel 4-5 gezeigt. Du musst eine Umgebungsvariable namensprometheus_multiproc_dir setzen. Sie verweist auf ein leeres Verzeichnis, das die Client-Bibliothek für das Tracking von Metriken verwendet. Bevor du die Anwendung startest, solltest du dieses Verzeichnis immer löschen, um eventuelle Änderungen an deiner Instrumentierung zu berücksichtigen.

Beispiel 4-5. Vorbereiten der Umgebung vor dem Start von Gunicorn mit zwei Arbeitern
hostname $ export prometheus_multiproc_dir=$PWD/multiproc
hostname $ rm -rf $prometheus_multiproc_dir
hostname $ mkdir -p $prometheus_multiproc_dir
hostname $ gunicorn -w 2 -c config.py app:app
[2018-01-07 19:05:30 +0000] [9634] [INFO] Starting gunicorn 19.7.1
[2018-01-07 19:05:30 +0000] [9634] [INFO] Listening at: http://127.0.0.1:8000 (9634)
[2018-01-07 19:05:30 +0000] [9634] [INFO] Using worker: sync
[2018-01-07 19:05:30 +0000] [9639] [INFO] Booting worker with pid: 9639
[2018-01-07 19:05:30 +0000] [9640] [INFO] Booting worker with pid: 9640

Wenn du dir den Pfad /metrics ansiehst, siehst du die beiden definierten Metriken, aberpython_info und die process_ Metriken sind nicht dabei.

Tipp

Jeder Prozess erstellt mehrere Dateien, die zur Expositionszeit inprometheus_multiproc_dir gelesen werden müssen. Wenn deine Worker häufig stoppen und starten, kann dies die Exposition bei tausenden von Dateien verlangsamen.

Es ist nicht sicher, einzelne Dateien zu löschen, da dies dazu führen könnte, dass die Zähler fälschlicherweise rückwärts laufen, aber du kannst entweder versuchen, den Churn zu reduzieren (z. B. indem du die Anzahl der Anfragen, die Worker vor dem Beenden bearbeiten, erhöhst oder entfernst5), oder indem du die Anwendung regelmäßig neu startest und die Dateien löschst.

Diese Schritte gelten für Gunicorn. Der gleiche Ansatz funktioniert auch mit anderen Python-Multiprozess-Konfigurationen, zum Beispiel mit dem Modul multiprocessing.

Geh

In Go ist http.Handler die Standardschnittstelle für die Bereitstellung von HTTP-Handlern, undpromhttp.Handler bietet diese Schnittstelle für die Go-Client-Bibliothek. Um zu zeigen, wie das funktioniert, füge den Code in Beispiel 4-6 in eine Datei namens example.go ein.

Beispiel 4-6. Ein einfaches Go-Programm zur Demonstration von Instrumentierung und Exposition
package main

import (
  "log"
  "net/http"

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

var (
  requests = promauto.NewCounter(
    prometheus.CounterOpts{
      Name: "hello_worlds_total",
      Help: "Hello Worlds requested.",
    })
)

func handler(w http.ResponseWriter, r *http.Request) {
  requests.Inc()
  w.Write([]byte("Hello World"))
}

func main() {
  http.HandleFunc("/", handler)
  http.Handle("/metrics", promhttp.Handler())
  log.Fatal(http.ListenAndServe(":8000", nil))
}

Du kannst die Abhängigkeiten abrufen und diesen Code auf die übliche Weise ausführen:

hostname $ go get -d -u github.com/prometheus/client_golang/prometheus
hostname $ go run example.go

In diesem Beispiel wird promauto verwendet, das deine Metrik automatisch in der Standardregistrierung registriert. Wenn du das nicht möchtest, kannst du stattdessenprometheus.NewCounter verwenden und dann MustRegister in einer initFunktion einsetzen:

func init() {
  prometheus.MustRegister(requests)
}

Das ist etwas anfälliger, denn du kannst die Metrik leicht erstellen und verwenden, aber den Aufruf von MustRegister vergessen.

Java

Die Java-Client-Bibliothek ist auch als Simpleclient bekannt. Sie hat den ursprünglichen Client ersetzt, der entwickelt wurde, bevor viele der aktuellen Praktiken und Richtlinien für das Schreiben einer Client-Bibliothek festgelegt wurden. Der Java-Client sollte für die Instrumentierung von Sprachen verwendet werden, die auf einer Java Virtual Machine (JVM) laufen.

HTTPServer

Ähnlich wie start_http_server in Python bietet dir die Klasse HTTPServer im Java-Client einen einfachen Weg, um loszulegen(Beispiel 4-7).

Beispiel 4-7. Ein einfaches Java-Programm zur Demonstration von Instrumentierung und Exposition
import io.prometheus.client.Counter;
import io.prometheus.client.hotspot.DefaultExports;
import io.prometheus.client.exporter.HTTPServer;

public class Example {
  private static final Counter myCounter = Counter.build()
      .name("my_counter_total")
      .help("An example counter.").register();

  public static void main(String[] args) throws Exception {
    DefaultExports.initialize();
    HTTPServer server = new HTTPServer(8000);
    while (true) {
      myCounter.inc();
      Thread.sleep(1000);
    }
  }
}

Du solltest Java-Metriken in der Regel als statische Klassenfelder haben, damit sie nur einmal registriert werden.

Der Aufruf von DefaultExports.initialize wird benötigt, damit die verschiedenen Metriken process undjvm funktionieren. In der Regel solltest du ihn in allen deinen Java-Anwendungen einmal aufrufen, z. B. in der Hauptfunktion. DefaultExports.initialize ist jedoch idempotent und thread-sicher, sodass zusätzliche Aufrufe unbedenklich sind.

Um den Code in Beispiel 4-7 ausführen zu können, brauchst du die Simpleclient-Abhängigkeiten. Wenn du Maven verwendest, sollte die dependencies in deiner pom.xmlwie inBeispiel 4-8 aussehen.

Beispiel 4-8. pom.xml-Abhängigkeiten für Beispiel 4-7
  <dependencies>
    <dependency>
      <groupId>io.prometheus</groupId>
      <artifactId>simpleclient</artifactId>
      <version>0.16.0</version>
    </dependency>
    <dependency>
      <groupId>io.prometheus</groupId>
      <artifactId>simpleclient_hotspot</artifactId>
      <version>0.16.0</version>
    </dependency>
    <dependency>
      <groupId>io.prometheus</groupId>
      <artifactId>simpleclient_httpserver</artifactId>
      <version>0.16.0</version>
    </dependency>
  </dependencies>

Servlet

Viele Java- und JVM-Frameworks unterstützen die Verwendung von Unterklassen von HttpServlet in ihren HTTP-Servern und Middleware. Jetty ist ein solcher Server. In Beispiel 4-9 siehst du, wie du die MetricsServlet des Java-Clients verwenden kannst.

Beispiel 4-9. Ein Java-Programm zur Demonstration der Darstellung mit MetricsServlet und Jetty
import io.prometheus.client.Counter;
import io.prometheus.client.exporter.MetricsServlet;
import io.prometheus.client.hotspot.DefaultExports;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import java.io.IOException;


public class Example {
  static class ExampleServlet extends HttpServlet {
    private static final Counter requests = Counter.build()
        .name("hello_worlds_total")
        .help("Hello Worlds requested.").register();

    @Override
    protected void doGet(final HttpServletRequest req,
        final HttpServletResponse resp)
        throws ServletException, IOException {
      requests.inc();
      resp.getWriter().println("Hello World");
    }
  }

  public static void main(String[] args) throws Exception {
      DefaultExports.initialize();

      Server server = new Server(8000);
      ServletContextHandler context = new ServletContextHandler();
      context.setContextPath("/");
      server.setHandler(context);
      context.addServlet(new ServletHolder(new ExampleServlet()), "/");
      context.addServlet(new ServletHolder(new MetricsServlet()), "/metrics");

      server.start();
      server.join();
  }
}

Du musst auch den Java-Client als Abhängigkeit angeben. Wenn du Maven verwendest, sieht das wie in Beispiel 4-10 aus.

Beispiel 4-10. pom.xml-Abhängigkeiten für Beispiel 4-9
  <dependencies>
    <dependency>
      <groupId>io.prometheus</groupId>
      <artifactId>simpleclient</artifactId>
      <version>0.16.0</version>
    </dependency>
    <dependency>
      <groupId>io.prometheus</groupId>
      <artifactId>simpleclient_hotspot</artifactId>
      <version>0.16.0</version>
    </dependency>
    <dependency>
      <groupId>io.prometheus</groupId>
      <artifactId>simpleclient_servlet</artifactId>
      <version>0.16.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-servlet</artifactId>
      <version>11.0.11</version>
    </dependency>
  </dependencies>

Pushgateway

Batch-Aufträge werden in der Regel nach einem regelmäßigen Zeitplan ausgeführt, z. B. stündlich oder täglich. Sie werden gestartet, erledigen einige Aufgaben und werden dann beendet. Da sie nicht ununterbrochen laufen, kann Prometheus sie nicht wirklich auslesen.6 An dieser Stelle kommt dasPushgateway ins Spiel.

Das Pushgateway7 ist ein Metrik-Cache für Batch-Aufträge auf Service-Ebene. Seine Architektur ist in Abbildung 4-1 dargestellt. Es erinnert sich nur an den letzten Push, den du ihm für jeden Batch-Job gibst. Du verwendest es, indem du deine Batch-Jobs ihre Metriken pushen lässt, kurz bevor sie beendet werden. Prometheus holt diese Metriken von deinem Pushgateway ab und du kannst sie dann als Alert anzeigen und grafisch darstellen. Normalerweise läuft ein Pushgateway neben einem Prometheus.

Pushgateway architecture diagram
Abbildung 4-1. Die Pushgateway-Architektur

Ein Batch-Job auf Service-Ebene ist ein Job, der nicht wirklich mit einem instance Label versehen werden kann. Das heißt, er gilt für alle deine Dienste und ist nicht von vornherein an eine Maschine oder eine Prozessinstanz gebunden.8 Wenn es dir egal ist, wo ein Batch-Job ausgeführt wird, du aber Wert darauf legst, dass er ausgeführt wird (auch wenn er derzeit per Cron auf einem Rechner läuft), handelt es sich um einen Batch-Job auf Dienstebene. Beispiele dafür sind ein Batch-Job pro Rechenzentrum, der nach fehlerhaften Rechnern sucht, oder ein Batch-Job, der die Speicherbereinigung für einen ganzen Dienst durchführt.

Hinweis

Das Pushgateway ist keine Möglichkeit, Prometheus von Pull auf Push umzustellen. Wenn zum Beispiel mehrere Pushs zwischen einem Prometheus-Scrape und dem nächsten liegen, gibt das Pushgateway nur den letzten Push für diesen Batch-Job zurück. Dies wird in "Netzwerke und Authentifizierung" näher erläutert .

Du kannst Pushgateway von der Prometheus Download-Seite herunterladen. Es ist ein Exporter, der standardmäßig auf Port 9091 läuft, und Prometheus sollte so eingestellt sein, dass es scrapen kann. Du solltest jedoch auch die Einstellung honor_labels: true in der Scrape-Konfiguration angeben, wie in Beispiel 4-11 gezeigt. Der Grund dafür ist, dass die Metriken, die du an das Pushgateway sendest, kein instanceLabel haben sollten und du nicht willst, dass das instance Ziel-Label des Pushgateways auf den Metriken landet, wenn Prometheus sie scrapt.9 honor_labels wird in"Label-Kollisionen und honor_labels" besprochen.

Beispiel 4-11. prometheus.yml scrape Konfiguration für ein lokales Pushgateway
scrape_configs:
 - job_name: pushgateway
   honor_labels: true
   static_configs:
    - targets:
      - localhost:9091

Du kannst Client-Bibliotheken verwenden, um an das Pushgateway zu pushen. Beispiel 4-12 zeigt die Struktur, die du für einen Python-Batch-Job verwenden würdest. Es wird eine benutzerdefinierte Registry erstellt, damit nur die von dir ausgewählten Metriken gepusht werden. Die Dauer des Batch-Jobs wird immer gepusht,10 und die Zeit, zu der er beendet wurde, wird nur übertragen, wenn der Auftrag erfolgreich war.

Es gibt drei verschiedene Möglichkeiten, wie du an das Pushgateway schreiben kannst. In Python sind das die Funktionen push_to_gateway, pushadd_to_gateway und delete_from_gateway:

push

Alle vorhandenen Metriken für diesen Auftrag werden entfernt und die gepushten Metriken hinzugefügt. Dies geschieht mit der HTTP-Methode PUT unter der Haube.

pushadd

Die gepushten Metriken überschreiben bestehende Metriken mit denselben Metriknamen für diesen Auftrag.Alle Metriken, die zuvor mit anderen Metriknamen existierten, bleiben unverändert. Dabei wird die HTTP-Methode POST unter dem Deckmantel verwendet.

delete

Die Metriken für diesen Auftrag werden entfernt. Dabei wird die HTTP-Methode DELETE verwendet.

Da in Beispiel 4-12 pushadd_to_gateway verwendet wird, wird der Wert von my_job_duration_seconds immer ersetzt. Der Wert vonmy_job_last_success_seconds# wird jedoch nur ersetzt, wenn es keine Ausnahmen gibt; er wird der Registrierung hinzugefügt und dann verschoben.

Beispiel 4-12. Instrumentierung eines Batch-Jobs und Übermittlung seiner Metriken an ein Pushgateway
from prometheus_client import CollectorRegistry, Gauge, pushadd_to_gateway

registry = CollectorRegistry()
duration = Gauge('my_job_duration_seconds',
        'Duration of my batch job in seconds', registry=registry)
try:
    with duration.time():
        # Your code here. 
        pass

    # This only runs if there wasn't an exception. 
    g = Gauge('my_job_last_success_seconds',
            'Last time my batch job successfully finished', registry=registry)
    g.set_to_current_time()
finally:
    pushadd_to_gateway('localhost:9091', job='batch', registry=registry)

Du kannst gepushte Daten auf der Statusseite sehen, wie Abbildung 4-2 zeigt. Eine zusätzliche Metrik push_time_seconds wurde vom Pushgateway hinzugefügt, weil Prometheus immer die Zeit, zu der es scrapt, als Zeitstempel der Pushgateway-Metriken verwendet. push_time_seconds gibt dir die Möglichkeit, den tatsächlichen Zeitpunkt zu erfahren, zu dem die Daten zuletzt gepusht wurden. Mit push_failure_time_seconds wurde eine weitere Metrik eingeführt, die den Zeitpunkt angibt, an dem eine Aktualisierung dieser Gruppe im Pushgateway zuletzt fehlgeschlagen ist.

Pushgateway Status page.
Abbildung 4-2. Die Pushgateway-Statusseite mit den Metriken eines Pushs

Du hast vielleicht in Abbildung 4-2 gesehen, dass der Push als Gruppe bezeichnet wird. Neben dem Label job kannst du beim Pushen weitere Labels angeben, die alle als Gruppierungsschlüssel bezeichnet werden. In Python kann dies mit dem Schlüsselwortargument grouping_key angegeben werden. Du würdest dies verwenden, wenn ein Batch-Auftrag gesplittet oder irgendwie aufgeteilt wurde. Wenn du z. B. 30 Datenbank-Shards hast und jeder davon einen eigenen Batch-Auftrag, kannst du sie mit dem Label shard unterscheiden.

Tipp

Einmal gepusht, bleiben die Gruppen für immer im Pushgateway. Du solltest es vermeiden, Gruppierungsschlüssel zu verwenden, die sich von einem Batch-Job-Lauf zum nächsten ändern, da dies die Arbeit mit den Metriken erschwert und zu Leistungsproblemen führt. Wenn du einen Batch-Job außer Betrieb nimmst, vergiss nicht, seine Metriken aus dem Pushgateway zu löschen.

Brücken

Die Prometheus-Client-Bibliotheken sind nicht auf die Ausgabe von Metriken im Prometheus-Format beschränkt. Es gibt eine Trennung zwischen Instrumentierung und Darstellung, sodass du die Metriken auf jede beliebige Weise verarbeiten kannst.

Die Go-, Python- und Java-Clients enthalten zum Beispiel jeweils eine Graphite-Bridge. Eine Bridge nimmt die Metrik-Ausgabe aus der Registry der Client-Bibliothek und gibt sie an etwas anderes als Prometheus aus. Die Graphite-Bridge wandelt die Metriken also in eine Form um, die Graphite verstehen kann11 und gibt sie an Graphite aus, wie inBeispiel 4-13 gezeigt.

Beispiel 4-13. Verwendung der Python GraphiteBridge, um alle 10 Sekunden an Graphite zu pushen
import time
from prometheus_client.bridge.graphite import GraphiteBridge

gb = GraphiteBridge(['graphite.your.org', 2003])
gb.start(10)
while True:
    time.sleep(1)

Das funktioniert, weil die Registry eine Methode hat, mit der du einen Schnappschuss aller aktuellen Metriken erhalten kannst. Das ist CollectorRegistry.collect in Python,CollectorRegistry.metricFamilySamples in Java und Registry.Gather in Go. Diese Methode wird in der HTTP-Exposition verwendet und du kannst sie auch nutzen. Du könntest diese Methode zum Beispiel verwenden, um Daten in eine andere, nicht zu Prometheus gehörende Instrumentierungsbibliothek einzuspeisen.12

Tipp

Wenn du dich jemals in die direkte Instrumentierung einklinken willst, solltest du stattdessen die von einer Registry ausgegebenen Metriken verwenden. Jedes Mal zu wissen, wenn ein Zähler erhöht wird, ist für ein metrikbasiertes Überwachungssystem nicht sinnvoll. Die Anzahl der Inkremente wird dir jedoch bereits vonCollectorRegistry.collect zur Verfügung gestellt und funktioniert für benutzerdefinierte Collectors.

Parser

Zusätzlich zur Registry der Client-Bibliothek, die den Zugriff auf die Metrik-Ausgabe ermöglicht, bieten die Go13 und Python-Clients verfügen auch über einen Parser für die Prometheus- und OpenMetrics-Ausgabeformate. Beispiel 4-14 gibt nur die Beispiele aus, aber du kannst die Prometheus-Metriken auch in andere Überwachungssysteme oder in dein lokales Tooling einspeisen.

Beispiel 4-14. Parsen des Prometheus-Textformats mit dem Python-Client
from prometheus_client.parser import text_string_to_metric_families

for family in text_string_to_metric_families(u"counter_total 1.0\n"):
  for sample in family.samples:
    print("Name: {0} Labels: {1} Value: {2}".format(*sample))

DataDog, InfluxDB, Sensu und Metricbeat14 sind einige der Überwachungssysteme, die über Komponenten verfügen, die das Textformat parsen können.Wenn du eines dieser Überwachungssysteme verwendest, kannst du die Vorteile des Prometheus-Ökosystems nutzen, ohne den Prometheus-Server zu starten. Wir glauben, dass dies eine gute Sache ist, da es derzeit viel Doppelarbeit zwischen den verschiedenen Überwachungssystemen gibt. Jedes von ihnen muss ähnlichen Code schreiben, um die unzähligen benutzerdefinierten Metrikausgaben zu unterstützen, die von der am häufigsten verwendeten Software bereitgestellt werden.

Format der Textexposition

Das Prometheus-Textexpositionsformat ist relativ einfach zu erzeugen und zu parsen.Obwohl du dich fast immer auf eine Client-Bibliothek verlassen solltest, die das Format für dich handhabt, gibt es Fälle, wie den Node Exporter Textfile Collector (siehe"Textfile Collector"), in denen du es selbst erzeugen musst.

Wir zeigen dir die Version 0.0.4 des Textformats, das den Inhaltstyp Header hat:

Content-Type: text/plain; version=0.0.4; charset=utf-8

In den einfachsten Fällen besteht das Textformat nur aus dem Namen der Metrik, gefolgt von einer 64-Bit-Gleitkommazahl. Jede Zeile wird mit einem Zeilenvorschubzeichen (\n) abgeschlossen:

my_counter_total 14
a_small_gauge 8.3e-96

Metrische Typen

Eine vollständigere Prometheus-Ausgabe im Textformat würde die HELP und TYPE der Metrik enthalten, wie in Beispiel 4-15 gezeigt. HELP ist eine Beschreibung der Metrik und sollte sich im Allgemeinen nicht von Scrape zu Scrape ändern. TYPE ist eine der Optionencounter, gauge, summary, histogram oder untyped. untyped wird verwendet, wenn du den Typ der Metrik nicht kennst, und ist die Standardeinstellung, wenn kein Typ angegeben wird. Es ist ungültig, wenn du eine doppelte Metrik hast. Achte also darauf, dass alle Zeitreihen, die zu einer Metrik gehören, in einer Gruppe zusammengefasst werden.

Beispiel 4-15. Darstellungsformat für ein Messgerät, einen Zähler, eine Zusammenfassung und ein Histogramm
# HELP example_gauge An example gauge
# TYPE example_gauge gauge
example_gauge -0.7
# HELP my_counter_total An example counter
# TYPE my_counter_total counter
my_counter_total 14
# HELP my_summary An example summary
# TYPE my_summary summary
my_summary_sum 0.6
my_summary_count 19
# HELP latency_seconds An example histogram
# TYPE latency_seconds histogram
latency_seconds_bucket{le="0.1"} 7 1
latency_seconds_bucket{le="0.2"} 18
latency_seconds_bucket{le="0.4"} 24
latency_seconds_bucket{le="0.8"} 28
latency_seconds_bucket{le="+Inf"} 29
latency_seconds_sum 0.6
latency_seconds_count 29 2
1

Bei Histogrammen haben die le Beschriftungen Fließkommawerte und müssen sortiert werden. Du solltest beachten, wie die Histogramm-Eimer kumuliert werden, dennle steht für kleiner oder gleich.

2

Der _count muss mit dem +Inf Bucket übereinstimmen, und der +Inf Bucket muss immer vorhanden sein. Die Buckets sollten sich nicht von Scrape zu Scrape ändern, da dies zu Problemen mit derhistogram_quantile Funktion von PromQL führt.

Etiketten

Das Histogramm im vorangegangenen Beispiel zeigt auch, wie Beschriftungen dargestellt werden. Mehrere Beschriftungen werden durch Kommas getrennt, und es ist in Ordnung, wenn vor der schließenden Klammer ein nachgestelltes Komma steht.

Die Reihenfolge der Labels spielt keine Rolle, aber es ist eine gute Idee, die Reihenfolge von Scrape zu Scrape einheitlich zu gestalten. Das macht das Schreiben deiner Unit-Tests einfacher, und eine konsistente Reihenfolge sorgt für die beste Ingestion-Leistung in Prometheus.

Hier ist ein Beispiel für eine Zusammenfassung im Textformat:

# HELP my_summary An example summary
# TYPE my_summary summary
my_summary_sum{foo="bar",baz="quu"} 1.8
my_summary_count{foo="bar",baz="quu"} 453
my_summary_sum{foo="blaa",baz=""} 0
my_summary_count{foo="blaa",baz="quu"} 0

Es ist möglich, eine Metrik ohne Zeitreihe zu haben, wenn keine Kinder initialisiert wurden, wie in "Kind" beschrieben :

# HELP a_counter_total An example counter
# TYPE a_counter_total counter

Die Flucht

Das Format der Textexposition ist in UTF-8 kodiert, und das vollständige UTF-815 ist sowohl in HELP als auch in den Label-Werten erlaubt. Daher musst du Backslashes verwenden, um Zeichen zu vermeiden, die bei der Verwendung von Backslashes Probleme verursachen würden. Für HELP sind das Zeilenvorschübe und Backslashes. Bei Labelwerten sind dies Zeilenvorschübe, Backslashes und doppelte Anführungszeichen.16 Das Format ignoriert zusätzliche Leerzeichen.

Hier ist ein Beispiel, das das Escaping im Format der Textexposition zeigt:

# HELP escaping A newline \\n and backslash \\ escaped
# TYPE escaping gauge
escaping{foo="newline \\n backslash \\ double quote \" "} 1

Zeitstempel

Es ist möglich, einen Zeitstempel für eine Zeitreihe anzugeben. Das ist ein ganzzahliger Wert in Millisekunden seit der Unix-Epoche,17 und steht hinter dem Wert. Zeitstempel im Expositionsformat sollten in der Regel vermieden werden, da sie nur in bestimmten Anwendungsfällen (z. B. bei der Föderation) anwendbar sind und mit Einschränkungen verbunden sind. Zeitstempel für Scraps werden in der Regel automatisch von Prometheus angewendet. Es ist nicht definiert, was passiert, wenn du mehrere Zeilen mit demselben Namen und denselben Bezeichnungen, aber unterschiedlichen Zeitstempeln angibst.

Dieses Messgerät hat einen Zeitstempel:

# HELP foo I'm trapped in a client library
# TYPE foo gauge
foo 1 15100992000000
Warnung

Zeitstempel werden im Prometheus-Textformat in Millisekunden seit der Epoche angegeben, während sie in OpenMetrics in Sekunden seit der Epoche angegeben werden.

Metriken prüfen

Prometheus 2.0 verwendet aus Effizienzgründen einen eigenen Parser. Nur weil ein /metrics-Endpunkt gecrappt werden kann, heißt das nicht, dass die Metriken mit dem Format konform sind.

Promtool ist ein Dienstprogramm, das in Prometheus enthalten ist und unter anderem überprüfen kann, ob deine Metrik-Ausgabe gültig ist und Lint-Checks durchführt:

curl http://localhost:8000/metrics | promtool check metrics

Häufige Fehler sind das Vergessen des Zeilenvorschubs in der letzten Zeile, die Verwendung von Wagenrücklauf und Zeilenvorschub statt nur Zeilenvorschub,18 und ungültige Metrik- oder Labelnamen. Zur Erinnerung: Metrik- und Labelnamen dürfen keine Bindestriche enthalten und nicht mit einer Zahl beginnen.

Du kennst dich jetzt mit dem Textformat aus. Die vollständige Spezifikation findest du in der offiziellen Prometheus-Dokumentation.

OpenMetrics

Das OpenMetrics-Format ähnelt dem Prometheus-Textformat, enthält aber einige inkompatible Änderungen gegenüber dem Prometheus-Textformat. Auch wenn sie ähnlich aussehen, ist die Ausgabe für eine bestimmte Menge von Metriken in der Regel unterschiedlich.

Wir zeigen dir die Version 1.0.0 des OpenMetrics-Formats, das den Content-Type-Header hat:

Content-Type: application/openmetrics-text; version=1.0.0; charset=utf-8

In den einfachsten Fällen besteht das Textformat nur aus dem Namen der Metrik, gefolgt von einer 64-Bit-Gleitkommazahl. Jede Zeile wird mit einem Zeilenvorschubzeichen (\n) abgeschlossen. Die Datei wird mit # EOF abgeschlossen:

my_counter_total 14
a_small_gauge 8.3e-96
# EOF

Metrische Typen

Die vom Prometheus-Textformat unterstützten Metrik-Typen werden auch in OpenMetrics unterstützt. Zusätzlich zu den Zählern, Messgeräten, Zusammenfassungen und Histogrammen wurden spezielle Typen hinzugefügt: StateSet, GaugeHistograms und Info.

StateSets stellen eine Reihe von zusammenhängenden booleschen Werten dar, die auch als Bitset bezeichnet werden. Ein Wert von 1 bedeutet true und 0 bedeutet false.

GaugeHistograms messen aktuelle Verteilungen. Der Unterschied zu Histogrammen ist, dass die Werte und die Summe der Eimer nach oben und unten gehen können.

Info-Metriken werden verwendet, um Textinformationen offenzulegen, die sich während der Prozessdauer nicht ändern. Die Version einer Anwendung, ein Commit der Versionskontrolle und die Version eines Compilers sind gute Kandidaten. Der Wert dieser Metriken ist immer 1.

Zusätzlich zu HELP und TYPE haben die Metrik-Familien in OpenMetrics ein optionales UNIT Metadatum, das die Einheit einer Metrik angibt.

Alle Typen werden in Beispiel 4-16 gezeigt.

Beispiel 4-16. Darstellungsformat für verschiedene Arten von Metriken
# HELP example_gauge An example gauge
# TYPE example_gauge gauge
example_gauge -0.7
# HELP my_counter An example counter
# TYPE my_counter counter
my_counter_total 14
my_counter_created 1.640991600123e+09
# HELP my_summary An example summary
# TYPE my_summary summary
my_summary_sum 0.6
my_summary_count 19
# HELP latency_seconds An example histogram
# TYPE latency_seconds histogram
# UNIT latency_seconds seconds
latency_seconds_bucket{le="0.1"} 7
latency_seconds_bucket{le="0.2"} 18
latency_seconds_bucket{le="0.4"} 24
latency_seconds_bucket{le="0.8"} 28
latency_seconds_bucket{le="+Inf"} 29
latency_seconds_sum 0.6
latency_seconds_count 29
# TYPE my_build_info info
my_build_info{branch="HEAD",version="0.16.0rc1"} 1.0
# TYPE my_stateset stateset
# HELP my_stateset An example stateset
my_stateset{feature="a"} 1
my_stateset{feature="b"} 0
# TYPE my_gaugehistogram gaugehistogram
# HELP my_gaugehistogram An example gaugehistogram
my_gaugehistogram_bucket{le="1.0"} 0
my_gaugehistogram_bucket{le="+Inf"} 3
my_gaugehistogram_gcount 3
my_gaugehistogram_gsum 2
# EOF

In OpenMetrics, wie in Beispiel 4-16 gezeigt, verwenden GaugeHistograms unterschiedliche _gcount und _gsum Suffixe für Zählungen und Summen, wodurch sie sich von den Histograms _count und _sum unterscheiden.

Etiketten

Das Histogramm und das GaugeHistogramm im vorangegangenen Beispiel haben auch gezeigt, wie Beschriftungen dargestellt werden.Mehrere Beschriftungen werden durch Kommas getrennt, aber anders als im Prometheus-Drahtformat sind Kommas vor der schließenden Klammer inOpenMetrics nicht erlaubt.

Zeitstempel

Es ist möglich, einen Zeitstempel für eine Zeitreihe anzugeben. Das ist ein Fließkommawert in Sekunden seit der Unix-Epoche,19 und steht, wie in diesem Beispiel gezeigt, hinter dem Wert:

# HELP foo I'm trapped in a client library
# TYPE foo gauge
foo 1 1.5100992e9
Warnung

Zeitstempel werden in OpenMetrics in Sekunden seit der Epoche angegeben, während sie im Prometheus-Textformat in Millisekunden seit der Epoche angegeben werden.

Du kennst jetzt das OpenMetrics-Format. Die vollständige Spezifikation findest du im OpenMetrics GitHub Repository.

Wir haben jetzt schon ein paar Mal über Labels gesprochen. Im folgenden Kapitel erfährst du, was sie im Detail sind.

1 Keine Exposition bedeutet, dass die Metriken nicht von einem Prometheus-Server abgefragt werden.

2 CPython ist der offizielle Name für die Standard-Python-Implementierung. Verwechsle sie nicht mit Cython, mit dem du C-Erweiterungen in Python schreiben kannst.

3 Das Pushgateway ist für diesen Anwendungsfall nicht geeignet, daher ist dies in der Praxis kein Problem.

4 child_exit wurde in Gunicorn Version 19.7, die im März 2017 veröffentlicht wurde, hinzugefügt.

5 Die Flagge von Gunicorn --max-requests ist ein Beispiel für eine solche Grenze.

6 Bei Batch-Aufträgen, deren Ausführung mehr als ein paar Minuten dauert, kann es jedoch auch sinnvoll sein, sie normal über HTTP zu scrapen, um Leistungsprobleme zu beheben.

7 In informellen Kontexten kann es auch als pgw bezeichnet werden.

8 Für Batch-Aufträge wie Datenbanksicherungen, die an den Lebenszyklus eines Rechners gebunden sind, ist der Node Exporter Textfile Collector die bessere Wahl. Dies wird im Abschnitt "Textdatei-Collector" erläutert .

9 Das Pushgateway exportiert ausdrücklich leere instance Labels für Metriken ohne instance Label. In Kombination mit honor_labels: true führt dies dazu, dass Prometheus diesen Metriken kein instance Label zuweist. Normalerweise sind leere Labels und fehlende Labels in Prometheus das Gleiche, aber das ist die Ausnahme.

10 Genau wie Zusammenfassungen und Histogramme haben Gauges einen Zeitfunktionsdekorator und einen Kontextmanager. Er ist nur für die Verwendung in Batch-Aufträgen gedacht.

11 Die Bezeichnungen werden in den Namen der Metrik eingefügt. Die Unterstützung von Tags (d. h. Bezeichnungen) für Graphite wurde erst kürzlich in Version 1.1.0 hinzugefügt.

12 Das funktioniert in beide Richtungen. Andere Instrumentierungsbibliotheken mit einer entsprechenden Funktion können ihre Metriken in eine Prometheus-Client-Bibliothek einspeisen. Dies wird unter "Benutzerdefinierte Collectors" beschrieben .

13 Der Parser des Go-Clients ist die Referenzimplementierung.

14 Teil des Elasticsearch-Stacks.

15 Das Null-Byte ist ein gültiges UTF-8-Zeichen.

16 Ja, es gibt zwei verschiedene Regeln für das Escape-Format im Textformat. In OpenMetrics wurde dies zu einer einzigen Regel vereinheitlicht, da doppelte Anführungszeichen auch in HELP escaped werden müssen.

17 Mitternacht 1. Januar 1970 UTC.

18 Unter Windows ist \r\n die Zeilenendung, während unter Unix \n verwendet wird. Prometheus hat ein Unix-Erbe, daher verwendet es \n.

19 Mitternacht 1. Januar 1970 UTC.

Get Prometheus: Up & Running, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.