Zont exporter init
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
name: Build and Push
|
||||
on:
|
||||
push:
|
||||
#branches:
|
||||
# - main
|
||||
# - 'feature/**'
|
||||
|
||||
jobs:
|
||||
maintence:
|
||||
runs-on: srvdnd
|
||||
outputs:
|
||||
date: ${{ steps.date.outputs.date }}
|
||||
steps:
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(TZ=":Asia/Yakutsk" date +'%FT%H-%M-%SR')" >> $GITHUB_OUTPUT
|
||||
|
||||
buildApp:
|
||||
needs: [maintence]
|
||||
runs-on: srvdnd
|
||||
env:
|
||||
TAG_NAME: ${{ needs.maintence.outputs.date }}${{ gitea.RUN_ID }}
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: https://git.tlgrf.ru/actions/checkout@v4
|
||||
- name: Create docker image!
|
||||
run: docker build -t git.tlgrf.local/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:$TAG_NAME -t git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:$TAG_NAME -t git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:latest ./zont-exporter -f ./zont-exporter/Dockerfile
|
||||
|
||||
pushApp:
|
||||
needs: [buildApp, maintence]
|
||||
runs-on: srvdnd
|
||||
env:
|
||||
TAG_NAME: ${{ needs.maintence.outputs.date }}${{ gitea.RUN_ID }}
|
||||
steps:
|
||||
- name: Login to git.tlgrf.ru
|
||||
uses: https://git.tlgrf.ru/actions/docker-login-action@v3
|
||||
with:
|
||||
registry: git.tlgrf.ru
|
||||
username: gitea
|
||||
password: ${{ secrets.GITEAPASSWORD }}
|
||||
- name: Push docker image!
|
||||
run: docker push git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:$TAG_NAME && docker push git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:latest
|
||||
|
||||
- name: Push notification
|
||||
uses: https://git.tlgrf.ru/actions/rocketchat-notification-action@v3.0.0
|
||||
with:
|
||||
server: https://rc.tlgrf.ru
|
||||
auth-token: ${{ secrets.ROCKETCHAT_AUTH_TOKEN }}
|
||||
user-id: ${{ secrets.ROCKETCHAT_USER_ID }}
|
||||
channel: python_rocketchat_api
|
||||
message: |
|
||||
Gitea | TLGRF
|
||||
|
||||
Repository: ${{ gitea.REPOSITORY }}
|
||||
*${{ gitea.ACTOR }}* pushed to *${{ gitea.REF_NAME }}*
|
||||
|
||||
Commit message: ${{ gitea.event.commits[0].message }}
|
||||
See changes: https://git.tlgrf.ru/${{ gitea.REPOSITORY }}/commit/${{gitea.SHA}}
|
||||
```
|
||||
docker pull git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:${{ env.TAG_NAME }}
|
||||
```
|
||||
```
|
||||
${{ env.TAG_NAME }}
|
||||
```
|
||||
|
||||
#- name: send telegram message on push
|
||||
# uses: appleboy/telegram-action@master
|
||||
# with:
|
||||
# to: -4251930066
|
||||
# token: ${{ secrets.TELEGRAM_TOKEN_MIKUPTIME }}
|
||||
# format: markdown
|
||||
# message: |
|
||||
# Gitea | TLGRF
|
||||
#
|
||||
# Repository: ${{ gitea.REPOSITORY }}
|
||||
# *${{ gitea.ACTOR }}* pushed to *${{ gitea.REF_NAME }}*
|
||||
#
|
||||
# Commit message: ${{ gitea.event.commits[0].message }}
|
||||
# See changes: https://git.tlgrf.ru/${{ gitea.REPOSITORY }}/commit/${{gitea.SHA}}
|
||||
# ```
|
||||
# docker pull git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:${{ env.TAG_NAME }}
|
||||
# ```
|
||||
# ```
|
||||
# ${{ env.TAG_NAME }}
|
||||
# ```
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
LABEL maintainer="zont-exporter"
|
||||
|
||||
# Отключаем создание .pyc файлов и включаем небуферизованный вывод
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Устанавливаем зависимости
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем исходный код
|
||||
COPY zont_exporter.py .
|
||||
|
||||
# Создаём непривилегированного пользователя
|
||||
RUN useradd -m -u 1000 exporter && chown -R exporter:exporter /app
|
||||
USER exporter
|
||||
|
||||
# Открываем порт экспортера
|
||||
EXPOSE 8000
|
||||
|
||||
# Healthcheck — проверяем, что /metrics отвечает
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/metrics').read()" || exit 1
|
||||
|
||||
CMD ["python", "zont_exporter.py"]
|
||||
@@ -0,0 +1,2 @@
|
||||
requests>=2.31.0
|
||||
prometheus-client>=0.19.0
|
||||
@@ -0,0 +1,173 @@
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
from prometheus_client import start_http_server, Gauge
|
||||
from prometheus_client.core import GaugeMetricFamily, REGISTRY
|
||||
|
||||
# --- Настройка логирования ---
|
||||
logging.basicConfig(
|
||||
level=os.getenv('LOG_LEVEL', 'INFO').upper(),
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('zont-exporter')
|
||||
|
||||
# --- Конфигурация из переменных окружения ---
|
||||
ZONT_LOGIN = os.getenv('ZONT_LOGIN')
|
||||
ZONT_PASSWORD = os.getenv('ZONT_PASSWORD')
|
||||
ZONT_CLIENT_EMAIL = os.getenv('ZONT_CLIENT_EMAIL', 'exporter@localhost')
|
||||
API_URL = os.getenv('ZONT_API_URL', 'https://my.zont.online/api')
|
||||
SCRAPE_INTERVAL = int(os.getenv('SCRAPE_INTERVAL', '60'))
|
||||
EXPORTER_PORT = int(os.getenv('EXPORTER_PORT', '8000'))
|
||||
|
||||
# Проверка обязательных параметров
|
||||
if not ZONT_LOGIN or not ZONT_PASSWORD:
|
||||
logger.error("Не заданы ZONT_LOGIN и/или ZONT_PASSWORD")
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
class ZontCollector(object):
|
||||
"""Кастомный коллектор, который собирает метрики с API ZONT."""
|
||||
|
||||
def __init__(self):
|
||||
self.token = None
|
||||
|
||||
def _get_auth_token(self):
|
||||
"""Получает или обновляет токен авторизации."""
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{API_URL}/get_authtoken',
|
||||
auth=(ZONT_LOGIN, ZONT_PASSWORD),
|
||||
headers={'X-ZONT-Client': ZONT_CLIENT_EMAIL},
|
||||
json={"client_name": "Prometheus-Exporter"},
|
||||
timeout=15
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get('ok'):
|
||||
self.token = data['token']
|
||||
logger.info("Токен успешно получен.")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Ошибка получения токена: {data.get('error_ui')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Сетевая ошибка при получении токена: {e}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Неожиданная ошибка при получении токена: {e}")
|
||||
return False
|
||||
|
||||
def _fetch_data(self):
|
||||
"""Запрашивает данные по всем устройствам."""
|
||||
if not self.token:
|
||||
if not self._get_auth_token():
|
||||
return None
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{API_URL}/devices',
|
||||
headers={
|
||||
'X-ZONT-Client': ZONT_CLIENT_EMAIL,
|
||||
'X-ZONT-Token': self.token
|
||||
},
|
||||
json={'load_io': True},
|
||||
timeout=15
|
||||
)
|
||||
|
||||
if response.status_code == 403:
|
||||
logger.warning("Токен недействителен, обновляем...")
|
||||
if self._get_auth_token():
|
||||
return self._fetch_data()
|
||||
return None
|
||||
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get('ok'):
|
||||
return data.get('devices', [])
|
||||
else:
|
||||
logger.error(f"Ошибка в ответе devices: {data.get('error_ui')}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Сетевая ошибка при запросе devices: {e}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Неожиданная ошибка при запросе devices: {e}")
|
||||
return None
|
||||
|
||||
def collect(self):
|
||||
"""Основной метод, который вызывает Prometheus."""
|
||||
devices = self._fetch_data()
|
||||
if not devices:
|
||||
logger.warning("Не удалось получить данные, пропускаем цикл.")
|
||||
return
|
||||
|
||||
# --- Метрики температур ---
|
||||
temp_gauge = GaugeMetricFamily(
|
||||
'zont_temperature_celsius',
|
||||
'Температура с датчиков ZONT',
|
||||
labels=['device_id', 'device_name', 'sensor_name']
|
||||
)
|
||||
|
||||
# --- Метрики состояния котла ---
|
||||
boiler_work_gauge = GaugeMetricFamily(
|
||||
'zont_boiler_working',
|
||||
'Работает ли котел (1 - да, 0 - нет)',
|
||||
labels=['device_id', 'device_name']
|
||||
)
|
||||
|
||||
# --- Метрики уровня сигнала ---
|
||||
signal_gauge = GaugeMetricFamily(
|
||||
'zont_signal_level',
|
||||
'Уровень сигнала (GSM или Wi-Fi)',
|
||||
labels=['device_id', 'device_name', 'type']
|
||||
)
|
||||
|
||||
# --- Метрики состояния охраны ---
|
||||
guard_gauge = GaugeMetricFamily(
|
||||
'zont_guard_state',
|
||||
'Состояние охраны (1 - включена, 0 - выключена)',
|
||||
labels=['device_id', 'device_name']
|
||||
)
|
||||
|
||||
for device in devices:
|
||||
dev_id = str(device.get('id'))
|
||||
dev_name = device.get('name', 'unknown')
|
||||
io_data = device.get('io', {})
|
||||
if not io_data:
|
||||
continue
|
||||
|
||||
# --- Обработка термодатчиков ---
|
||||
for sensor in device.get('thermometers', []):
|
||||
if sensor.get('is_assigned_to_slot') and sensor.get('last_value') is not None:
|
||||
temp_gauge.add_metric(
|
||||
[dev_id, dev_name, sensor.get('name', 'sensor')],
|
||||
sensor['last_value']
|
||||
)
|
||||
|
||||
# --- Обработка состояния котла ---
|
||||
boiler_state = io_data.get('last-boiler-state')
|
||||
if boiler_state:
|
||||
is_working = 1 if boiler_state.get('boiler_work_time', 0) > 0 else 0
|
||||
boiler_work_gauge.add_metric([dev_id, dev_name], is_working)
|
||||
|
||||
# --- Обработка уровня сигнала GSM ---
|
||||
gsm_state = io_data.get('gsm-state')
|
||||
if gsm_state and gsm_state.get('level') is not None:
|
||||
signal_gauge.add_metric([dev_id, dev_name, 'gsm'], gsm_state['level'])
|
||||
|
||||
# --- Обработка состояния охраны ---
|
||||
guard_state = io_data.get('guard-state')
|
||||
if guard_state is not None:
|
||||
is_guarded = 1 if guard_state == 'enabled' else 0
|
||||
guard_gauge.add_metric([dev_id, dev_name], is_guarded)
|
||||
|
||||
yield temp_gauge
|
||||
yield boiler_work_gauge
|
||||
yield signal_gauge
|
||||
yield guard_gauge
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
REGISTRY.register(ZontCollector())
|
||||
start_http_server(EXPORTER_PORT)
|
||||
logger.info(f"Экспортер запущен на порту {EXPORTER_PORT}")
|
||||
|
||||
while True:
|
||||
time.sleep(10)
|
||||
Reference in New Issue
Block a user