Prometheus: створення Custom Prometheus Exporter на Python

Автор |  18/02/2023
 

У Прометеуса є багато готових до використання експортерів, але інколи може з’явитися потреба у зборі своїх власних метрик.

Для цього Прометеус надає клієнтські бібліотеки, які можемо використати для генерації метрик з потрібними лейблами.

Експортер можна включити прямо в код вашого додатку, або можна запускати окремим сервісом, який буде звертатися до якогось вашого сервісу і отримувати від нього дані, які потім буде конвертувати в Prometheus-формат та віддавати серверу Prometheus.

Prometheus Metrics

Загальна схема роботи Prometheus-серверу та його експортерів у Kubernetes виглядає так:

Тут маємо:

  1. Prometheus Server, який в нашому випадку розгортається за допомогою Kube Promeheus Stack та Prometheus Operator
  2. за допомогою ServiceMonitor через Operator ми створюємо Scrape Job, яка має один чи декілька Targets, тобто сервісів, які буде опитувати Prometheus для отримання метрик, які він зберігає у своїй Time Series Database
  3. за URL, які вказані в Target, Prometheus звертається до ендпоінту Prometheus Exporter
  4. а Prometheus Exporter збирає метрики з вашого додатку, які потім віддає до Prometheus

Типи метрик Prometheus

Коли плануємо писати свій експортер, необхідно знати які типи метрик ми можемо в ньому використовувати. Основні типи:

  • Counter: може тільки збільшувати своє значення, наприклад для підрахунку кількості HTTP-запитів
  • Enum: має попередньо задані значення, використовується наприклад для моніторингу кількості подів у статус Running або Failed
  • Histograms: зберігає значення за проміжок часу, можна використовавти для, наприклад, отримання часу відповіді веб-северу за період часу – rate(metric_name{}[5m])
  • Gauges: може приймати будь-яке значення, можемо використовувати для, наприклад, зберігання значень нагрузки на CPU
  • Info: key-value storage, наприклад для Build information, Version information, або metadata

У кожного типу є свої методи, тож варто подивитись документацію, там ще й приклади є. Див. Prometheus Python Client, або в документації самої бібліотеки:

[simterm]

>>> import prometheus_client
>>> help(prometheus_client.Enum)

[/simterm]

Python Custom Prometheus Exporter

Python HTTPServer Exporter з Counter

Для початку, давайте подивимось як воно взагалі працює – напишимо скрипт на Python, в якому на порту 8080 буде звичайний HTTP-сервер, а на порту 9000 – експортер, який буде збирати статистику по запитах з кодами відповідей і створювати метрику http_requests з двома лейблами – в одній будемо зберігати код відповіді, а в іншій – ім’я хоста, з якого метрика була отримана.

Встановлюємо бібліотеку:

[simterm]

$ pip install prometheus_client

[/simterm]

Пишемо скрипт:

#!/usr/bin/env python

import os
import re
import platform
from time import sleep

from http.server import BaseHTTPRequestHandler, HTTPServer

from prometheus_client import start_http_server, Counter, REGISTRY

class HTTPRequestHandler(BaseHTTPRequestHandler):
    
    def do_GET(self):
        if self.path == '/':
            self.send_response(200)
            self.send_header('Content-type','text/html')
            self.end_headers()
            self.wfile.write(bytes("<b> Hello World !</b>", "utf-8"))
            request_counter.labels(status_code='200', instance=platform.node()).inc()
        else:
            self.send_error(404)
            request_counter.labels(status_code='404', instance=platform.node()).inc()
    
                
if __name__ == '__main__':
            
    start_http_server(9000)
    request_counter = Counter('http_requests', 'HTTP request', ["status_code", "instance"])

    webServer = HTTPServer(("localhost", 8080), HTTPRequestHandler).serve_forever()
    print("Server started")

Тут ми:

  • start_http_server() – запускаємо HTTP-сервер самого експортеру на порту 9000
  • request_counter – створюємо метрику з ім’ям http_requests, типом Counter, і додаємо їй дві labels – status_code та instance
  • webServer – запускаємо звичайний HTTP-сервер на Python на порту 8080

Далі, коли ми робитимо HTTP-запит на localhost:8080, він буде попадати до do_GET(), в якому буде перевірятися URI. Якщо йдемо на / – то отримаємо код 200, якщо будь-який інший – то 404.

