All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Метрик в проекте не было ни одной: ни экспортеров, ни /metrics в бэкендах, единственный канал наблюдения — journald, единственный сигнал об аварии — исключение в GlitchTip. Из-за этого целый класс отказов невидим в принципе: задача рапортует done, строк ноль, исключения нет. Так протухли данные на семь месяцев (#2998), 34 дня был мёртв house_imv_backfill (#2698), 8 суток писал ноль newbuilding_enrich (#2767), 91 день копилось раздутие listings (#2992). Grafana не заменяет GlitchTip: ошибки остаются там. Grafana OSS не принимает Sentry DSN ни одним компонентом, а скрубберы в before_send — требование 152-ФЗ. Здесь появляется другой класс данных: ряды и алерты по трендам. Наблюдатель поставлен у ДРУГОГО провайдера, чем наблюдаемое: серверная сторона на Beget, рядом с GlitchTip. Если ляжет Poincare, мониторинг должен об этом сказать, а не лечь вместе с ним. Транспорт push, а не pull: агент на Poincare шлёт remote_write и логи исходящим HTTPS, поэтому там не открывается ни одного входящего порта сверх 22/80/443. При обрыве канала Alloy копит в WAL и досылает — pull-скрейп в той же ситуации терял бы точки именно в аварии, ради которой мониторинг и нужен. Два контура доступа с разными учётками. Пароль приёмника по построению лежит открытым на продуктовом хосте, значит его компрометация неизбежна вместе с хостом; будь это учётка витрины, утёк бы и доступ к дашбордам. GlitchTip читается прямым SQL, а не Sentry-плагином: у плагина на 6.1.6 stats_v2 отдаёт 500 (баг GlitchTip #381), Events/Discover — 404 (#416), а в grafana/sentry-datasource слово glitchtip не встречается ни разу. Схема сверена на живой базе: колонка времени называется timestamp, а не received, и отдельной таблицы IssueIndex не существует — агрегаты лежат на самой issue_events_issue. Алерты за профилем alerts: канал доставки — открытый вопрос #3078, и стек не должен на нём стоять. Деплой предупреждает, что уведомлять пока некому. Каждая настройка, способная отказать молча, закрыта явно: ретенция Prometheus задана и по времени и по размеру, retention_enabled у компактора Loki (без него retention_period не работает вовсе), путь к журналу и запуск Alloy от root (иначе агент читает ноль записей без ошибки), проверка Caddy до перезагрузки (на этом хосте тот же Caddy держит git, errors и obsidian). Refs #3078
77 lines
3.1 KiB
Bash
77 lines
3.1 KiB
Bash
#!/usr/bin/env bash
|
||
# Собирает строки подключения для postgres_exporter на продуктовом хосте из
|
||
# паролей, которые там уже есть. Идемпотентен: заданные значения не трогает.
|
||
#
|
||
# ПОЧЕМУ НЕ ЗАВОДИМ ОТДЕЛЬНЫЕ СЕКРЕТЫ. Каждая новая копия пароля — ещё одно
|
||
# место, откуда он может утечь, и ещё одно, которое забудут повернуть при
|
||
# ротации. Экспортеру нужны те же учётки, что уже лежат в окружении хоста.
|
||
#
|
||
# Запускать на продуктовом хосте. Вызывается из deploy-metrics.yml.
|
||
set -euo pipefail
|
||
|
||
ENVF=/opt/gendesign/backend/.env.runtime
|
||
|
||
if [ ! -f "$ENVF" ]; then
|
||
echo " ОШИБКА: не найден файл окружения бэкенда." >&2
|
||
exit 1
|
||
fi
|
||
|
||
set -a
|
||
# shellcheck source=/dev/null
|
||
. "$ENVF"
|
||
set +a
|
||
|
||
add_key() {
|
||
local key="$1" value="$2"
|
||
if grep -qE "^${key}=" "$ENVF" 2>/dev/null; then
|
||
echo " $key — уже задан, не трогаю"
|
||
return 0
|
||
fi
|
||
if [ -z "$value" ]; then
|
||
echo " $key — нечем заполнить, пропускаю (метрики этой базы не поедут)"
|
||
return 0
|
||
fi
|
||
printf '%s=%s\n' "$key" "$value" >> "$ENVF"
|
||
echo " $key — записан"
|
||
}
|
||
|
||
# Экспортер ходит по сетевому алиасу, а не по IP: адреса контейнеров меняются
|
||
# при каждом пересоздании, и DSN с IP протух бы на первом же деплое.
|
||
add_key GENDESIGN_EXPORTER_DSN \
|
||
"${GENDESIGN_EXPORTER_DSN:-${DATABASE_URL:-}}"
|
||
|
||
add_key TRADEIN_EXPORTER_DSN \
|
||
"${TRADEIN_EXPORTER_DSN:-${TRADEIN_DATABASE_URL:-}}"
|
||
|
||
# postgres_exporter не понимает схему postgresql+psycopg:// из SQLAlchemy —
|
||
# ему нужна чистая postgresql://. Приводим, если досталась приложенческая форма.
|
||
python3 - "$ENVF" <<'PY'
|
||
import io, re, sys
|
||
|
||
path = sys.argv[1]
|
||
with io.open(path, encoding="utf-8") as fh:
|
||
lines = fh.readlines()
|
||
|
||
changed = False
|
||
out = []
|
||
for line in lines:
|
||
m = re.match(r'^((?:GENDESIGN|TRADEIN)_EXPORTER_DSN)=(.*)$', line.rstrip('\n'))
|
||
if not m:
|
||
out.append(line)
|
||
continue
|
||
key, dsn = m.group(1), m.group(2)
|
||
fixed = re.sub(r'^postgresql\+\w+://', 'postgresql://', dsn)
|
||
fixed = re.sub(r'^postgres\+\w+://', 'postgresql://', fixed)
|
||
if 'sslmode=' not in fixed:
|
||
fixed += ('&' if '?' in fixed else '?') + 'sslmode=disable'
|
||
if fixed != dsn:
|
||
changed = True
|
||
print(" %s — схема приведена к postgresql:// для экспортера" % key)
|
||
out.append("%s=%s\n" % (key, fixed))
|
||
|
||
if changed:
|
||
with io.open(path, "w", encoding="utf-8", newline="\n") as fh:
|
||
fh.writelines(out)
|
||
PY
|
||
|
||
echo " строки подключения экспортеров готовы"
|