Add loki
This commit is contained in:
+350
-51
@@ -1,46 +1,80 @@
|
||||
"""
|
||||
ZONT API Prometheus Exporter
|
||||
ZONT API Prometheus Exporter + Loki event exporter
|
||||
|
||||
Экспортирует метрики отопительного контроллера ZONT (H-1, H-2, H-1000, H-2000,
|
||||
Mega SX, ZTC и др.) в формате Prometheus.
|
||||
Mega SX, ZTC и др.) в формате Prometheus и отправляет события устройства в Loki.
|
||||
|
||||
Документация API: https://my.zont.online/api/
|
||||
|
||||
Переменные окружения:
|
||||
ZONT_LOGIN — логин от my.zont.online (обязательно)
|
||||
ZONT_PASSWORD — пароль (обязательно, если не задан ZONT_TOKEN)
|
||||
ZONT_DEVICE_ID — ID устройства
|
||||
ZONT_EMAIL — значение заголовка X-ZONT-Client (по умолчанию exporter@example.com)
|
||||
ZONT_TOKEN — готовый auth-токен (опционально)
|
||||
ZONT_POLL_INTERVAL — интервал опроса, сек (по умолчанию 60)
|
||||
ZONT_EXPORTER_PORT — порт HTTP-сервера метрик (по умолчанию 9101)
|
||||
ZONT_LOG_LEVEL — DEBUG/INFO/WARNING/ERROR (по умолчанию INFO)
|
||||
ZONT_DOOR_INVERT — инвертировать логику двери (1=да, 0=нет, по умолчанию 0)
|
||||
ZONT_DOOR_SENSOR_TYPES — через запятую, sensor_type дискретных датчиков (по умолчанию 20)
|
||||
ZONT_LOGIN — логин от my.zont.online (обязательно)
|
||||
ZONT_PASSWORD — пароль (обязательно, если не задан ZONT_TOKEN)
|
||||
ZONT_DEVICE_ID — ID устройства (обязательно)
|
||||
ZONT_CLIENT — значение заголовка X-ZONT-Client (по умолчанию exporter@example.com)
|
||||
ZONT_POLL_INTERVAL — интервал опроса, сек (по умолчанию 60)
|
||||
ZONT_EXPORTER_PORT — порт HTTP-сервера метрик (по умолчанию 9101)
|
||||
ZONT_LOG_LEVEL — DEBUG/INFO/WARNING/ERROR (по умолчанию INFO)
|
||||
ZONT_DOOR_INVERT — инвертировать логику двери (true/false, по умолчанию false)
|
||||
ZONT_DOOR_SENSOR_TYPES — через запятую, sensor_type дискретных датчиков (по умолчанию 20)
|
||||
|
||||
ZONT_LOKI_ENABLED — включить отправку событий в Loki (true/false, по умолчанию true)
|
||||
ZONT_LOKI_URL — URL Loki push API (например http://loki:3100/loki/api/v1/push).
|
||||
Если не задан — отправка событий отключена.
|
||||
ZONT_LOKI_JOB — значение метки job для Loki (по умолчанию zont)
|
||||
ZONT_EVENT_LOOKBACK — окно событий при первом опросе, сек (по умолчанию 2*POLL_INTERVAL)
|
||||
ZONT_EVENT_FILTER — через запятую, типы событий (пусто = все)
|
||||
ZONT_EVENT_MAX — максимум событий за один запрос (по умолчанию 500)
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from prometheus_client import start_http_server, Gauge
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# --- Утилиты ---
|
||||
|
||||
def env_bool(name, default=False):
|
||||
"""Читает boolean из переменной окружения. Принимает true/false/1/0/yes/no/on/off."""
|
||||
v = os.getenv(name)
|
||||
if v is None:
|
||||
return default
|
||||
return v.strip().lower() in ('1', 'true', 'yes', 'on', 'y', 't')
|
||||
|
||||
|
||||
# --- Конфигурация ---
|
||||
logging.basicConfig(
|
||||
level=os.getenv('ZONT_LOG_LEVEL', 'INFO').upper(),
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
ZONT_LOGIN = os.getenv('ZONT_LOGIN')
|
||||
ZONT_PASSWORD = os.getenv('ZONT_PASSWORD')
|
||||
ZONT_CLIENT = os.getenv('ZONT_CLIENT', 'my_exporter@example.com')
|
||||
DEVICE_ID = int(os.getenv('ZONT_DEVICE_ID'))
|
||||
DEVICE_ID = int(os.getenv('ZONT_DEVICE_ID', '0'))
|
||||
API_BASE = "https://my.zont.online/api"
|
||||
POLL_INTERVAL = int(os.getenv('ZONT_POLL_INTERVAL', 60))
|
||||
EXPORTER_PORT = int(os.getenv('ZONT_EXPORTER_PORT', 9101))
|
||||
LOG_LEVEL = os.getenv('ZONT_LOG_LEVEL', 'INFO').upper()
|
||||
|
||||
# Логика двери
|
||||
DOOR_INVERT = os.getenv('ZONT_DOOR_INVERT', '0') == '1'
|
||||
DOOR_INVERT = env_bool('ZONT_DOOR_INVERT', False)
|
||||
DOOR_SENSOR_TYPES = set(
|
||||
int(x) for x in os.getenv('ZONT_DOOR_SENSOR_TYPES', '20').split(',') if x.strip()
|
||||
)
|
||||
|
||||
# Loki
|
||||
LOKI_URL = os.getenv('ZONT_LOKI_URL', '')
|
||||
LOKI_ENABLED = env_bool('ZONT_LOKI_ENABLED', True) and bool(LOKI_URL)
|
||||
LOKI_JOB = os.getenv('ZONT_LOKI_JOB', 'zont')
|
||||
|
||||
# События
|
||||
EVENT_LOOKBACK = int(os.getenv('ZONT_EVENT_LOOKBACK', str(2 * POLL_INTERVAL)))
|
||||
EVENT_FILTER = set(
|
||||
x.strip() for x in os.getenv('ZONT_EVENT_FILTER', '').split(',') if x.strip()
|
||||
)
|
||||
EVENT_MAX = int(os.getenv('ZONT_EVENT_MAX', 500))
|
||||
|
||||
# --- Метрики Prometheus ---
|
||||
|
||||
# Общие
|
||||
@@ -62,7 +96,7 @@ MEMORY_BYTES = Gauge('zont_memory_used_bytes', 'Memory usage bytes', ['device_id
|
||||
# Погода
|
||||
INTERNET_WEATHER = Gauge('zont_internet_weather_celsius', 'Internet weather outside temperature', ['device_id'])
|
||||
|
||||
# Температуры (объекты из z3k-state)
|
||||
# Температуры
|
||||
TEMP_CURR = Gauge('zont_temperature_celsius', 'Current temperature', ['device_id', 'object_id', 'name'])
|
||||
SENSOR_OK = Gauge('zont_temperature_sensor_ok', 'Temperature sensor OK (1=ok)', ['device_id', 'object_id', 'name'])
|
||||
|
||||
@@ -81,31 +115,134 @@ BOILER_DS = Gauge('zont_boiler_ds_celsius', 'Boiler DHW setpoint', ['device_id',
|
||||
BOILER_CONNECTED = Gauge('zont_boiler_connected', 'Boiler connected (1=yes,0=no)', ['device_id', 'adapter_id'])
|
||||
BOILER_FAIL = Gauge('zont_boiler_fail', 'Boiler failure (1=fail)', ['device_id', 'adapter_id'])
|
||||
|
||||
# Дискретные датчики (дверь, окно и т.п.) на аналоговых входах
|
||||
DOOR_OPEN = Gauge(
|
||||
'zont_door_open',
|
||||
'Door open (1=open, 0=closed)',
|
||||
['device_id', 'input_id', 'name']
|
||||
)
|
||||
DOOR_VOLTAGE = Gauge(
|
||||
'zont_door_voltage_volts',
|
||||
'Voltage on door sensor analog input',
|
||||
['device_id', 'input_id', 'name']
|
||||
)
|
||||
# Дискретные датчики
|
||||
DOOR_OPEN = Gauge('zont_door_open', 'Door open (1=open, 0=closed)', ['device_id', 'input_id', 'name'])
|
||||
DOOR_VOLTAGE = Gauge('zont_door_voltage_volts', 'Voltage on door sensor analog input', ['device_id', 'input_id', 'name'])
|
||||
|
||||
# Охранные зоны (помещения)
|
||||
GUARD_ZONE_ONGUARD = Gauge(
|
||||
'zont_guard_zone_onguard',
|
||||
'Guard zone on guard (1=yes, 0=no)',
|
||||
['device_id', 'zone_id', 'name']
|
||||
)
|
||||
# Охранные зоны
|
||||
GUARD_ZONE_ONGUARD = Gauge('zont_guard_zone_onguard', 'Guard zone on guard (1=yes, 0=no)', ['device_id', 'zone_id', 'name'])
|
||||
|
||||
# Справочники ID -> имя для читаемых меток
|
||||
# --- Справочники ---
|
||||
_name_cache = {}
|
||||
_analog_input_types = {} # {input_id: sensor_type}
|
||||
|
||||
_analog_input_types = {}
|
||||
auth_token = None
|
||||
_last_event_poll_time = 0
|
||||
|
||||
GSM_STATE_MAP = {
|
||||
'not-registered': 0,
|
||||
'home-network': 1,
|
||||
'searching': 2,
|
||||
'rejected': 3,
|
||||
'roaming': 5,
|
||||
}
|
||||
|
||||
# Человекочитаемые названия типов событий
|
||||
EVENT_TYPE_RU = {
|
||||
'Stop': 'Остановка',
|
||||
'PowerOn': 'Включение устройства',
|
||||
'PowerOff': 'Выключение устройства',
|
||||
'MainPowerLost': 'Пропало основное питание',
|
||||
'MainPowerFound': 'Появилось основное питание',
|
||||
'GPSLost': 'Потеря GPS',
|
||||
'GPSFound': 'Поиск GPS',
|
||||
'GSMLost': 'Потеря GSM',
|
||||
'GSMFound': 'Регистрация в GSM',
|
||||
'connected': 'Соединение с сервером',
|
||||
'disconnected': 'Отключение от сервера',
|
||||
'reconnected': 'Переподключение к серверу',
|
||||
'GuardOn': 'Постановка на охрану',
|
||||
'GuardOff': 'Снятие с охраны',
|
||||
'AlarmButton': 'Тревожная кнопка',
|
||||
'Shock1': 'Слабый удар',
|
||||
'Shock2': 'Сильный удар',
|
||||
'Door': 'Открытие двери',
|
||||
'Door1': 'Открытие двери 1',
|
||||
'Door2': 'Открытие двери 2',
|
||||
'Door3': 'Открытие двери 3',
|
||||
'Door4': 'Открытие двери 4',
|
||||
'Door5': 'Открытие двери 5',
|
||||
'Door6': 'Открытие двери 6',
|
||||
'Hood': 'Открытие капота',
|
||||
'Trunk': 'Открытие багажника',
|
||||
'HoodTrunk': 'Открытие капота/багажника',
|
||||
'AutoIgnition': 'Автозапуск',
|
||||
'EngineBreakdown': 'Аварийная остановка двигателя',
|
||||
'EngineStarted': 'Двигатель заведён',
|
||||
'EngineStopped': 'Двигатель остановлен',
|
||||
'SoftwareUpgrade': 'Обновление ПО',
|
||||
'Alarm': 'Тревога',
|
||||
'AlarmIgnition': 'Тревога по зажиганию',
|
||||
'AlarmTilt': 'Тревога по наклону',
|
||||
'AlarmBrake': 'Тревога по тормозу',
|
||||
'EngineBlock': 'Блокировка двигателя',
|
||||
'Moving': 'Движение',
|
||||
'Blackout': 'Глушение GSM',
|
||||
'GSMLostAlarm': 'Тревога: пропал GSM',
|
||||
'DriverCallButton': 'Кнопка вызова водителя',
|
||||
'OutCallTone': 'Исходящий звонок: гудок',
|
||||
'OutCallConnection': 'Исходящий звонок: соединение',
|
||||
'OutCallEnd': 'Исходящий звонок: завершён',
|
||||
'OutSMS': 'Исходящее SMS',
|
||||
'InCall': 'Входящий звонок',
|
||||
'InCallConnection': 'Входящий звонок: соединение',
|
||||
'InCallEnd': 'Входящий звонок: завершён',
|
||||
'InSMS': 'Входящее SMS',
|
||||
'ThermometerUpdate': 'Найден термодатчик',
|
||||
'TemperatureHigh': 'Температура выше порога',
|
||||
'TemperatureLow': 'Температура ниже порога',
|
||||
'ThermometerMalfunction': 'Термодатчик недоступен',
|
||||
'BatteryLow': 'Разряд аккумулятора',
|
||||
'RadioFOBAdded': 'Добавлен брелок/радиореле',
|
||||
'BoilerFail': 'Авария котла',
|
||||
'BoilerFailCancel': 'Авария котла устранена',
|
||||
'Landmark': 'Метка',
|
||||
'ZoneAlarm': 'Срабатывание охранной зоны',
|
||||
'ZoneAlarmCancel': 'Восстановление охранной зоны',
|
||||
'OTFail': 'Авария OpenTherm',
|
||||
'OTLost': 'Потеря связи OpenTherm',
|
||||
'OTFound': 'Восстановление связи OpenTherm',
|
||||
'UploadSuccess': 'Файлы загружены',
|
||||
'UploadFailed': 'Не удалось загрузить файлы',
|
||||
'UserEvent': 'Пользовательское событие',
|
||||
'UserAlarm': 'Пользовательская тревога',
|
||||
'ExtSensorWarning': 'Предупреждение доп. датчика',
|
||||
'ExtSensorAlarm': 'Тревога доп. датчика',
|
||||
'CurrentECUErrors': 'Текущие ошибки ЭБУ',
|
||||
'SavedECUErrors': 'Сохранённые ошибки ЭБУ',
|
||||
}
|
||||
|
||||
REASON_RU = {
|
||||
'fob': 'брелок',
|
||||
'call': 'голосовое меню',
|
||||
'sms': 'SMS',
|
||||
'app': 'приложение/веб',
|
||||
'autoguard': 'автопостановка',
|
||||
'centrallock': 'центральный замок',
|
||||
'handsfree': 'радиометка',
|
||||
'pin': 'ПИН-код',
|
||||
'manual': 'вручную',
|
||||
'temperature': 'по температуре',
|
||||
'timer': 'по таймеру',
|
||||
'schedule': 'по расписанию',
|
||||
}
|
||||
|
||||
RESULT_RU = {
|
||||
'success': 'успешно',
|
||||
'fail': 'неудачно',
|
||||
'breakdown': 'двигатель заглох',
|
||||
}
|
||||
|
||||
OT_FAIL_FLAGS_RU = {
|
||||
'sr': 'требуется обслуживание',
|
||||
'lr': 'требуется ручной сброс',
|
||||
'wp': 'низкое давление воды',
|
||||
'gf': 'сбой газа/горелки',
|
||||
'ap': 'сбой давления воздуха',
|
||||
'wot': 'перегрев воды',
|
||||
}
|
||||
|
||||
|
||||
# --- Аутентификация ---
|
||||
|
||||
def get_auth_token():
|
||||
global auth_token
|
||||
@@ -128,8 +265,9 @@ def get_auth_token():
|
||||
logging.error(f"Exception while getting auth token: {e}")
|
||||
|
||||
|
||||
# --- Справочники имён ---
|
||||
|
||||
def build_name_cache(device):
|
||||
"""Собираем человекочитаемые имена для ID объектов."""
|
||||
cache = {}
|
||||
z3k = device.get('z3k_config', {})
|
||||
|
||||
@@ -149,21 +287,170 @@ def build_name_cache(device):
|
||||
|
||||
|
||||
def build_analog_input_types(device):
|
||||
"""Возвращает {input_id: sensor_type} для всех аналоговых входов."""
|
||||
result = {}
|
||||
for ai in device.get('z3k_config', {}).get('analog_inputs', []):
|
||||
result[ai['id']] = ai.get('sensor_type')
|
||||
return result
|
||||
|
||||
|
||||
GSM_STATE_MAP = {
|
||||
'not-registered': 0,
|
||||
'home-network': 1,
|
||||
'searching': 2,
|
||||
'rejected': 3,
|
||||
'roaming': 5,
|
||||
}
|
||||
# --- Описание событий ---
|
||||
|
||||
def describe_event(ev_type, data):
|
||||
if not data:
|
||||
return ''
|
||||
parts = []
|
||||
|
||||
if 'reason' in data:
|
||||
parts.append(f"причина: {REASON_RU.get(data['reason'], data['reason'])}")
|
||||
if 'fob_number' in data:
|
||||
parts.append(f"брелок №{data['fob_number']}")
|
||||
if 'phonenumber' in data:
|
||||
parts.append(f"тел.: {data['phonenumber']}")
|
||||
if 'number' in data:
|
||||
parts.append(f"тел.: {data['number']}")
|
||||
if 'message' in data:
|
||||
parts.append(str(data['message']))
|
||||
if 'zone' in data:
|
||||
parts.append(f"зона: {data['zone']}")
|
||||
if 'slot' in data:
|
||||
parts.append(f"слот: {data['slot']}")
|
||||
if 'type' in data and ev_type == 'ThermometerUpdate':
|
||||
parts.append(f"тип: {data['type']}")
|
||||
if 'serial' in data:
|
||||
parts.append(f"SN: {data['serial']}")
|
||||
if 'voltage' in data:
|
||||
parts.append(f"{data['voltage']} В")
|
||||
if 'result' in data:
|
||||
parts.append(f"результат: {RESULT_RU.get(data['result'], data['result'])}")
|
||||
if 'text' in data:
|
||||
parts.append(f"текст: {data['text']}")
|
||||
if 'label' in data:
|
||||
parts.append(f"метка: {data['label']}")
|
||||
if 'firmware_version' in data:
|
||||
parts.append(f"прошивка: {data['firmware_version']}")
|
||||
if 'profile_version' in data:
|
||||
parts.append(f"профиль: {data['profile_version']}")
|
||||
|
||||
if ev_type == 'OTFail':
|
||||
flags = data.get('f', []) or []
|
||||
c = data.get('c', 0)
|
||||
if c:
|
||||
parts.append(f"код: {c}")
|
||||
if flags:
|
||||
parts.append("флаги: " + ", ".join(OT_FAIL_FLAGS_RU.get(f, f) for f in flags))
|
||||
|
||||
if ev_type in ('CurrentECUErrors', 'SavedECUErrors'):
|
||||
errs = data.get('errors', [])
|
||||
if errs:
|
||||
parts.append("коды: " + ", ".join(str(e) for e in errs))
|
||||
|
||||
return '; '.join(parts)
|
||||
|
||||
|
||||
# --- Loki ---
|
||||
|
||||
def push_to_loki(streams):
|
||||
if not LOKI_ENABLED or not streams:
|
||||
return
|
||||
try:
|
||||
r = requests.post(
|
||||
LOKI_URL,
|
||||
json={'streams': streams},
|
||||
headers={'Content-Type': 'application/json'},
|
||||
timeout=10
|
||||
)
|
||||
r.raise_for_status()
|
||||
logging.debug(f"Pushed {sum(len(s['values']) for s in streams)} log lines to Loki")
|
||||
except Exception as e:
|
||||
logging.error(f"Loki push failed: {e}")
|
||||
|
||||
|
||||
def collect_events(did):
|
||||
global _last_event_poll_time
|
||||
|
||||
if not LOKI_ENABLED:
|
||||
return
|
||||
|
||||
now = int(time.time())
|
||||
mintime = _last_event_poll_time or (now - EVENT_LOOKBACK)
|
||||
|
||||
payload = {
|
||||
'device_id': DEVICE_ID,
|
||||
'mintime': mintime,
|
||||
'maxtime': now,
|
||||
}
|
||||
if EVENT_FILTER:
|
||||
payload['only'] = list(EVENT_FILTER)
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"{API_BASE}/raw_events",
|
||||
headers={
|
||||
'X-ZONT-Client': ZONT_CLIENT,
|
||||
'X-ZONT-Token': auth_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json=payload,
|
||||
timeout=15
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if not data.get('ok'):
|
||||
logging.error(f"raw_events error: {data.get('error_ui', data.get('error'))}")
|
||||
return
|
||||
|
||||
events = data.get('events', [])
|
||||
if len(events) > EVENT_MAX:
|
||||
events = events[-EVENT_MAX:]
|
||||
|
||||
_last_event_poll_time = now
|
||||
|
||||
if not events:
|
||||
return
|
||||
|
||||
streams_map = {}
|
||||
|
||||
for ev in events:
|
||||
if len(ev) < 8:
|
||||
continue
|
||||
ev_id, ev_time, ev_type, lon, lat, duration, extra, is_alarm = ev[:8]
|
||||
ev_type = ev_type or 'unknown'
|
||||
|
||||
description = describe_event(ev_type, extra or {})
|
||||
type_ru = EVENT_TYPE_RU.get(ev_type, ev_type)
|
||||
|
||||
log_obj = {
|
||||
'time': ev_time,
|
||||
'type': ev_type,
|
||||
'type_ru': type_ru,
|
||||
'alarm': bool(is_alarm),
|
||||
'description': description,
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'duration': duration,
|
||||
}
|
||||
line = json.dumps(log_obj, ensure_ascii=False)
|
||||
|
||||
labels = {
|
||||
'job': LOKI_JOB,
|
||||
'device_id': did,
|
||||
'event_type': ev_type,
|
||||
'alarm': 'true' if is_alarm else 'false',
|
||||
}
|
||||
skey = tuple(sorted(labels.items()))
|
||||
if skey not in streams_map:
|
||||
streams_map[skey] = {'stream': labels, 'values': []}
|
||||
streams_map[skey]['values'].append([str(ev_time * 1_000_000_000), line])
|
||||
|
||||
push_to_loki(list(streams_map.values()))
|
||||
logging.info(f"Pushed {len(events)} events to Loki for device {DEVICE_ID}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error collecting events: {e}")
|
||||
|
||||
|
||||
# --- Сбор метрик ---
|
||||
|
||||
def collect_metrics():
|
||||
global auth_token, _name_cache, _analog_input_types
|
||||
@@ -191,6 +478,7 @@ def collect_metrics():
|
||||
logging.error(f"API error: {data.get('error_ui', data.get('error'))}")
|
||||
if data.get('error') in ('unauthorized', 'token_expired'):
|
||||
auth_token = None
|
||||
_last_event_poll_time = 0
|
||||
return
|
||||
|
||||
devices = data.get('devices', [])
|
||||
@@ -199,7 +487,6 @@ def collect_metrics():
|
||||
logging.warning(f"Device {DEVICE_ID} not found in account.")
|
||||
return
|
||||
|
||||
# Обновляем кэши
|
||||
_name_cache = build_name_cache(device)
|
||||
_analog_input_types = build_analog_input_types(device)
|
||||
|
||||
@@ -253,13 +540,12 @@ def collect_metrics():
|
||||
if 'sensor_ok' in obj:
|
||||
SENSOR_OK.labels(device_id=did, object_id=obj_id, name=name).set(1 if obj['sensor_ok'] else 0)
|
||||
|
||||
# Аналоговый вход для контроля напряжения (id=20550) — оставляем отдельную метрику
|
||||
# Аналоговый вход для контроля напряжения
|
||||
if isinstance(obj, dict) and 'voltage' in obj and obj_id == '20550':
|
||||
if obj['voltage'] is not None:
|
||||
ANALOG_VOLTAGE.labels(device_id=did, object_id=obj_id, name=name).set(obj['voltage'])
|
||||
|
||||
# Дискретные датчики (дверь/окно) — есть поля voltage и value,
|
||||
# а в конфиге sensor_type входит в DOOR_SENSOR_TYPES
|
||||
# Дискретные датчики (дверь/окно)
|
||||
if (isinstance(obj, dict) and 'value' in obj and 'voltage' in obj
|
||||
and _analog_input_types.get(int(obj_id)) in DOOR_SENSOR_TYPES):
|
||||
if obj.get('voltage') is not None:
|
||||
@@ -270,7 +556,7 @@ def collect_metrics():
|
||||
door_value = 1 - door_value
|
||||
DOOR_OPEN.labels(device_id=did, input_id=obj_id, name=name).set(door_value)
|
||||
|
||||
# Охранные зоны (есть поле onguard)
|
||||
# Охранные зоны
|
||||
if isinstance(obj, dict) and 'onguard' in obj:
|
||||
GUARD_ZONE_ONGUARD.labels(device_id=did, zone_id=obj_id, name=name).set(
|
||||
1 if obj['onguard'] else 0
|
||||
@@ -304,15 +590,28 @@ def collect_metrics():
|
||||
|
||||
logging.info(f"Metrics collected successfully for device {DEVICE_ID}")
|
||||
|
||||
# --- События -> Loki ---
|
||||
collect_events(did)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error during metric collection: {e}")
|
||||
|
||||
|
||||
# --- Запуск ---
|
||||
|
||||
if __name__ == '__main__':
|
||||
if not all([ZONT_LOGIN, ZONT_PASSWORD, DEVICE_ID]):
|
||||
logging.critical("Missing required env vars: ZONT_LOGIN, ZONT_PASSWORD, ZONT_DEVICE_ID")
|
||||
exit(1)
|
||||
|
||||
if LOKI_ENABLED:
|
||||
logging.info(f"Loki push enabled: {LOKI_URL}")
|
||||
else:
|
||||
if LOKI_URL and not env_bool('ZONT_LOKI_ENABLED', True):
|
||||
logging.info("Loki push disabled by ZONT_LOKI_ENABLED=false")
|
||||
else:
|
||||
logging.info("Loki push disabled (ZONT_LOKI_URL not set)")
|
||||
|
||||
get_auth_token()
|
||||
start_http_server(EXPORTER_PORT)
|
||||
logging.info(f"Prometheus exporter started on port {EXPORTER_PORT}")
|
||||
|
||||
Reference in New Issue
Block a user