558 lines
No EOL
29 KiB
Markdown
Executable file
558 lines
No EOL
29 KiB
Markdown
Executable file
# 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`, `-w`, `-fd`, `-dt`).
|
||
|
||
**Интерактивное меню** (`showMenu`): ASCII-баннер, numbered menu, `bufio.Reader` для ввода. Поддерживает как цифры (`1`-`10`), так и текстовые алиасы (`-c`, `c`).
|
||
|
||
```
|
||
auspex [флаг] [аргументы]
|
||
│
|
||
├── -ss → runPortsView()
|
||
├── -c → runCertCheck()
|
||
├── -p → runPortCheck() [WAF, build tag]
|
||
├── -m → runMatchCheck(clientFilter) [WAF, build tag]
|
||
├── -l → runLogCheck() [WAF, build tag]
|
||
├── -sv → runServiceCheck()
|
||
├── -d → runDockerCheck()
|
||
├── -w → runWebCheck(sid) [WAF, build tag]
|
||
├── -fd → runFDCheck()
|
||
├── -dt → runDockerTUI()
|
||
├── -v → версия (+ вариант сборки: full/generic)
|
||
└── -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>` | ` ```текст``` ` |
|
||
| `━━━━━` | `---` |
|
||
| `<` `>` `&` | `<` `>` `&` |
|
||
| Прочие теги | удаляются |
|
||
|
||
---
|
||
|
||
### `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 |
|
||
|---|---|
|
||
| 61–120 | ⚠️ |
|
||
| 121–240 | 🔥 |
|
||
| > 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"}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
---
|
||
|
||
### `dockerview.go` — Docker Live TUI (аналог lazydocker)
|
||
|
||
**Точка входа:** `runDockerTUI()`
|
||
|
||
В отличие от `dockercheck.go` — это интерактивный инструмент прямого действия, а не cron-снапшот с диффом. Переиспользует `dcNormalizeStatus`/`dcGetHealth`/`dcStatusEmoji`/`dcCheckDockerAvailable` из `dockercheck.go` (тот же пакет `main`), не дублируя парсинг статуса.
|
||
|
||
**Реализация:** `bubbletea`/`bubbles`/`lipgloss` (те же зависимости, что и в `portsview.go`, новых пакетов в `go.mod` не добавляет — `viewport` уже часть модуля `bubbles`).
|
||
|
||
**Автообновление:** список контейнеров (`docker ps -a` + `docker inspect` на health) обновляется раз в 3 сек, если включено (`a`).
|
||
|
||
**Действия:**
|
||
|
||
| Клавиша | Действие | Механизм |
|
||
|---|---|---|
|
||
| `l` | Логи | `docker logs -f --tail 200 <name>`, терминал передаётся через `tea.ExecProcess` |
|
||
| `e` | Шелл в контейнер | `docker exec -it <name> sh -c 'exec bash \|\| exec sh'`, тоже через `tea.ExecProcess` |
|
||
| `i` | `docker inspect` | JSON форматируется через `encoding/json.Indent`, показывается в прокручиваемом `viewport` |
|
||
| `s`/`t`/`R`/`d` | stop/start/restart/remove (`rm -f`) | Через диалог подтверждения (`y`/`n`), показывающий точную команду |
|
||
| `r` / `a` | Ручной рефреш / автообновление | — |
|
||
|
||
`tea.ExecProcess` — ключевой механизм для логов и шелла: приостанавливает рендеринг Bubble Tea, отдаёт реальный терминал дочернему процессу (полноценный интерактивный сеанс — ввод, TTY), и восстанавливает TUI после его завершения. Работает только в интерактивном терминале (не через cron/pipe).
|
||
|
||
---
|
||
|
||
### `fdcheck.go` — удалённые-но-открытые файловые дескрипторы
|
||
|
||
**Точка входа:** `runFDCheck()`
|
||
|
||
Находит ситуации "место не освобождается после `rm`, потому что кто-то держит файл открытым".
|
||
|
||
**Алгоритм:**
|
||
1. Обход `/proc/[pid]/fd/*` по всем процессам, `os.Readlink` на каждый — ищем таргеты с суффиксом `" (deleted)"`
|
||
2. Фильтрация шума: memfd, anon_inode, сокеты/пайпы, всё вне обычной ФС — не считаются утечкой места
|
||
3. Размер — через `os.Stat` на `/proc/PID/fd/N` (следует по magic-symlink к живому, хоть и удалённому, инoду)
|
||
4. Таблица отсортирована по размеру, интерактивный CLI-выбор записи → меню действий
|
||
|
||
**Действия:**
|
||
|
||
| Действие | Механизм | Примечание |
|
||
|---|---|---|
|
||
| Recover | `io.Copy` из `/proc/PID/fd/N` в новый файл | Снимок данных, места не освобождает |
|
||
| Reopen (SIGHUP/SIGUSR1) | `syscall.Kill(pid, sig)` | Работает только если демон сам поддерживает reopen (nginx/angie/rsyslog) |
|
||
| Restart | `systemctl restart <unit>` | `unit` определяется через regex по `/proc/PID/cgroup` |
|
||
| Kill | SIGTERM → (3 сек) → SIGKILL | Освобождает место немедленно |
|
||
| Truncate на месте | `os.OpenFile(fdPath, O_WRONLY).Truncate(0)` | Тот же приём, что `logrotate copytruncate`; безопасно только для `O_APPEND`-файлов — режим открытия проверяется через `/proc/PID/fdinfo/N` (`flags:`, бит `O_APPEND`) |
|
||
|
||
Truncate — единственное действие, которое освобождает место **без рестарта демона**: `ftruncate` меняет размер inode, а не конкретный fd, поэтому воздействует на все файловые описания, ссылающиеся на тот же inode, включая fd исходного процесса.
|
||
|
||
---
|
||
|
||
### `webcheck.go` — проверка доступности сайтов
|
||
|
||
**Точка входа:** `runWebCheck(sidArg string)`
|
||
|
||
Проверяет HTTP-доступность доменов WAF-клиента через браузер **lynx**. Использование lynx позволяет обойти антибот-системы которые блокируют стандартные HTTP-клиенты (Go, Python, curl).
|
||
|
||
**Аргумент:** один или несколько SID через запятую:
|
||
```bash
|
||
auspex -w 14285
|
||
auspex -w 14285,11071,9823
|
||
```
|
||
|
||
**Алгоритм (один SID):**
|
||
1. Проверяет наличие `lynx` через `exec.LookPath` — если нет, понятная ошибка с командой установки
|
||
2. Подключается к PostgreSQL (primary → secondary failover) — один раз для всех SID
|
||
3. `wcGetDomains` — запрашивает `domain_name` и `aliases` из `sp_info WHERE sid = $1`, затем `client_title` из `apps_settings WHERE l7resourceid = $1`
|
||
4. Парсит `aliases` из JSONB-массива (`[]string`)
|
||
5. Очищает домены через `wcCleanDomain` — убирает markdown-ссылки вида `[текст](url)`, протоколы, слеши
|
||
6. Для каждого домена вызывает `wcCheckDomain` → `wcPrintResult`
|
||
7. Выводит итоговую статистику через `wcPrintSummary`
|
||
8. Сохраняет CSV-файл
|
||
|
||
**Алгоритм (несколько SID):**
|
||
|
||
При передаче нескольких SID утилита спрашивает способ сохранения CSV:
|
||
|
||
```
|
||
Несколько SID (3). Сохранить результаты:
|
||
1) В один файл (с разделителями по SID)
|
||
2) В отдельные файлы для каждого SID
|
||
```
|
||
|
||
- Вариант **1** — один файл `webcheck_{sid1}_{sid2}_{date}.csv` с секциями:
|
||
```
|
||
=== SID: 14285 | Название клиента ===
|
||
domain,protocol,http_code,available,checked_at
|
||
...
|
||
|
||
=== SID: 11071 | Другой клиент ===
|
||
...
|
||
```
|
||
- Вариант **2** — отдельный файл для каждого SID как обычно
|
||
|
||
БД и lynx инициализируются один раз независимо от количества SID.
|
||
|
||
**`wcGetDomains`** — два запроса к БД:
|
||
```sql
|
||
-- 1. Домены клиента
|
||
SELECT domain_name, aliases FROM sp_info WHERE sid = $1
|
||
|
||
-- 2. Название клиента (fallback: sid если не найдено)
|
||
SELECT client_title FROM apps_settings WHERE l7resourceid = $1 LIMIT 1
|
||
```
|
||
|
||
**`wcCheckDomain`:**
|
||
- Wildcard домены (`*.example.com`) — пропускаются немедленно с кодом `wildcard`
|
||
- Пробует `https://` → при DNS-ошибке возвращает `dns_error` без попытки http
|
||
- Пробует `http://` как fallback если https не дал HTTP-кода
|
||
- Если оба протокола не дали результата → `unavailable`
|
||
|
||
**`wcLynxGet(url)`** — ядро проверки:
|
||
```
|
||
lynx -head -dump -connect_timeout=10 -read_timeout=10 <url>
|
||
```
|
||
- `-head` — HTTP HEAD запрос (только заголовки, без загрузки тела)
|
||
- `-dump` — неинтерактивный режим, вывод в stdout
|
||
- Парсит HTTP-код регулярным выражением `HTTP/[\d.]+ (\d{3})`
|
||
- При редиректах берёт **последний** код — он актуальный
|
||
- DNS-ошибка определяется по строкам `Unable to locate remote host` и `Can't access startfile` в выводе lynx
|
||
|
||
**Коды результата:**
|
||
|
||
| HTTPCode | Значение |
|
||
|---|---|
|
||
| `200`, `301`, `302`... | HTTP-код ответа сервера |
|
||
| `dns_error` | Домен не резолвится |
|
||
| `wildcard` | Wildcard домен, пропущен |
|
||
| `error` | Оба протокола недоступны |
|
||
|
||
**`wcIsSuccessCode`** — считает успешными коды `2xx` и `3xx`.
|
||
|
||
**CSV-файл** сохраняется в `/var/log/auspex/` с UTF-8 BOM для корректного открытия в Excel:
|
||
- Один SID: `webcheck_{sid}_{date}.csv`
|
||
- Несколько SID (раздельно): `webcheck_{sid}_{date}.csv` для каждого
|
||
- Несколько SID (объединённо): `webcheck_{sid1}_{sid2}_..._{date}.csv`
|
||
|
||
**Просмотр CSV в терминале:**
|
||
```bash
|
||
column -t -s',' /var/log/auspex/webcheck_14285_2026-04-29.csv
|
||
```
|
||
|
||
### `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()` / `wcConnectDB()` — функции идентичны по логике, префиксы разные для избежания коллизий имён в пакете `main`.
|
||
|
||
`webcheck` также использует PostgreSQL для получения доменов из таблицы `sp_info`.
|
||
|
||
---
|
||
|
||
## Соглашения по именованию
|
||
|
||
Все идентификаторы в каждом модуле имеют уникальный префикс — это позволяет всем файлам жить в одном пакете `main` без конфликтов:
|
||
|
||
| Модуль | Префикс |
|
||
|---|---|
|
||
| certcheck | `cc` |
|
||
| portcheck | `pc` |
|
||
| logcheck | `lc` |
|
||
| matchcheck | `mc` |
|
||
| servicecheck | `sc` |
|
||
| dockercheck | `dc` |
|
||
| dockerview | `dv` |
|
||
| fdcheck | `fd` |
|
||
| email | `email` |
|
||
| mattermost | `mm` |
|
||
| webcheck | `wc` |
|
||
| config | `cfg` |
|
||
|
||
---
|
||
|
||
## Build tags: full vs generic
|
||
|
||
`logcheck.go`, `webcheck.go`, `portcheck.go`, `matchcheck.go` требуют БД `waf_info` и/или структуру каталогов PT AF — они помечены `//go:build waf` и компилируются только при `go build -tags waf .`.
|
||
|
||
Для generic-сборки (`go build .`, без тега) их заменяют файлы `*_stub.go` с обратным ограничением `//go:build !waf` — те же имена и сигнатуры функций (`runLogCheck`, `runWebCheck`, `runPortCheck`, `runMatchCheck`), но вместо логики — `fmt.Errorf` с подсказкой пересобрать с нужным тегом. Благодаря этому `main.go` не содержит условной компиляции и одинаково линкуется в обоих вариантах.
|
||
|
||
`buildinfo_waf.go` / `buildinfo_generic.go` по той же схеме задают константу `buildVariant` (`"full"` / `"generic"`), которую показывают баннер меню и `auspex -v`.
|
||
|
||
`lib/pq` используется только в трёх WAF-тегированных файлах — в generic-сборке он просто не компилируется (в `go.mod` остаётся объявленным, это нормально для двух конфигураций одного модуля; не стоит гонять `go mod tidy` без тега `waf`, иначе он может решить, что зависимость лишняя). |