І там же оновлюємо значення метрики http_requests – додаємо код відповіді та ім’я хоста, і викликаємо метод Counter.inc(), який інкрементить значення метрики на одиницю. Таким чином кожен запит, який буде оброблено веб-сервером webServer буде додавати +1 в нашу метрику, а в залежності від коду відповіді – ми отримаємо цю метрику з двома різними лейблами – 200 та 404.

Перевіряємо – запускаємо сам скрипт:

[simterm]

$ ./py_http_exporter.py

[/simterm]

Робимо декілька запитів з різними URI:

[simterm]

$ curl -I -X GET localhost:8080/
HTTP/1.0 200 OK
$ curl -I -X GET localhost:8080/
HTTP/1.0 200 OK
$ curl -I -X GET localhost:8080/
HTTP/1.0 200 OK

$ curl -I -X GET localhost:8080/blablabla
HTTP/1.0 404 Not Found
$ curl -I -X GET localhost:8080/blablabla
HTTP/1.0 404 Not Found
$ curl -I -X GET localhost:8080/blablabla
HTTP/1.0 404 Not Found

[/simterm]

А тепер перевіримо, що маємо на ендпоінті експортеру:

[simterm]

$ curl -X GET localhost:9000
...
# HELP http_requests_total HTTP request
# TYPE http_requests_total counter
http_requests_total{instance="setevoy-wrk-laptop",status_code="200"} 3.0
http_requests_total{instance="setevoy-wrk-laptop",status_code="404"} 3.0

[/simterm]

Чудово – маємо три запроси з кодом 200, та три – з кодом 404.

Jenkins Jobs Exporter з Gauage

Або інший варіант – коли експортер буде звертатися до якось зовнішнього ресурсу, отримувати значення, і вносити їх до метрики.

Наприклад, ми можемо звертатись до якогось API, і від нього отримати дані, в цьому прикладі це буде Jenkins:

#!/usr/bin/env python

import time
import random
from prometheus_client import start_http_server, Gauge
from api4jenkins import Jenkins

jenkins_client = Jenkins('http://localhost:8080/', auth=('admin', 'admin'))

jenkins_jobs_counter = Gauge('jenkins_jobs_count', "Number of Jenkins jobs")

def get_metrics():
   jenkins_jobs_counter.set(len(list(jenkins_client.iter_jobs())))

if __name__ == '__main__':
    start_http_server(9000)
    while True:
        get_metrics()
        time.sleep(15)

Тут ми за допомогою бібліотеки api4jenkins створюємо об’єкт jenkins_client, який підключається до інстансу Jenkins та отримує кількість його jobs. Потім в функції get_metrics() ми рахуємо кількість об’єктів із jenkins_client.iter_jobs(), та вносимо їх до метрики jenkins_jobs_counter.

Запускаємо в Docker:

[simterm]

$ docker run -p 8080:8080 jenkins:2.60.3

[/simterm]

Створюємо тестову задачу:

В результаті отримуємо такий результат:

[simterm]

$ curl localhost:9000
...
# HELP jenkins_jobs_count Number of Jenkins jobs
# TYPE jenkins_jobs_count gauge
jenkins_jobs_count 1.0

[/simterm]

Prometheus Exporter та Kubernetes

І давайте протестимо якийсь більш реальний приклад.

У нас є API-сервіс, який використовує базу даних PostgreSQL. Для перевірки підключення девелопери створили ендпоінт, на який ми можемо звертатися для отримання поточного статусу – є чи нема підключення до серверу баз даних.

Зараз для його моніторингу ми використовуємо Blackbox Exporter, але згодом хочется трохи розширити можливості, тож спробуємо створити експортер, котрий поки що буде просто перевіряти код відповіді він цього ендпоінту.

Exporter з Enum та Histogram

#!/usr/bin/env python

import os
import requests
import time
from prometheus_client import start_http_server, Enum, Histogram

hitl_psql_health_status = Enum("hitl_psql_health_status", "PSQL connection health", states=["healthy", "unhealthy"])
hitl_psql_health_request_time = Histogram('hitl_psql_health_request_time', 'PSQL connection response time (seconds)')

def get_metrics():

    with hitl_psql_health_request_time.time():
        resp = requests.get(url=os.environ['HITL_URL'])
    
    print(resp.status_code)
            
    if not (resp.status_code == 200):
        hitl_psql_health_status.state("unhealthy")
            
if __name__ == '__main__':
    start_http_server(9000)
    while True:
        get_metrics()
        time.sleep(1)

Задаємо змінну оточення з URL:

[simterm]

$ export HITL_URL=https://hitl.qa.api.example.com/api/v1/postgres/health-check

