Пишем свой prometheus-exporter для sbercloud cdn на python

Описание

Мы используем CDN для раздачи статичных файлов и хотим иметь статистику по этой раздаче (коды ответов, rps, полосу пропускания)

Первый и самый главный вопрос - откуда получить эти данные? Конечно же из CDN API.

Ищем… Ага! Статистика по конкретному аккаунту с разбивкой по ресурсам

Пример успешного ответа realtimestat

[
  {
    "http_status_percent": {
      "200": 8.3333,
      "304": 91.6667
    },
    "account": "aoprc3vtj4",
    "cache_status_percent_by_requests": {
      "HIT": 0,
      "MISS": 100
    },
    "cache_status_percent_by_volume": {
      "HIT": 0,
      "MISS": 100
    },
    "requests_per_second": 1.1903,
    "bandwidth_out_bits_per_second": 3656.8472,
    "resource": "u7qy39gwbj"
  },
  {
    "http_status_percent": {
      "200": 9.1443,
      "304": 90.8557
    },
    "account": "aoprc3vtj4",
    "cache_status_percent_by_requests": {
      "HIT": 5.5746,
      "MISS": 94.4254
    },
    "cache_status_percent_by_volume": {
      "HIT": 99.7363,
      "MISS": 0.2637
    },
    "requests_per_second": 101.3832,
    "bandwidth_out_bits_per_second": 129778356.037,
    "resource": "bgc2555isn"
  }
]

В ответе есть, всё, что надо:

  • http_status_percent. Коды ответов в процентном соотношении от общего числа ответов
  • requests_per_second. Запросы в секунду
  • bandwidth_out_bits_per_second. Исходящий канал

Код

Нет смысла копировать весь код в статью. Исходники можно найти здесь

Разберём основные моменты.

main.py

Запускаем http сервер с хэндлером из прометеевской либы, который уже умеет отдавать при запросе все добавленные в коде метрики.

class HttpHandler(MetricsHandler)
HTTPServer(("0.0.0.0", settings.get("WEB_PORT", 8080)), HttpHandler).serve_forever()

При этом в нашем хэндлере мы:

  • Инициализируем класс для обновления метрик

    metrics = Metrics()
    
  • Переопределяем метод do_GET, чтобы прямо при обработке запроса на путь /metrics обновлять метрики с помощью self.metrics.refresh_metrics(). При этом не забываем вызвать родительский метод do_GET() через super(), чтобы отдать метрики в формате прометея.

    def do_GET(self):
        if self.path == "/metrics":
            self.metrics.refresh_metrics()
            super().do_GET()
        else:
            self.send_error(404)
    

metrics.py

В этом файле описан класс для непосредственного обновления метрик прометея

class Metrics():
    def __init__(self):
        self.cdn = CDN(
                username=settings.get("CDN_USERNAME"),
                password=settings.get("CDN_PASSWORD"),
                account_name=settings.get("CDN_ACCOUNT_NAME"),
                url=settings.get("CDN_URL"))
        self.resources_map = {}
        self.labels_map = {
            "http_status_percent": "code",
            "cache_status_percent_by_requests": "status",
            "cache_status_percent_by_volume": "status"
        }
        self.gauges = {}

        self.update_errors_count = 0
        self.update_resources()
  • При инициализации создаём инстанс cdn = CDN(...), чтобы через него работать с CDN API

  • В resources_map при инициализации вызовом update_resources() мы сохраняем соответствие id ресурса и его имени

    def update_resources(self):
        for resource in self.cdn.get_resource():
            self.resources_map[resource["id"]] = resource["name"]
    

    Это нужно для того, чтобы в лейблах метрик было читаемое название ресурса, а не id, который отдаётся в ответе API (см. выше Пример успешного ответа realtimestat).

  • С помощью labels_map мы преобразуем ключи полей ответов в значения соответсвующих лейблов. К примеру, чтобы из "http_status_percent": { "200": 8.3333, "304": 91.6667 } получить http_status_percent{code="200"} 8.3333 и http_status_percent{code="304"} 91.6667

  • В gauges по соответствующим ключам мы храним объекты метрик Gauge, которые обновляются в методе update_metrics()

