Python / Собираем свои счетчики через collectd протокол

в 17:50, , рубрики: collectd, метки:

Приветствую!

Думаете как собирать счетчики со своих собственных сервисов?
Запарились парсить логи?
Постоянно забываете настроить сбор счетчиков для нового или переехавшего в другое место сервиса?

Python / Собираем свои счетчики через collectd протокол

Тогда welcome!

В любом крупном проекте со временем появляется куча всяких разных узкоспециализированных сервисов за которыми нужно следить хотя бы для того, чтобы понимать когда следует заказать еще железа.
Для этого обычно выдумываются «жизненные показатели» по которым хочется видеть красивые графики с целью понять близко или далеко, например, «потолок» производительности.

Конечно же разработчики не скупятся на логи и раньше мне приходилось долго и мучительно парсить гигабайты этого добра, чтобы построить какой-нибудь график.

С этим можно жить, но жизнь можно сделать лучше, особенно когда вы имеете доступ к коду сервиса. Тогда можно научить сервис самостоятельно отсылать данные о себе на сервис сбора данных — collectd

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

Суть collectd:

  • Это демон, который слушает один порт
  • Принимает udp-пакеты (см. протокол)
  • Извлеченные из пакетов данные записывает в rrd базу

Осталось научить свои сервисы формировать и отправлять такие udp-пакеты и графики почти готовы.

Конфигурация collectd

Сначала нужно научить collectd понимать ваши собственные счетчики.

Правим /etc/collectd.conf и добавляем описание своих типов:

TypesDB "/usr/lib/collectd/types.db" "/etc/collectd/customtypes.db"

Пример №1. Данные будут сохранены в отдельных rrd-файлах

http_server_avg_item_process_time value:GAUGE:0:U
http_server_items_processed value:GAUGE:0:U

Пример №2. Данные будут сохранены в одном rrd-файле

process_memory working_set:GAUGE:0:U, peak_working_set:GAUGE:0:U

Разница в том, что для первого примера придется отправлять 2 udp-пакета, а для второго можно послать один пакет с двумя значениями.

Перезапускаем collectd и он готов к приему ваших данных.

Binary protocol на питоне

На самом деле реализовать binary protocol достаточно просто на любом языке, на раз уж блог про питон, то будет на питоне :)

NB: Я использую стандартную версию collectd демона из пакета debian 5 — 4.4.2. Она уже довольно старая, но вроде binary protocol там не менялся, поэтому думается, что версия особой роли не играет.

Дефолтная реализация в принципе работает, но она лишена возможности отправки множества значений в одном пакете.

Если выкинуть из дефолтной реализации почти все лишнее, и допилить отправку множественных значений, то можно получить, например, такой код:

import struct import time import socket  SEND_INTERVAL = 10      # seconds MAX_PACKET_SIZE = 1024  # bytes  TYPE_NAME = "gauge"  TYPE_HOST            = 0x0000 TYPE_TIME            = 0x0001 TYPE_PLUGIN          = 0x0002 TYPE_PLUGIN_INSTANCE = 0x0003 TYPE_TYPE            = 0x0004 TYPE_TYPE_INSTANCE   = 0x0005 TYPE_VALUES          = 0x0006 TYPE_INTERVAL        = 0x0007 LONG_INT_CODES = [TYPE_TIME, TYPE_INTERVAL] STRING_CODES = [TYPE_HOST, TYPE_PLUGIN, TYPE_PLUGIN_INSTANCE, TYPE_TYPE, TYPE_TYPE_INSTANCE]  VALUE_COUNTER  = 0 VALUE_GAUGE    = 1 VALUE_DERIVE   = 2 VALUE_ABSOLUTE = 3 VALUE_CODES = {     VALUE_COUNTER:  "!Q",     VALUE_GAUGE:    "<d",     VALUE_DERIVE:   "!q",     VALUE_ABSOLUTE: "!Q" }  def pack_numeric(type_code, number):     return struct.pack("!HHq", type_code, 12, number)  def pack_string(type_code, string):     return struct.pack("!HH", type_code, 5 + len(string)) + string + ""  def pack(typeId, value):     if typeId in LONG_INT_CODES:         return pack_numeric(typeId, value)     elif typeId in STRING_CODES:         return pack_string(typeId, value)     else:         raise AssertionError("invalid type code " + str(id))  def pack_counters(counters):         length = 6 + len(counters)*9         result = []         result.append(struct.pack("!HHH", TYPE_VALUES, length, len(counters)))         for name in counters.keys():                 result.append(struct.pack("<B", VALUE_GAUGE))         for value in counters.values():                 result.append(struct.pack("<d", value))         return result  def message_start(when=None, host=socket.gethostname(), plugin_inst="", plugin_name="any", value_type=TYPE_NAME):     return "".join([         pack(TYPE_HOST, host),         pack(TYPE_TIME, when or time.time()),         pack(TYPE_PLUGIN, plugin_name),         pack(TYPE_PLUGIN_INSTANCE, plugin_inst),         pack(TYPE_TYPE, value_type),         pack(TYPE_TYPE, value_type),         pack(TYPE_INTERVAL, SEND_INTERVAL)     ])  def create_message(counters, when=None, host=socket.gethostname(), plugin_inst="", plugin_name="any", type_name=TYPE_NAME):     message = [message_start(when, host, plugin_inst, plugin_name, type_name)]     parts = pack_counters(counters)     message.extend(parts)     return "".join(message) 

Пример формирования udp-пакета для отправки с двумя счетчиками

create_message({'working_set':working_set, 'peak_working_set':working_set}, plugin_name='service_name', type_name='process_memory') 

При получении такого пакета collectd создаст или добавит в файл ваши данные примерно по такому вот пути:
/var/lib/collectd/rrd/hostname/service_name/process_memory.rrd

В отладке может сильно помочь Wireshark, который понимает collectd протокол и может подсказать что с ним не так, если в логе самого collectd ничего внятного нет.

Строим графики

Collectd — это сборщик данных и ничего более. Для того, чтобы построить график ему нужна веб-морда.
Идеальный партнер для collectd, которого мне удалось найти — drraw.
Это веб-морда для rrdtool и ничего более.

Главная фишка которая понравилась лично мне и которой нет у остальных веб-морд — это гибкая настройка графиков по регулярным выражениям. Drraw автоматически найдет все хосты/сервисы/etc и объединит их на одном графике.

Скриншот настройки графика (частично)

Python / Собираем свои счетчики через collectd протокол

Пример графика

Python / Собираем свои счетчики через collectd протокол

Автор: gmlexx


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js