[/simterm]

Та запускаємо скрипт:

[simterm]

$ ./py_hitl_exporter.py 
500
500
500

[/simterm]

Ммм… Чудово – ендпоінт лежить 🙂 То в понеділок нехай девелопери перевірять, нам підходить і це.

Docker-образ

Далі, збираємо Docker-образ – спочатку створимо requirements.txt з залежностями:

requests
prometheus_client

Теперь Dockerfile:

FROM python:3.8

COPY py_hitl_exporter.py /app/py_hitl_exporter.py
COPY requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip install -r requirements.txt
ENV HITL_URL $HITL_URL
CMD ["python3", "/app/py_hitl_exporter.py"]

Збираємо образ:

[simterm]

$ docker build -t setevoy/test-exporter .

[/simterm]

Запускаємо локально для перевірки:

[simterm]

$ docker run -p 9000:9000 -e HITL_URL=https://hitl.qa.api.example.com/api/v1/postgres/health-check setevoy/test-exporter

[/simterm]

Перевіряємо метрики:

[simterm]

$ curl localhost:9000
...
# HELP hitl_psql_health_status PSQL connection health
# TYPE hitl_psql_health_status gauge
hitl_psql_health_status{hitl_psql_health_status="healthy"} 0.0
hitl_psql_health_status{hitl_psql_health_status="unhealthy"} 1.0
# HELP hitl_psql_health_request_time PSQL connection response time (seconds)
# TYPE hitl_psql_health_request_time histogram
hitl_psql_health_request_time_bucket{le="0.005"} 0.0
hitl_psql_health_request_time_bucket{le="0.01"} 0.0
hitl_psql_health_request_time_bucket{le="0.025"} 0.0
hitl_psql_health_request_time_bucket{le="0.05"} 0.0
hitl_psql_health_request_time_bucket{le="0.075"} 0.0
hitl_psql_health_request_time_bucket{le="0.1"} 0.0
hitl_psql_health_request_time_bucket{le="0.25"} 0.0
hitl_psql_health_request_time_bucket{le="0.5"} 0.0
hitl_psql_health_request_time_bucket{le="0.75"} 0.0
hitl_psql_health_request_time_bucket{le="1.0"} 0.0
hitl_psql_health_request_time_bucket{le="2.5"} 0.0
hitl_psql_health_request_time_bucket{le="5.0"} 0.0
hitl_psql_health_request_time_bucket{le="7.5"} 0.0
hitl_psql_health_request_time_bucket{le="10.0"} 0.0
hitl_psql_health_request_time_bucket{le="+Inf"} 9.0
hitl_psql_health_request_time_count 9.0
hitl_psql_health_request_time_sum 96.56228125099824

[/simterm]

Добре.

Пушимо його в Docker Hub:

[simterm]

$ docker login
$ docker push setevoy/test-exporter

[/simterm]

Kubernetes Pod, Service та ServiceMonitor для Prometheus

Далі – треба цей образ запустити в Kubernetes, та створити ServiceMonitor для Prometheus, який там вже запущено.

Створюємо маніфест з Pod, Service та ServiceMonitor:

apiVersion: v1
kind: Pod
metadata:
  name: hitl-exporter-pod
  labels:
    app: hitl-exporter
spec:
  containers:
    - name: hitl-exporter-container
      image: setevoy/test-exporter
      env:
      - name: HITL_URL
        value: https://hitl.qa.api.example.com/api/v1/postgres/health-check
      ports:
        - name: metrics
          containerPort: 9000
          protocol: TCP
---     
apiVersion: v1
kind: Service
metadata:
  name: hitl-exporter-service
  labels:
    app: hitl-exporter
spec:
  selector:
    app: hitl-exporter
  ports:
    - name: metrics
      port: 9000
      targetPort: metrics
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    release: kps
    app: hitl-exporter
  name: hitl-exporter-monitor
spec:
  endpoints:
    - port: metrics
      interval: 15s
      scrapeTimeout: 10s
  namespaceSelector:
    matchNames:
    - monitoring
  selector:
    matchLabels:
      app: hitl-exporter

Запускаємо:

[simterm]

$ kubectl -n monitoring apply -f hitl_exporter.yaml 
pod/hitl-exporter-pod created
service/hitl-exporter-service created
servicemonitor.monitoring.coreos.com/hitl-exporter-monitor created

[/simterm]

І за хвилину-дві перевіряємо Service Discovery:

Targets:

Та Jobs:

Переходимо до графіків – і маємо наші метрики:

Готово.