Метрики обновляем через метод refresh_metrics()

def refresh_metrics(self):
    self.clear_metrics()
    try:
        self.update_metrics()
        self.update_errors_count = 0
    except HTTPError as e:
        if self.update_errors_count > 2:
            log.error(f'I can not update metrics. Error "{e}". Bye!')
            sys.exit(1)
        self.update_errors_count += 1
        log.info(f'#{self.update_errors_count} Update metrics error "{e}". I will try to update token and continue')
        self.metrics.cdn._refresh_token()
        self.refresh_metrics()
  • Метрики мы сбрасываем перед обновлением методом clear_metrics(), т.к. коды ответов мы получаем уже в процентном соотношении к общему числу ответов и не хотим накапливать старые данные в gauge’ах

    def clear_metrics(self):
        for gauge in self.gauges.values():
            gauge.clear()
    
  • Т.к. мы не следим за свежестью токена, то просто делаем пару ретраев при попытке обновить метрики. В том числе пробуем обновить токен

Результат

Пример отдаваемых метрик

# HELP http_status_percent http_status_percent
# TYPE http_status_percent gauge
http_status_percent{account="aoprc3vtj4",code="304",resource_id="u7qy39gwbj",resource_name="static_example_com"} 100.0
http_status_percent{account="aoprc3vtj4",code="200",resource_id="bgc2555isn",resource_name="images_example_com"} 6.6667
http_status_percent{account="aoprc3vtj4",code="404",resource_id="bgc2555isn",resource_name="images_example_com"} 1.5385
http_status_percent{account="aoprc3vtj4",code="304",resource_id="bgc2555isn",resource_name="images_example_com"} 91.2821
http_status_percent{account="aoprc3vtj4",code="502",resource_id="bgc2555isn",resource_name="images_example_com"} 0.5128
# HELP cache_status_percent_by_requests cache_status_percent_by_requests
# TYPE cache_status_percent_by_requests gauge
cache_status_percent_by_requests{account="aoprc3vtj4",resource_id="u7qy39gwbj",resource_name="static_example_com",status="HIT"} 0.0
cache_status_percent_by_requests{account="aoprc3vtj4",resource_id="u7qy39gwbj",resource_name="static_example_com",status="MISS"} 100.0
cache_status_percent_by_requests{account="aoprc3vtj4",resource_id="bgc2555isn",resource_name="images_example_com",status="HIT"} 3.0769
cache_status_percent_by_requests{account="aoprc3vtj4",resource_id="bgc2555isn",resource_name="images_example_com",status="MISS"} 96.9231
# HELP cache_status_percent_by_volume cache_status_percent_by_volume
# TYPE cache_status_percent_by_volume gauge
cache_status_percent_by_volume{account="aoprc3vtj4",resource_id="u7qy39gwbj",resource_name="static_example_com",status="HIT"} 0.0
cache_status_percent_by_volume{account="aoprc3vtj4",resource_id="u7qy39gwbj",resource_name="static_example_com",status="MISS"} 100.0
cache_status_percent_by_volume{account="aoprc3vtj4",resource_id="bgc2555isn",resource_name="images_example_com",status="HIT"} 99.7153
cache_status_percent_by_volume{account="aoprc3vtj4",resource_id="bgc2555isn",resource_name="images_example_com",status="MISS"} 0.2847
# HELP requests_per_second requests_per_second
# TYPE requests_per_second gauge
requests_per_second{account="aoprc3vtj4",resource_id="u7qy39gwbj",resource_name="static_example_com"} 0.0493
requests_per_second{account="aoprc3vtj4",resource_id="bgc2555isn",resource_name="images_example_com"} 9.6546
# HELP bandwidth_out_bits_per_second bandwidth_out_bits_per_second
# TYPE bandwidth_out_bits_per_second gauge
bandwidth_out_bits_per_second{account="aoprc3vtj4",resource_id="u7qy39gwbj",resource_name="static_example_com"} 118.7743
bandwidth_out_bits_per_second{account="aoprc3vtj4",resource_id="bgc2555isn",resource_name="images_example_com"} 7.6798019124e+06