Fix
Build and Push / maintence (push) Successful in 1s
Build and Push / buildApp (push) Failing after 8s
Build and Push / pushApp (push) Skipped

This commit is contained in:
energys
2026-09-25 20:58:42 +09:00
parent ef6356781d
commit eb9b49b3e1
2 changed files with 139 additions and 606 deletions
+9 -27
View File
@@ -1,33 +1,15 @@
# syntax=docker/dockerfile:1.7 FROM python:3.11-slim
FROM python:3.12-slim AS base
# Меньше мусора в логах python
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Непривилегированный пользователь
RUN groupadd --system --gid 1001 app \
&& useradd --system --uid 1001 --gid app --create-home app
WORKDIR /app WORKDIR /app
# Ставим зависимости отдельным слоем (кэшируется) # Установка зависимостей
COPY requirements.txt ./ RUN pip install --no-cache-dir requests prometheus-client
RUN pip install --no-cache-dir -r requirements.txt
# Копируем код # Копирование кода
COPY --chown=app:app zont_exporter.py ./ COPY exporter.py .
USER app # Экспорт порта Prometheus
EXPOSE 9101
EXPOSE 9877 # Запуск
CMD ["python", "exporter.py"]
# Healthcheck: сам /metrics должен отвечать 200
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD python -c "import urllib.request,sys; \
sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:9877/metrics', timeout=4).status==200 else 1)"
ENTRYPOINT ["python", "zont_exporter.py"]
+130 -579
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
""" """
ZONT API Prometheus Exporter ZONT API Prometheus Exporter
@@ -17,608 +16,160 @@ Mega SX, ZTC и др.) в формате Prometheus.
EXPORTER_PORT — порт HTTP-сервера метрик (по умолчанию 9877) EXPORTER_PORT — порт HTTP-сервера метрик (по умолчанию 9877)
LOG_LEVEL — DEBUG/INFO/WARNING/ERROR (по умолчанию INFO) LOG_LEVEL — DEBUG/INFO/WARNING/ERROR (по умолчанию INFO)
""" """
import os import os
import sys
import time import time
import signal
import logging import logging
from typing import Any, Optional
import requests import requests
from prometheus_client import start_http_server, Gauge, Info from prometheus_client import start_http_server, Gauge
# --------------------------------------------------------------------------- # Настройка логирования
# Настройки (через переменные окружения) logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# ---------------------------------------------------------------------------
ZONT_LOGIN = os.environ.get("ZONT_LOGIN", "demo")
ZONT_PASSWORD = os.environ.get("ZONT_PASSWORD", "demo")
ZONT_DEVICE_ID = int(os.environ.get("ZONT_DEVICE_ID", "0")) # 0 = авто-выбор
ZONT_EMAIL = os.environ.get("ZONT_EMAIL", "exporter@example.com")
ZONT_TOKEN = os.environ.get("ZONT_TOKEN") or None
ZONT_POLL_INTERVAL = int(os.environ.get("ZONT_POLL_INTERVAL", "60"))
EXPORTER_PORT = int(os.environ.get("EXPORTER_PORT", "9877"))
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
# Конфигурация из переменных окружения
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'))
API_BASE = "https://my.zont.online/api" API_BASE = "https://my.zont.online/api"
SCRAPE_INTERVAL = int(os.getenv('SCRAPE_INTERVAL', 60))
logging.basicConfig( # Определение метрик Prometheus
level=getattr(logging, LOG_LEVEL.upper(), logging.INFO), TEMP_GAUGE = Gauge('zont_temperature_celsius', 'Current temperature', ['device_id', 'sensor_name', 'sensor_type'])
format="%(asctime)s [%(levelname)s] %(message)s", BOILER_WORK = Gauge('zont_boiler_work_seconds', 'Boiler work time in last minute', ['device_id'])
datefmt="%Y-%m-%dT%H:%M:%S", TARGET_TEMP = Gauge('zont_target_temperature_celsius', 'Target temperature', ['device_id'])
)
log = logging.getLogger("zont-exporter")
# Глобальная переменная для токена
auth_token = None
# --------------------------------------------------------------------------- def get_auth_token():
# Клиент ZONT API """Получает токен аутентификации ZONT API."""
# --------------------------------------------------------------------------- global auth_token
class ZontClient: try:
"""Тонкая обёртка над REST API ZONT с автоматическим переполучением токена.""" resp = requests.post(
def __init__(
self,
login: str,
password: str,
client_email: str,
token: Optional[str] = None,
):
self.login = login
self.password = password
self.session = requests.Session()
self.session.headers.update({
"X-ZONT-Client": client_email,
"Content-Type": "application/json",
"Accept": "application/json",
})
self.session.auth = (login, password)
self.token: Optional[str] = token
# -- Аутентификация ----------------------------------------------------
def get_authtoken(self, client_name: str = "prometheus-exporter") -> str:
"""POST /get_authtoken — получаем токен по логину/паролю."""
r = self.session.post(
f"{API_BASE}/get_authtoken", f"{API_BASE}/get_authtoken",
json={"client_name": client_name}, auth=(ZONT_LOGIN, ZONT_PASSWORD),
timeout=20, headers={'X-ZONT-Client': ZONT_CLIENT},
json={"client_name": "Prometheus Exporter"},
timeout=10
) )
r.raise_for_status() resp.raise_for_status()
data = r.json() data = resp.json()
if not data.get("ok"): if data.get('ok'):
raise RuntimeError(f"get_authtoken failed: {data}") auth_token = data.get('token')
self.token = data["token"] logging.info("Successfully obtained ZONT auth token.")
log.info("Получен новый auth-токен") else:
return self.token logging.error(f"Failed to get auth token: {data}")
except Exception as e:
logging.error(f"Exception while getting auth token: {e}")
def _headers(self) -> dict: def fetch_last_value(dta_list):
h = {} """Извлекает последнее значение из Delta-time array."""
if self.token: if not dta_list or not isinstance(dta_list, list):
h["X-ZONT-Token"] = self.token
return h
# -- Универсальный POST -------------------------------------------------
def _post(self, method: str, payload: dict) -> dict:
"""POST с автоматическим переполучением токена при 403."""
if not self.token:
self.get_authtoken()
url = f"{API_BASE}/{method}"
for attempt in (1, 2):
r = self.session.post(
url, json=payload, headers=self._headers(), timeout=20
)
if r.status_code == 403 and attempt == 1:
log.warning("Токен отозван, получаю новый...")
self.get_authtoken()
continue
r.raise_for_status()
data = r.json()
if not data.get("ok"):
raise RuntimeError(f"{method} error: {data}")
return data
raise RuntimeError(f"{method}: не удалось выполнить запрос")
# -- Высокоуровневые методы --------------------------------------------
def devices(self, load_io: bool = True) -> list:
"""POST /devices — список устройств с состояниями."""
return self._post("devices", {"load_io": load_io}).get("devices", [])
def load_data(
self,
device_id: int,
data_types: list,
mintime: Optional[int] = None,
maxtime: Optional[int] = None,
) -> dict:
"""POST /load_data — история данных."""
req: dict = {"device_id": device_id, "data_types": data_types}
if mintime is not None:
req["mintime"] = mintime
if maxtime is not None:
req["maxtime"] = maxtime
return self._post("load_data", {"requests": [req]})["responses"][0]
# ---------------------------------------------------------------------------
# Метрики Prometheus
# ---------------------------------------------------------------------------
# --- Общие ---
g_device_info = Info(
"zont_device",
"Информация об устройстве ZONT",
)
# --- Термостат / отопление ---
g_target_temp = Gauge(
"zont_target_temp_celsius",
"Целевая температура термостата, °C",
)
g_pza_temp = Gauge(
"zont_pza_temp_celsius",
"Расчётная температура ПЗА, °C",
)
g_boiler_work = Gauge(
"zont_boiler_work_seconds",
"Время работы котла за последнюю минуту, сек",
)
g_power = Gauge(
"zont_power_on",
"Наличие основного питания (1=есть, 0=нет)",
)
g_boiler_fail = Gauge(
"zont_boiler_fail",
"Авария котла (1=есть, 0=нет)",
)
g_gate = Gauge(
"zont_gate_on",
"Режим комнатного термостата активен (1/0)",
)
# --- Температурные датчики (H-серия) ---
g_thermo_temp = Gauge(
"zont_thermometer_temperature_celsius",
"Показание термодатчика, °C",
["uuid", "name", "type"],
)
g_thermo_state = Gauge(
"zont_thermometer_state",
"Состояние термодатчика (1=ok, 0=malfunction)",
["uuid", "name", "type"],
)
g_thermo_last_update = Gauge(
"zont_thermometer_last_update_timestamp",
"Unix-время последнего обновления показаний датчика",
["uuid", "name"],
)
# --- OpenTherm ---
g_ot_flag = Gauge(
"zont_opentherm_flag",
"Флаги состояния OpenTherm (1=активен)",
["flag"],
)
g_ot_cs = Gauge(
"zont_opentherm_ch_setpoint_celsius",
"Расчётная температура теплоносителя отопления (OpenTherm), °C",
)
g_ot_bt = Gauge(
"zont_opentherm_boiler_temp_celsius",
"Фактическая температура теплоносителя (OpenTherm), °C",
)
g_ot_dt = Gauge(
"zont_opentherm_dhw_temp_celsius",
"Фактическая температура ГВС (OpenTherm), °C",
)
g_ot_rwt = Gauge(
"zont_opentherm_return_temp_celsius",
"Температура обратного потока (OpenTherm), °C",
)
g_ot_outside = Gauge(
"zont_opentherm_outside_temp_celsius",
"Уличная температура (OpenTherm), °C",
)
g_ot_rml = Gauge(
"zont_opentherm_flame_modulation_percent",
"Уровень модуляции горелки (OpenTherm), %",
)
g_ot_wp = Gauge(
"zont_opentherm_water_pressure_bar",
"Давление теплоносителя (OpenTherm), бар",
)
g_ot_flow = Gauge(
"zont_opentherm_dhw_flow_lpm",
"Скорость потока ГВС (OpenTherm), л/мин",
)
g_ot_fail_code = Gauge(
"zont_opentherm_fail_code",
"OEM-код аварии котла (OpenTherm)",
)
# --- Связь / контроллер ---
g_gsm_level = Gauge(
"zont_gsm_level",
"Уровень сигнала GSM [0..31]",
)
g_gsm_state = Gauge(
"zont_gsm_state",
"Состояние регистрации в GSM-сети (см. документацию)",
)
g_gsm_balance = Gauge(
"zont_gsm_balance",
"Баланс SIM-карты (в копейках/центах)",
)
g_voltage = Gauge(
"zont_supply_voltage_volts",
"Напряжение основного питания, В",
)
g_wifi_rssi = Gauge(
"zont_wifi_rssi",
"Уровень сигнала Wi-Fi (RSSI)",
)
g_status_flags = Gauge(
"zont_status_flags",
"Битовая маска состояния контроллера (см. документацию)",
)
g_internal_temp = Gauge(
"zont_internal_temperature_celsius",
"Показания встроенных температурных входов контроллера, °C",
["channel"],
)
# --- Охрана / авто (ZTC) ---
g_guard_state = Gauge(
"zont_guard_enabled",
"Охрана активна (1=enabled, 0=disabled)",
)
g_siren = Gauge(
"zont_siren_on",
"Сирена активна (1/0)",
)
g_engine_block = Gauge(
"zont_engine_block_on",
"Блокировка двигателя активна (1/0)",
)
g_webasto = Gauge(
"zont_webasto_on",
"Предпусковой подогреватель включён (1/0)",
)
g_auto_ignition = Gauge(
"zont_auto_ignition_state",
"Состояние автозапуска (0=disabled,1=enabling,2=enabled,3=webasto,4=webasto-confirmed)",
)
g_ignition = Gauge(
"zont_ignition_on",
"Зажигание включено (1/0)",
)
g_engine = Gauge(
"zont_engine_on",
"Двигатель запущен (1/0)",
)
# --- Служебные ---
g_scrape_success = Gauge(
"zont_scrape_success",
"1 если последний опрос прошёл успешно, 0 иначе",
)
g_scrape_timestamp = Gauge(
"zont_scrape_timestamp_seconds",
"Unix-время последнего успешного опроса",
)
# ---------------------------------------------------------------------------
# Хелперы
# ---------------------------------------------------------------------------
def _b(v: Any) -> Optional[int]:
"""Bool → 0/1, None → None."""
if v is None:
return None return None
return 1 if v else 0 # Последний элемент массива имеет актуальное значение
last_entry = dta_list[-1]
if isinstance(last_entry, list) and len(last_entry) >= 2:
def _guard_state_to_int(v: Optional[str]) -> Optional[int]: return last_entry[1]
if v is None:
return None
if v == "enabled":
return 1
if v in ("disabled", "enabling"):
return 0
return None return None
def collect_metrics():
"""Основная функция сбора метрик."""
if not auth_token:
logging.warning("No auth token available, trying to authenticate...")
get_auth_token()
if not auth_token:
return
def _auto_ign_state_to_int(v: Optional[str]) -> Optional[int]: # Запрос данных thermostat_work за последний час (3600 секунд)
"""
Кодируем строку автозапуска в число:
0 — disabled
1 — enabling
2 — enabled
3 — webasto
4 — webasto-confirmed
"""
mapping = {
"disabled": 0,
"enabling": 1,
"enabled": 2,
"webasto": 3,
"webasto-confirmed": 4,
}
if v is None:
return None
return mapping.get(v)
def _set(gauge: Gauge, value: Any, **labels: str) -> None:
"""Безопасная установка метрики."""
if value is None:
return
try:
if labels:
gauge.labels(**labels).set(float(value))
else:
gauge.set(float(value))
except Exception as e:
log.debug("Не удалось установить метрику: %s", e)
def _dta_last_value(dta: list) -> Optional[float]:
"""
Возвращает последнее значение из Delta-time Array.
Формат: [[t1, v1], [-dt, v2], ...]
"""
if not dta:
return None
try:
return dta[-1][1]
except (IndexError, TypeError):
return None
# ---------------------------------------------------------------------------
# Обновление метрик из /devices (state = поле io)
# ---------------------------------------------------------------------------
def update_from_devices(device: dict) -> None:
io = device.get("io") or {}
# --- Общая информация ---
g_device_info.info({
"id": str(device.get("id", "")),
"serial": str(device.get("serial", "")),
"name": str(device.get("name", "")),
"devtype": str(device.get("devtype", device.get("type", ""))),
})
# --- Термостат (last-boiler-state) ---
lbs = io.get("last-boiler-state")
if isinstance(lbs, dict):
_set(g_target_temp, lbs.get("target_temp"))
_set(g_pza_temp, lbs.get("pza_t"))
_set(g_boiler_work, lbs.get("boiler_work_time"))
_set(g_power, _b(lbs.get("power")))
_set(g_boiler_fail, _b(lbs.get("fail")))
_set(g_gate, _b(lbs.get("gate")))
ot = lbs.get("ot")
if isinstance(ot, dict):
# Флаги OpenTherm — сбрасываем все известные и ставим активные
known_flags = ["f", "ch", "dhw", "fl", "cl", "ch2", "di"]
active = set(ot.get("s") or [])
for flag in known_flags:
_set(g_ot_flag, 1 if flag in active else 0, flag=flag)
_set(g_ot_cs, ot.get("cs"))
_set(g_ot_bt, ot.get("bt"))
_set(g_ot_dt, ot.get("dt"))
_set(g_ot_rwt, ot.get("rwt"))
_set(g_ot_outside, ot.get("ot"))
_set(g_ot_rml, ot.get("rml"))
_set(g_ot_wp, ot.get("wp"))
_set(g_ot_flow, ot.get("fr"))
ff = ot.get("ff")
if isinstance(ff, dict):
_set(g_ot_fail_code, ff.get("c"))
# --- Температурные датчики (в настройке thermometers) ---
thermometers = device.get("thermometers") or []
# Удаляем старые серии, чтобы не тянуть мёртвые датчики
for metric in (g_thermo_temp, g_thermo_state, g_thermo_last_update):
try:
metric.clear()
except Exception:
pass
for t in thermometers:
if not t.get("is_assigned_to_slot", True):
continue
uuid = str(t.get("uuid", ""))
name = str(t.get("name", ""))
ttype = str(t.get("type", ""))
last_state = t.get("last_state")
state_val = 1 if last_state == "ok" else 0
_set(g_thermo_temp, t.get("last_value"), uuid=uuid, name=name, type=ttype)
_set(g_thermo_state, state_val, uuid=uuid, name=name, type=ttype)
_set(g_thermo_last_update, t.get("last_value_time"), uuid=uuid, name=name)
# --- Связь / контроллер ---
_set(g_gsm_level, io.get("gsm-level"))
gsm_state = io.get("gsm-state")
if isinstance(gsm_state, dict):
_set(g_gsm_level, gsm_state.get("level"))
# state приходит строкой — приводим к int по справочнику
st_map = {
"not-registered": 0,
"home-network": 1,
"searching": 2,
"rejected": 3,
"roaming": 5,
"unknown": 255,
}
st = gsm_state.get("state")
if st in st_map:
_set(g_gsm_state, st_map[st])
_set(g_voltage, io.get("voltage"))
_set(g_wifi_rssi, io.get("wifi-rssi"))
_set(g_status_flags, io.get("status-flags"))
# --- Охрана ---
_set(g_guard_state, _guard_state_to_int(io.get("guard-state")))
_set(g_siren, _b(io.get("siren")))
# --- Автомобиль (ZTC) ---
_set(g_engine_block, _b(io.get("engine-block")))
_set(g_webasto, _b(io.get("webasto")))
_set(g_ignition, _b(io.get("ignition-state")))
_set(g_engine, _b(io.get("engine-state")))
auto = io.get("auto-ignition")
if isinstance(auto, dict):
_set(g_auto_ignition, _auto_ign_state_to_int(auto.get("state")))
# ---------------------------------------------------------------------------
# Обновление метрик из /load_data (история: ztc_state, temperature, ...)
# ---------------------------------------------------------------------------
def update_from_load_data(client: ZontClient, device_id: int) -> None:
"""
Догружаем некоторые исторические метрики,
которых нет в /devices: ztc_state, temperature (для H-серии).
Берём окно за последние 10 минут.
"""
now = int(time.time()) now = int(time.time())
mintime = now - 600 mintime = now - 3600
payload = {
"requests": [{
"device_id": DEVICE_ID,
"data_types": ["thermostat_work"],
"mintime": mintime,
"maxtime": now
}]
}
# ztc_state — есть не у всех устройств, ошибку игнорируем
try: try:
resp = client.load_data( resp = requests.post(
device_id=device_id, f"{API_BASE}/load_data",
data_types=["ztc_state"], headers={
mintime=mintime, 'X-ZONT-Client': ZONT_CLIENT,
maxtime=now, 'X-ZONT-Token': auth_token,
'Content-Type': 'application/json'
},
json=payload,
timeout=15
) )
st = resp.get("ztc_state") resp.raise_for_status()
if isinstance(st, dict): data = resp.json()
_set(g_gsm_level, _dta_last_value(st.get("gsm_level") or []))
_set(g_gsm_state, _dta_last_value(st.get("gsm_state") or [])) if not data.get('ok'):
_set(g_gsm_balance, _dta_last_value(st.get("gsm_balance") or [])) logging.error(f"API returned error: {data.get('error_ui', data.get('error'))}")
_set(g_voltage, _dta_last_value(st.get("voltage") or [])) # Если токен протух — сбрасываем его
_set(g_wifi_rssi, _dta_last_value(st.get("wifi_rssi") or [])) if data.get('error') in ['unauthorized', 'token_expired']:
_set(g_status_flags, _dta_last_value(st.get("status_flags") or [])) global auth_token
auth_token = None
return
responses = data.get('responses', [])
if not responses:
logging.warning("Empty responses from API.")
return
work_data = responses[0].get('thermostat_work')
if not work_data:
logging.info("No thermostat_work data in response.")
return
# 1. Время работы котла за последнюю минуту
boiler_work = fetch_last_value(work_data.get('boiler_work_time'))
if boiler_work is not None:
BOILER_WORK.labels(device_id=str(DEVICE_ID)).set(boiler_work)
# 2. Целевая температура
target_temp = fetch_last_value(work_data.get('target_temp'))
if target_temp is not None:
TARGET_TEMP.labels(device_id=str(DEVICE_ID)).set(target_temp)
# 3. Температура с датчиков (если есть в ответе)
# В thermostat_work датчики могут быть в отдельном поле или не быть вовсе.
# Здесь пример для поля temperature, если оно придёт.
temps = work_data.get('temperature')
if temps and isinstance(temps, dict):
for sensor_id, sensor_info in temps.items():
if not isinstance(sensor_info, dict): continue
name = sensor_info.get('name', sensor_id)
val = fetch_last_value(sensor_info.get('temperature'))
if val is not None:
TEMP_GAUGE.labels(
device_id=str(DEVICE_ID),
sensor_name=name,
sensor_type='thermostat_sensor'
).set(val)
logging.info(f"Metrics collected successfully for device {DEVICE_ID}")
temps = st.get("temperature") or []
if isinstance(temps, list):
for idx, series in enumerate(temps):
_set(
g_internal_temp,
_dta_last_value(series or []),
channel=str(idx),
)
except Exception as e: except Exception as e:
log.debug("ztc_state недоступен: %s", e) logging.error(f"Error during metric collection: {e}")
# temperature — есть у H-серии if __name__ == '__main__':
try: # Проверка обязательных переменных
resp = client.load_data( if not all([ZONT_LOGIN, ZONT_PASSWORD, DEVICE_ID]):
device_id=device_id, logging.critical("Missing required environment variables: ZONT_LOGIN, ZONT_PASSWORD, ZONT_DEVICE_ID")
data_types=["temperature"], exit(1)
mintime=mintime,
maxtime=now,
)
temp = resp.get("temperature")
if isinstance(temp, dict):
# НЕ очищаем здесь — иначе затрём данные из /devices.
# Просто дописываем исторические серии с суффиксом type=history.
for uuid, item in temp.items():
if not isinstance(item, dict):
continue
_set(
g_thermo_temp,
_dta_last_value(item.get("temperature") or []),
uuid=str(uuid),
name=str(item.get("name", "")),
type="history",
)
except Exception as e:
log.debug("temperature недоступен: %s", e)
# Первичная аутентификация
get_auth_token()
# --------------------------------------------------------------------------- # Запуск HTTP-сервера Prometheus на порту 9101
# Основной цикл start_http_server(9101)
# --------------------------------------------------------------------------- logging.info("Prometheus exporter started on port 9101")
def pick_device(client: ZontClient, preferred_id: int) -> Optional[dict]: # Основной цикл
"""Возвращает объект устройства (первое подходящее или с заданным ID).""" while True:
devices = client.devices(load_io=True) collect_metrics()
if not devices: time.sleep(SCRAPE_INTERVAL)
log.warning("У пользователя нет устройств")
return None
if preferred_id:
for d in devices:
if d.get("id") == preferred_id:
return d
log.warning("Устройство id=%s не найдено, беру первое", preferred_id)
return devices[0]
def scrape_once(client: ZontClient) -> None:
device = pick_device(client, ZONT_DEVICE_ID)
if device is None:
g_scrape_success.set(0)
return
update_from_devices(device)
update_from_load_data(client, device["id"])
g_scrape_success.set(1)
g_scrape_timestamp.set(time.time())
log.info(
"Метрики обновлены (device_id=%s, name=%s)",
device.get("id"),
device.get("name"),
)
def _handle_sigterm(signum, frame): # noqa: ARG001
log.info("Получен сигнал %s, завершаюсь...", signum)
sys.exit(0)
def main() -> None:
# Корректная остановка в контейнере (docker stop → SIGTERM)
signal.signal(signal.SIGTERM, _handle_sigterm)
signal.signal(signal.SIGINT, _handle_sigterm)
log.info("Запуск ZONT exporter на порту %d", EXPORTER_PORT)
log.info(
"Логин: %s, устройство: %s, интервал: %ds",
ZONT_LOGIN,
ZONT_DEVICE_ID or "auto",
ZONT_POLL_INTERVAL,
)
client = ZontClient(ZONT_LOGIN, ZONT_PASSWORD, ZONT_EMAIL, token=ZONT_TOKEN)
# Первый опрос синхронно, чтобы /metrics сразу отдавал данные
try:
scrape_once(client)
except Exception as e:
log.exception("Первый опрос не удался: %s", e)
g_scrape_success.set(0)
start_http_server(EXPORTER_PORT)
try:
while True:
time.sleep(ZONT_POLL_INTERVAL)
try:
scrape_once(client)
except Exception as e:
log.exception("Ошибка опроса: %s", e)
g_scrape_success.set(0)
except KeyboardInterrupt:
log.info("Остановка по сигналу")
if __name__ == "__main__":
main()