auspex/ARCHITECTURE.md
2026-04-16 15:21:23 +03:00

395 lines
No EOL
18 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Auspex — Architecture
## Обзор
Auspex — монолитное CLI-приложение на Go. Все модули живут в одном пакете `main`, конфигурация глобальная, каналы доставки алертов shared. Такой подход выбран осознанно — утилита запускается по cron, живёт секунды, сложная изоляция не нужна.
```
┌─────────────────────────────────────────────────────┐
│ auspex │
│ │
│ main.go ──► runByFlag() / showMenu() │
│ │ │
│ ┌──────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ certcheck portcheck logcheck ... │
│ │ │ │ │
│ └──────────┴──────────────┘ │
│ │ │
│ Alert Pipeline │
│ ┌──────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ telegram email mattermost │
└─────────────────────────────────────────────────────┘
```
---
## Файлы и их роли
### `main.go` — точка входа
Отвечает за два режима запуска:
**CLI-флаги** (`runByFlag`): прямой вызов нужного модуля по флагу (`-c`, `-p`, `-l`, `-sv`, `-d`, `-m`, `-ss`).
**Интерактивное меню** (`showMenu`): ASCII-баннер, numbered menu, `bufio.Reader` для ввода. Поддерживает как цифры (`1`-`7`), так и текстовые алиасы (`-c`, `c`).
```
auspex [флаг] [аргументы]
├── -ss → runPortsView()
├── -c → runCertCheck()
├── -p → runPortCheck()
├── -m → runMatchCheck(clientFilter)
├── -l → runLogCheck()
├── -sv → runServiceCheck()
├── -d → runDockerCheck()
├── -v → версия
└── -h → usage
```
---
### `config.go` — конфигурация
Все настройки — глобальные переменные вида `cfgXxx`. Загрузка из `/etc/auspex/auspex.env` при старте через `loadConfig()``applyConfig()`.
Формат `.env`:
```
KEY=VALUE # кавычки обрезаются автоматически
# комментарии игнорируются
```
Если файл не найден — используются значения по умолчанию, вывод предупреждения в stdout. Программа продолжает работу.
**Группы переменных:**
- Telegram (токен, chat ID, прокси)
- Mattermost (URL, токен бота, channel ID)
- Email/SMTP (хост, порт, логин, пароль, from, to)
- PostgreSQL (user, password, dbname, port, primary host, secondary host)
- Пути (SSL dir, nginx glob, log dir, state files)
- Пороги (cert days, log stale minutes)
- Параметры проверки (timeout, retries, workers)
- Список служб для проверки
**Вспомогательные функции:**
- `isTelegramConfigured()` — проверяет наличие токена и chat ID
- `isEmailConfigured()` — проверяет host, from, to
- `isMattermostConfigured()` — проверяет URL, токен, channel ID
- `printConfigStatus()` — выводит статус всех каналов при запуске
---
### `telegram.go` — доставка в Telegram
**Функции отправки:**
- `telegramSendHTML(message)` — HTML-форматирование (теги `<b>`, `<code>`, `<i>`)
- `telegramSendPlainText(message)` — plain text без форматирования (используется в portcheck)
**Цепочка прокси (`telegramHTTPClients`):**
Возвращает срез `[]*http.Client` в порядке приоритета:
```
1. HTTP прокси (если задан TELEGRAM_HTTP_PROXY)
2. SOCKS5 (если задан TELEGRAM_SOCKS5_PROXY)
3. Прямое (всегда, как последний fallback)
```
`telegramDoPost` и `telegramDoPostForm` перебирают клиентов по очереди. При ошибке одного — переходят к следующему с логом предупреждения. Успешный вариант логируется.
**SOCKS5:** реализован через `golang.org/x/net/proxy`. Логин/пароль задаются отдельными переменными (не в URL) — это исключает проблемы со спецсимволами вроде `!` и `$` в паролях.
**HTTP прокси:** реализован через стандартный `http.Transport{Proxy: http.ProxyURL(...)}`.
---
### `email.go` — доставка по Email
**`emailSendHTML(subject, message)`** — отправка HTML-письма через SMTP.
Особенности реализации:
- Telegram-теги (`<b>`, `<code>`, `<i>`) являются валидным HTML и отображаются в письме без конвертации
- `\n` заменяются на `<br>` для корректного отображения переносов строк
- Письмо оборачивается в HTML-шаблон с CSS стилями (`emailBuildHTML`)
- Поддерживается `AUTH LOGIN` (тип авторизации который использует Microsoft Exchange и многие корпоративные серверы) через кастомный `emailLoginAuth`, реализующий интерфейс `smtp.Auth`
- Стандартный `smtp.PlainAuth` намеренно не используется — он несовместим с рядом серверов
- Порт 587 + STARTTLS работает автоматически через `smtp.SendMail`
- `EMAIL_TO` поддерживает несколько адресов через запятую
**Диагностика при неполной конфигурации:**
- Не задан HOST/FROM/TO → лог `[Email] Не настроен — пропускаем`
- Не задан USER или PASSWORD → предупреждение, попытка без авторизации (для relay-серверов)
- Ошибка SMTP → лог с кодом ответа и телом ошибки
---
### `mattermost.go` — доставка в Mattermost
**`mattermostSend(message)`** — отправка через Bot API.
Endpoint: `POST /api/v4/posts` с заголовком `Authorization: Bearer <token>`.
Успешный ответ: `201 Created` (не `200`, как в большинстве API).
**`mmHTMLToMarkdown(html)`** — конвертер форматирования:
| HTML (Telegram) | Markdown (Mattermost) |
|---|---|
| `<b>текст</b>` | `**текст**` |
| `<i>текст</i>` | `_текст_` |
| `<code>текст</code>` | `` `текст` `` |
| `<pre>текст</pre>` | ` ```текст``` ` |
| `━━━━━` | `---` |
| `&lt;` `&gt;` `&amp;` | `<` `>` `&` |
| Прочие теги | удаляются |
---
### `certcheck.go` — проверка SSL сертификатов
**Точка входа:** `runCertCheck()`
**Алгоритм:**
1. `ccFindCerts(cfgSSLDir)` — рекурсивный обход директории, фильтрация по расширениям (`.crt`, `.pem`, `.cer`, `.cert`, `.der`, `.p7b`, `.p7c`, `.p7s`, `.csr`)
2. Параллельный парсинг через пул горутин (`cfgMaxWorkers`)
3. `ccParseCert(path)` — определение формата (PEM/DER), парсинг через `crypto/x509`, вычисление `daysLeft`
4. `ccCheckAndAlert(cert)` — сравнение с порогами и отправка алерта
**Уровни алертов:**
| Условие | Emoji | Уровень |
|---|---|---|
| `daysLeft < 0` | 💀💀💀 | Сертификат истёк |
| `daysLeft <= CERT_CRITICAL_DAYS` | 🚨🚨🚨 | Критично |
| `daysLeft <= CERT_WARNING_DAYS` | ⚠️⚠️ | Предупреждение |
| `daysLeft <= CERT_INFO_DAYS` | | Информация |
| Иначе | — | Алерт не нужен |
---
### `portcheck.go` — проверка доступности портов
**Точка входа:** `runPortCheck()`
**Алгоритм:**
1. Подключение к PostgreSQL (primary → secondary failover)
2. Загрузка карты `check_ports` из таблицы `apps_settings` — какие конфиги проверять
3. `pcParseNginxConfig(file)` — парсинг `upstream { server ip:port; }` блоков regex'ом
4. Дедупликация endpoint'ов по всем файлам
5. Параллельная TCP-проверка (`net.DialTimeout`) с повторами (`cfgMaxRetries`)
6. `pcCompareStates` — сравнение с предыдущим state-файлом
7. Отправка алерта только при изменениях
**State-файл** (`/tmp/port_checker_state.json`):
```json
{
"timestamp": "2026-04-14 13:10:10",
"status": {
"91.206.126.83:443": {"endpoint": "...", "available": true, "config_file": "Client_12345"}
}
}
```
**Имена конфигов** строятся из имени файла без расширения: `Client_12345.conf``Client_12345`.
---
### `logcheck.go` — проверка свежести логов
**Точка входа:** `runLogCheck()`
**Алгоритм (4 шага через PostgreSQL):**
1. `lcGetInstancesByHostname` — инстансы для текущего хоста (`instances_new`)
2. `lcGetClientsByInstances` — клиенты для этих инстансов (`client_info`)
3. `lcGetAppsByClients` — приложения с `check_write_logs = true` (`apps_settings`)
4. `lcCheckLogFile` — поиск файла по паттерну `{LOG_DIR}/*/*/{l7resourceid}*_access.log`, проверка `ModTime`
Файл считается устаревшим если `time.Since(ModTime) > cfgLogStaleMinutes`.
**Уровни в алерте** по времени с последней записи:
| Минут | Emoji |
|---|---|
| 61120 | ⚠️ |
| 121240 | 🔥 |
| > 240 | 💀 |
---
### `matchcheck.go` — проверка совпадений Angie ↔ Docker
**Точка входа:** `runMatchCheck(clientFilter)`
Проверяет соответствие между портами в конфигах Angie и запущенными Docker контейнерами. Поддерживает фильтрацию по имени клиента.
**Алгоритм:**
1. Парсинг конфигов Angie из `cfgAngieConfDir` — извлечение портов на которых слушает Angie
2. `docker ps` — список запущенных контейнеров с их портами
3. Сопоставление: для каждого клиента проверяется есть ли соответствующий контейнер и совпадают ли порты
4. Алерт при расхождениях
---
### `servicecheck.go` — проверка служб systemd
**Точка входа:** `runServiceCheck()`
**Алгоритм:**
1. Параллельная проверка всех служб из `cfgServicesToCheck` через горутины
2. `scCheckService(name)``systemctl is-active <name>` + `systemctl show <name> --property=SubState,Description`
3. Загрузка предыдущего state-файла
4. `scCompareStates` — сравнение текущих статусов с предыдущими
5. Алерт только при изменениях
**State-файл** (`/tmp/auspex_service_state.json`):
```json
{
"timestamp": "2026-04-14 13:00:00",
"services": {
"angie": "active",
"docker": "failed"
}
}
```
**Типы изменений в алерте:**
| Изменение | Emoji |
|---|---|
| `active` → любой другой | ❌ |
| любой → `active` | ✅ (восстановление) |
| `failed` | 💀 |
| `inactive` | 🔴 |
| `activating/deactivating` | 🟡 |
---
### `dockercheck.go` — проверка Docker контейнеров
**Точка входа:** `runDockerCheck()`
**Алгоритм:**
1. `dcCheckDockerAvailable()``docker info` для проверки доступности демона
2. `dcGetContainers()``docker ps -a --format '{{.ID}}|{{.Names}}|{{.Status}}|{{.Image}}'`
3. `dcGetHealth(id)``docker inspect --format '{{if .State.Health}}...{{end}}'` для каждого контейнера отдельно (`.Health` недоступен в `docker ps` в ряде сборок)
4. Нормализация статуса: `"Up 2 hours"``"running"`, `"Exited (1) 3 min ago"``"exited"`
5. `dcCompareStates` — сравнение с предыдущим state-файлом
6. Алерт только при изменениях
**Типы событий:**
| Kind | Условие | Emoji |
|---|---|---|
| `appeared` | Новый контейнер (не при первом запуске) | 🆕 |
| `disappeared` | Контейнер исчез из `docker ps -a` | 👻 |
| `status_changed` → не running | Упал/завис | 🔴 💀 🔄 |
| `status_changed` → running | Восстановился | ✅ |
| `health_changed` → unhealthy | Нездоров | 🤒 |
| `health_changed` → healthy | Оздоровился | ✅ |
**State-файл** (`/tmp/auspex_docker_state.json`):
```json
{
"timestamp": "2026-04-14 13:00:00",
"containers": {
"portainer": {"status": "running", "health": "none"},
"zabbix-agent-7.0": {"status": "running", "health": "none"}
}
}
```
---
### `portsview.go` — TUI просмотр портов
**Точка входа:** `runPortsView()`
Интерактивный TUI на базе [Bubble Tea](https://github.com/charmbracelet/bubbletea). Показывает занятые порты на хосте (`ss -tlnp` / `netstat`). Поддерживает фильтрацию, навигацию клавишами.
---
## Pipeline доставки алертов
Все модули с алертами следуют единому паттерну:
```go
// 1. Telegram
if err := telegramSendHTML(message); err != nil {
log.Printf("❌ Ошибка отправки в Telegram: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен в Telegram")
}
// 2. Email
if err := emailSendHTML("Тема", message); err != nil {
log.Printf("❌ Ошибка отправки Email: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен на Email")
}
// 3. Mattermost
if err := mattermostSend(message); err != nil {
log.Printf("❌ Ошибка отправки в Mattermost: %v", err)
} else {
log.Println("✅ Алерт успешно отправлен в Mattermost")
}
```
Все три вызова независимы — ошибка одного не влияет на остальные.
---
## Паттерн state-файлов
Модули `-p`, `-sv`, `-d` используют одинаковый механизм для предотвращения спама при запуске по cron:
```
Запуск N: Загрузить prev_state → Получить curr_state → Compare → Alert if changed → Save curr_state
Запуск N+1: Загрузить prev_state → Получить curr_state → Compare → ...
```
**Первый запуск** (state-файл отсутствует): состояние сохраняется, алерты не отправляются. Исключение: в `dockercheck` появление нового контейнера (`dcKindAppeared`) тоже не алертится при первом запуске — флаг `isFirstRun` передаётся в `dcCompareStates`.
---
## Параллелизм
| Модуль | Механизм |
|---|---|
| `certcheck` | Пул горутин (`cfgMaxWorkers`) через канал задач |
| `servicecheck` | Горутина на каждую службу + `sync.WaitGroup` |
| `dockercheck` | Последовательно (inspect на каждый контейнер отдельно) |
| `portcheck` | Горутина на каждый endpoint |
---
## Работа с PostgreSQL
Модули `portcheck` и `logcheck` используют PostgreSQL с failover:
```
primary (cfgPrimaryDBHost) → ping OK? → использовать
→ ping fail → secondary (cfgSecondaryDBHost)
```
Каждый модуль реализует свой `pcConnectDB()` / `lcConnectDB()` — функции идентичны по логике, префиксы разные для избежания коллизий имён в пакете `main`.
---
## Соглашения по именованию
Все идентификаторы в каждом модуле имеют уникальный префикс — это позволяет всем файлам жить в одном пакете `main` без конфликтов:
| Модуль | Префикс |
|---|---|
| certcheck | `cc` |
| portcheck | `pc` |
| logcheck | `lc` |
| matchcheck | `mc` |
| servicecheck | `sc` |
| dockercheck | `dc` |
| email | `email` |
| mattermost | `mm` |
| config | `cfg` |