Обновление

Перед началом: сделайте резервную копию

Обновление применяет миграции схемы баз данных — это необратимо (обратных миграций «на всякий случай» никто не запускает автоматически). Прежде чем обновлять, снимите бэкап и PostgreSQL, и ClickHouse — см. Резервное копирование и восстановление. Не пропускайте этот шаг, даже если обновление кажется мелким.

Что меняется при обновлении с версий до 0.4.2: детали алертов уходят не всем

Раньше решение «слать ли в уведомление текст ошибки» принималось по типу канала: Telegram и вебхук считались внешними, почта — своей, и на любой почтовый адрес уезжал полный текст. Теперь оно принимается по получателю: доверенным считается адрес на хосте вашего инстанса, на домене из GOTCHA_TRUSTED_RECIPIENTS или во внутренней сети.

Что это значит на практике:

  • почта на публичном сервисе (@gmail.com, @yandex.ru и т. п.) больше не получает текст ошибки — только ссылку на проблему в интерфейсе;
  • почта на домене вашей организации, если этот домен отличается от хоста инстанса, тоже перестанет получать детали, пока вы не укажете его в GOTCHA_TRUSTED_RECIPIENTS;
  • вебхук на вашу внутреннюю инфраструктуру теперь, наоборот, детали получает — раньше для этого приходилось включать GOTCHA_EXTERNAL_CHANNEL_DETAILS_ENABLED глобально, открывая заодно и Telegram.

Если ваша почта живёт на домене организации, добавьте его перед обновлением:

GOTCHA_TRUSTED_RECIPIENTS=corp.example

Проверить, что применилось, можно по логу старта — строка alert details: sent only to trusted recipients перечисляет действующий список. Подробности — в разделе Приватность и 152-ФЗ.

Разовое действие при обновлении с версий до 0.4.2: перевыпустите секреты каналов

До этой версии секрет канала доставки (bot-токен Telegram, ключ подписи вебхука) клался в очередь уведомлений notification_outbox в открытом виде — колонка payload это обычный jsonb, и шифрование alert_channels.secret этим обесценивалось. Миграция 0025 вычищает такие значения из очереди при обновлении, а новый код их туда больше не кладёт: секрет достаётся по идентификатору канала в момент отправки.

Но миграция чистит только живую базу. Если инстанс работал на старой версии, эти секреты лежат открытым текстом во всех резервных копиях PostgreSQL, снятых до обновления, — а бэкапы вы, надеемся, снимаете регулярно. Поэтому после обновления:

  1. Перевыпустите bot-токены Telegram у ботов, которых использует Gotcha (/revoke в @BotFather, затем новый токен в канал).
  2. Смените ключи подписи вебхуков на принимающей стороне и в канале.
  3. Если старые бэкапы больше не нужны — удалите их; если нужны, храните на том же уровне защиты, что и секреты.

Пропустить шаг можно только если каналов доставки у вас не было вовсе.

Что меняется при обновлении с версий до 0.4.2: исключённые участники теряют доступ через команды

Раньше исключение участника из организации не трогало его членство в командах, и он продолжал иметь доступ к проектам любой команды, в которую входил, — даже перестав быть участником самой организации. Миграция 0029 чинит это на уровне схемы: членство в команде больше не может существовать без членства в организации.

Применение миграции делает две вещи:

  • удаляет уже накопившиеся висячие членства (участник давно не в организации, но всё ещё числится в одной из её команд) — число удалённых строк миграция печатает в свой лог;
  • закрепляет правило в самой базе: исключение участника из организации автоматически каскадом снимает его членство в командах, так что расхождение больше не накопится снова.

Если в вашей организации кого-то в прошлом исключили, но доступ к проекту через команду у него сохранялся, — после этого обновления доступ пропадёт. Это и есть работающее исправление, но выглядеть оно может как поломка, если никто не ждал такого эффекта: стоит проверить перед обновлением, у кого сейчас есть доступ через команды, — или быть готовым объяснить это постфактум.

Эта миграция помечена backward-compatible: no. После её применения гейт схемы откажется запускать бинарь предыдущей версии на этой базе (см. «Откат назад» ниже) — обычный путь отката для неё недоступен, только восстановление бэкапа, снятого заранее.

Ссылок-приглашений, выданных до обновления, это не касается: их токены продолжают работать как прежде.

Что меняется при обновлении с версий до 0.4.2: длительности в карточках регрессий показывались завышенными в тысячу раз

Значения длительности регрессии производительности до этой версии записывались в микросекундах, а все, кто их читал, — карточки регрессий, письма-уведомления, вебхуки и Telegram — трактовали число как миллисекунды: реальный эндпойнт в 200 мс показывался как 200 секунд. Web-vital-метрики (lcp, fcp, ttfb, inp) это не затрагивало — не в том формате была записана только duration. Та же путаница мешала и абсолютному порогу, который отсекает ложные тревоги на малых значениях: он не срабатывал, потому что даже небольшая по факту регрессия выглядела огромной.

Миграция 0030 делит каждое сохранённое значение duration на 1000, приводя уже накопленные строки к единице, которой теперь пользуется остальная система. Никаких ручных действий не требуется — пересчёт выполняется автоматически как часть обновления, точно так же, как любая другая миграция. Откат домножает те же строки обратно, возвращая прежние числа с той точностью, которую допускает арифметика с плавающей точкой (значения, кратные 1000, восстанавливаются точно; у прочих может отличаться далёкий десятичный знак) — сверять вручную нечего в обоих направлениях.

Что меняется при обновлении с версий до 0.4.2: новые индексы этого обновления строятся без блокировки — но проверьте их после обновления

Это обновление разом добавляет двадцать четыре новых индекса на разные таблицы, тремя партиями: шесть — под часовую чистку сущностей (чистильщик, internal/telemetry/entity_janitor.go, чистит issues, perf_issues, incidents, perf_regressions, profile_regressions и metric_incidents по времени последнего появления либо закрытия — ни один существующий индекс этих таблиц не подходил под такой фильтр, каждый проход был полным сканом всей таблицы); шестнадцать — под внешние ключи, которые раньше были без индекса с нужной стороны (замедляет каскадные удаления и JOIN по ним); и два — под поиск подстроки в списке проблем (GIN-индексы по триграммам на issues.title/issues.culprit).

Такое количество новых индексов на диске не бесплатно — ожидайте заметного роста занятого места, особенно от двух GIN-индексов по триграммам: индексы такого типа на текстовых колонках обычно весят заметную долю от размера самой колонки. Насколько именно вырастет том — зависит от объёма ваших данных, отдельно не измеряли; после обновления последите за метриками свободного места (gotcha_storage_free_bytes/gotcha_storage_total_bytes/gotcha_storage_used_bytes, см. Мониторинг gotcha).

Все двадцать четыре индекса строятся через CREATE INDEX CONCURRENTLY — это осознанный выбор: обычный CREATE INDEX держит таблицу заблокированной на запись на всё время построения, а на боевом объёме это остановило бы приём событий на ощутимое время. CONCURRENTLY этой блокировки не ставит, но платит за это одной операционной особенностью.

Если построение индекса прервётся (обрыв соединения с базой, нехватка памяти, ручная отмена, перезапуск процесса посреди миграции) — PostgreSQL не убирает недостроенный индекс, а оставляет его в каталоге со статусом «недействителен». При повторном применении той же миграции CREATE INDEX CONCURRENTLY IF NOT EXISTS увидит объект с таким именем и молча пропустит создание — не станет чинить недействительный индекс. Миграция отчитается об успехе, гейт схемы будет зелёным, а рабочего индекса не будет: планировщик недействительные индексы игнорирует, и находка, ради которой заведён конкретный индекс, останется незакрытой без единого признака, что что-то не так.

Поэтому после обновления, независимо от того, прошло ли оно на вид гладко, проверьте:

SELECT indexrelid::regclass, indisvalid FROM pg_index WHERE NOT indisvalid;

Если список непуст — любой из двадцати четырёх индексов этого обновления, попавший в него, недостроен и не будет использован планировщиком (не только шесть индексов чистильщика — то же относится к индексам под внешние ключи и под поиск, добавленным этим же обновлением; список специально не перечисляется здесь по именам, чтобы не устареть при следующей миграции). Почините так:

DROP INDEX CONCURRENTLY <имя_индекса>;

и заново примените миграции (перезапуск с GOTCHA_AUTO_MIGRATE_ENABLED=true либо повторный запуск с --migrate-only) — на этот раз IF NOT EXISTS не увидит объекта с таким именем и построит индекс с нуля.

Что меняется при обновлении: типы у DSN-ключей приёма

Начиная с этого обновления у DSN-ключа есть тип (browser/server/agent), ограничивающий, какую телеметрию этим ключом можно прислать — полный разбор на странице Ключи приёма. Для существующей установки это обновление ничего не ломает:

  • все ключи, выпущенные до обновления, продолжают работать без каких-либо действий с вашей стороны — они автоматически получают тип legacy с полным допуском, бессрочно;
  • в настройках проекта такие ключи помечены бейджем «Без типа» — это не ошибка и не повод срочно что-то менять;
  • новые проекты, созданные уже после обновления, получают сразу три ключа — по одному на browser/server/agent — вместо одного общего;
  • хост после обновления по-прежнему регистрируется автоматически, но только экспортом с ключа типа agent (или со старого ключа без типа); экспорт метрик с ключом другого типа принимается, как и раньше, а хост по нему просто не заводится — если у вас уже стоит агент или коллектор со старого шаблона конфига, ничего переделывать не нужно: он использует тот же ключ, что и до обновления.

Развести источники по новым типизированным ключам, не останавливая приём, — отдельная и необязательная задача; порядок действий — на странице Ключи приёма.

Что меняется при обновлении с версий до 0.23.0: десять переменных окружения переименованы

Релиз v0.23.0 («контрактная уборка») переименовал десять серверных переменных окружения. Переменная под прежним именем с непустым значением роняет старт с явным сообщением вида «старое имя → новое», а не тихо подменяется дефолтом — эта проверка не имеет срока давности и продолжает действовать на любой, сколь угодно старой установке.

БылоСтало
GOTCHA_METRIC_EVAL_INTERVALGOTCHA_METRIC_EVAL_INTERVAL_SECONDS
GOTCHA_PROFILE_EVAL_INTERVALGOTCHA_PROFILE_EVAL_INTERVAL_SECONDS
GOTCHA_HOST_EVAL_INTERVALGOTCHA_HOST_EVAL_INTERVAL_SECONDS
GOTCHA_SLO_EVAL_INTERVALGOTCHA_SLO_EVAL_INTERVAL_SECONDS
GOTCHA_ESCALATION_INTERVALGOTCHA_ESCALATION_INTERVAL_SECONDS
GOTCHA_RETENTION_DAYSGOTCHA_EVENT_RETENTION_DAYS
GOTCHA_SERVER_URLGOTCHA_PROBE_SERVER_URL
GOTCHA_INGEST_RATE_LIMITGOTCHA_INGEST_RATE_PER_SEC
GOTCHA_AGENT_DIST_DIRGOTCHA_DIST_DIR
GOTCHA_AGENT_DIST_RATE_PER_MINGOTCHA_DIST_RATE_PER_MIN

Если вы обновляетесь с версии старше v0.23.0 — пройдите по всем местам, где заданы эти переменные, тем же порядком, что описан ниже для волны заморозки контракта перед 1.0: .env, юниты systemd, .env выносных проб на других хостах.

Что меняется при обновлении: семнадцать переменных окружения переименованы

Это обновление переименовывает семнадцать серверных переменных окружения — единица измерения, модификатор и подсистема теперь честно читаются из имени, а не из соседства с ним. Переменная под прежним именем с непустым значением роняет старт с явным сообщением вида «старое имя → новое», а не тихо подменяется дефолтом.

БылоСтало
GOTCHA_ADDRGOTCHA_LISTEN_ADDR
GOTCHA_LOG_LEVELGOTCHA_LOGGING_LEVEL
GOTCHA_LOG_FORMATGOTCHA_LOGGING_FORMAT
GOTCHA_LOCAL_REGIONGOTCHA_UPTIME_LOCAL_REGION
GOTCHA_REGISTRATIONGOTCHA_REGISTRATION_MODE
GOTCHA_EXPORT_TTL_HOURSGOTCHA_EXPORT_RETENTION_HOURS
GOTCHA_SCRUB_KEYSGOTCHA_SCRUB_DENY_KEYS
GOTCHA_SCRUB_ALLOW_KEYSGOTCHA_SCRUB_KEEP_KEYS
GOTCHA_RUN_EVALUATORSGOTCHA_EVALUATORS_ENABLED
GOTCHA_AUTO_MIGRATEGOTCHA_AUTO_MIGRATE_ENABLED
GOTCHA_ALLOW_INSECURE_SECRETGOTCHA_SECRET_KEY_ALLOW_INSECURE
GOTCHA_MAX_BUFFER_BYTESGOTCHA_MAX_WRITER_BUFFER_BYTES
GOTCHA_MAX_QUEUE_BYTESGOTCHA_MAX_INGEST_QUEUE_BYTES
GOTCHA_PROBE_TOKENGOTCHA_PROBE_KEY
GOTCHA_EXTERNAL_CHANNEL_DETAILSGOTCHA_EXTERNAL_CHANNEL_DETAILS_ENABLED
GOTCHA_OIDC_NAMEGOTCHA_OIDC_DISPLAY_NAME
GOTCHA_PURGE_RECONCILE_HOURSGOTCHA_PROJECT_PURGE_RECONCILE_HOURS

После обновления сервера пройдите по всем местам, где у вас заданы переменные GOTCHA_*, а не только по .env рядом с docker-compose.yml: юниты systemd, отдельные .env-файлы CI/CD и, отдельно, .env выносных проб (--mode=probe) на других хостах — апгрейд сервера до них физически не дотягивается, они продолжат жить со старыми именами, пока их не обновят вручную.

Самый заметный случай этого радиуса — переменная токена выносной пробы: её новое имя — GOTCHA_PROBE_KEY (строка «было → стало» — в таблице выше). Уже зарегистрированные и работающие пробы держат прежнее имя в окружении своего хоста. Перезапустите их с переменной под новым именем сразу после обновления центрального сервера — иначе при следующем перезапуске пробы (обновление образа, перезагрузка хоста и т.п.) она уйдёт в отказ старта вместо повторного подключения, а не продолжит молча работать со старым значением. См. Пробы и Конфигурацию.

Тот же радиус и у трёх переменных окружения самого агента gotcha-agent — одна со сменой типа: интервал сбора теперь целое число секунд, а не duration-строка вида «30s». Агент — тоже отдельный процесс на удалённом хосте, куда апгрейд сервера не дотягивается: если /etc/gotcha-agent/gotcha-agent.env хранит их под прежними именами, ближайший запуск команды обновления откажет с сообщением config check failed — установщик проверяет конфиг новым бинарём (--check) раньше, чем трогает systemd-юнит, так что старый агент продолжит спокойно работать на прежнем бинаре, пока вы не поправите имена в файле вручную (по таблице ниже) и не запустите команду обновления ещё раз. Актуальные имена и диапазоны — в справочнике переменных на странице Хосты.

БылоСтало
GOTCHA_AGENT_INTERVALGOTCHA_AGENT_INTERVAL_SECONDS
GOTCHA_AGENT_KEYGOTCHA_AGENT_INGEST_KEY
GOTCHA_AGENT_TLS_SKIP_VERIFYGOTCHA_AGENT_TLS_INSECURE_SKIP_VERIFY

Что меняется при обновлении: переменные compose и сборки переименованы

Той же волной переименовываются ещё одиннадцать переменных — восемь переменных подстановки Docker Compose (пароли и потолки памяти контейнеров баз, потолок памяти и MTU сети контейнера приложения, публикуемый порт и адрес бинда) получают префикс GOTCHA_COMPOSE_, а три переменные, которыми Makefile передаёт версию сборки в docker-compose.yml (DOCKER_BUILD_ENV), — префикс GOTCHA_BUILD_. Описание самих переменных и их дефолтов — в Конфигурации, разделы «Переменные только для compose» и «Переменные только для сборки».

БылоСтало
GOTCHA_PG_PASSWORDGOTCHA_COMPOSE_PG_PASSWORD
GOTCHA_CH_PASSWORDGOTCHA_COMPOSE_CH_PASSWORD
GOTCHA_PG_MEM_LIMITGOTCHA_COMPOSE_PG_MEM_LIMIT
GOTCHA_CH_MEM_LIMITGOTCHA_COMPOSE_CH_MEM_LIMIT
GOTCHA_MEM_LIMITGOTCHA_COMPOSE_MEM_LIMIT
GOTCHA_NET_MTUGOTCHA_COMPOSE_NET_MTU
GOTCHA_PORTGOTCHA_COMPOSE_PORT
GOTCHA_BINDGOTCHA_COMPOSE_BIND
GOTCHA_VERSIONGOTCHA_BUILD_VERSION
GOTCHA_COMMITGOTCHA_BUILD_COMMIT
GOTCHA_DATEGOTCHA_BUILD_DATE

В отличие от серверных и агентских переменных, эти одиннадцать не читает вообще никакой процесс gotcha — их видит только сам Docker Compose (подстановка ${...} в compose-файле) и make. Страховка при этом та же: docker-compose.yml подключает .env целиком (env_file), так что устаревшее имя в .env всё равно попадает в окружение процесса gotcha и ловится тем же отказом старта «старое имя → новое», хотя сам процесс эти переменные не читает и никогда не читал.

Если вы задаёте эти переменные не через .env, а прямо в блоке environment:/build.args compose-файла или отдельно передаёте их в make (например, make up GOTCHA_COMPOSE_PORT=...), переименуйте их там же вручную по таблице выше — отказ старта такую правку не поймает, потому что до окружения процесса gotcha она никогда не доходит.

Что меняется при обновлении: конфигурация с опечаткой больше не стартует

До этого обновления часть неверных значений GOTCHA_* принималась молча — либо откатывалась на дефолт пакета, либо всплывала не на старте, а где-то дальше, при первом реальном использовании (первая отправка почты, первое подключение к базе, первая выгрузка). Теперь всё из списка ниже — отказ старта, с именем переменной в сообщении:

  • мусор в булевой переменной (GOTCHA_SCRUB_IP=ture и подобное) — раньше молча выключал настройку;
  • GOTCHA_MAX_WRITER_BUFFER_BYTES=0 или GOTCHA_MAX_INGEST_QUEUE_BYTES=0 — раньше откатывались на дефолт пакета; явный 0 (как и отрицательное значение) теперь отказ старта;
  • нераспознанный GOTCHA_LOGGING_LEVEL/GOTCHA_LOGGING_FORMAT — раньше молча откатывался на info/text;
  • GOTCHA_SMTP_PORT вне диапазона 1..65535 — теперь безусловный отказ, даже если GOTCHA_SMTP_HOST не задан;
  • четвёрка лимитов выгрузок (GOTCHA_EXPORT_MAX_ROWS, _MAX_BYTES, _DISK_BUDGET_BYTES, _RETENTION_HOURS) — теперь проверяется на старте, а не глотается в slog.Warn при первом использовании;
  • GOTCHA_PG_DSN/GOTCHA_CH_DSN — теперь разбираются (без подключения) на старте, опечатка всплывает сразу, а не на первом обращении к базе;
  • пробельное (но непустое) значение GOTCHA_SECRET_KEY/GOTCHA_PG_DSN/ GOTCHA_CH_DSN — раньше откатывалось на дефолт;
  • GOTCHA_SECRET_KEY теперь обрезается по краям от пробелов — раньше бралось дословно, вместе с пробелом. Если ваш ключ хоть раз копировался с хвостовым пробелом, всё, что зашифровано под ним at-rest (секреты каналов доставки, client secret SSO), после обновления перестаёт расшифровываться молча — без единой ошибки на старте. Симптом: алерты в Telegram/webhook перестают уходить, вход по SSO ломается. Путь восстановления: задайте GOTCHA_SECRET_KEY_PREV равным старому значению ключа буквально, вместе с пробелом (эта переменная, в отличие от GOTCHA_SECRET_KEY, читается дословно, специально ради такого восстановления), рядом с новым GOTCHA_SECRET_KEY — приложение расшифрует и старым, и новым ключом, зашифрует уже новым. После перезапуска с новым ключом уберите GOTCHA_SECRET_KEY_PREV. Подробности процедуры ротации — в «Приватность и 152-ФЗ».

Пред-полётная проверка. Прежде чем обновлять боевой инстанс, стоит заранее проверить, что новый бинарь примет ваш .env без единой находки — но --migrate-only не просто проверяет конфиг: пройдя проверки, он подключается к базе и реально применяет к ней миграции схемы. Прогоните эту проверку на стенде, а не на проде, — той же командой, которой ниже пользуется раздел «Раздельное применение миграций» для инициализации схемы:

docker compose --env-file .env run --rm --no-deps gotcha --migrate-only

Разбор и все проверки конфига (весь список выше, плюс переименования из разделов над этим) выполняются раньше, чем приложение открывает хоть одно соединение с базой, — если в .env есть находка, эта команда завершится ненулевым кодом и одной строкой ERROR ... GOTCHA_ИМЯ: ... до применения миграций к стенду. Нулевой код означает не только «конфиг чист» — за ним уже последовало реальное применение миграций к базе, на которую указала команда (см. ниже, куда именно).

Куда именно уезжают миграции. В штатном docker-compose.yml этого репозитория GOTCHA_PG_DSN/GOTCHA_CH_DSN сервиса gotcha заданы прямо в блоке environment: и указывают на соседние postgres/clickhouse этого же compose-проекта — этот блок перекрывает те же имена из .env (см. комментарий над ним в файле), так что команда выше физически не может достучаться до прод-БД через скопированный в .env прод-DSN: она всегда идёт в локальные контейнеры конкретно ТОГО стенда, на котором её запустили. Это не освобождает от осторожности в двух случаях:

  • если вы запускаете эту команду не через штатный docker-compose.yml, а бинарём напрямую (gotcha --migrate-only с .env, экспортированным в окружение процесса) — тогда GOTCHA_PG_DSN/GOTCHA_CH_DSN берутся из .env буквально, без чьего-либо перекрытия, и прод-DSN в скопированном .env уедет ровно туда, куда указывает;
  • если стенд, где выполняется команда, — не отдельный docker-compose.yml, а тот же самый прод-проект (тот же хост, тот же compose-стек) — тогда «локальные контейнеры этого стенда» и есть прод-база.

Если вам нужно направить пред-полётную проверку на конкретную стендовую базу вместо локальных контейнеров compose-проекта (например, на общий стендовый инстанс PostgreSQL/ClickHouse), перекройте оба DSN явно флагом -e — он имеет более высокий приоритет, чем environment: в docker-compose.yml (проверено: -e реально побеждает и --env-file, и блок environment: сервиса):

docker compose --env-file .env run --rm --no-deps \
  -e GOTCHA_PG_DSN='postgres://user:pass@staging-pg:5432/gotcha_staging?sslmode=disable' \
  -e GOTCHA_CH_DSN='clickhouse://user:pass@staging-ch:9000/gotcha_staging' \
  gotcha --migrate-only

Никогда не подставляйте сюда боевой DSN-e перекрывает штатную защиту docker-compose.yml буквально, и команда применит миграции к той базе, которую вы укажете, без дополнительного подтверждения.

Обычное обновление (один сервер, режим --mode=all)

Если вы используете штатный docker-compose.yml как есть (одна реплика приложения, --mode=all) — это самый частый случай для self-hosted установки:

cd gotcha   # папка с docker-compose.yml
git pull
make up-rebuild

Разбор:

  1. git pull — подтягивает новый код из репозитория.
  2. make up-rebuild — пересобирает образ приложения gotcha с обновлённым кодом и пересоздаёт контейнер (Postgres/ClickHouse используют готовые официальные образы, их специально пересобирать не нужно — Compose скачает нужную версию сам, если она изменилась в docker-compose.yml). При старте (это стандартное поведение, GOTCHA_AUTO_MIGRATE_ENABLED=true по умолчанию) приложение само применяет недостающие миграции схемы к PostgreSQL и ClickHouse, прежде чем начать принимать запросы — отдельно ничего делать не нужно.

Собирайте через make, а не голым docker compose build: только make вычисляет git-версию и вшивает её в бинарь. Образ, собранный compose-командами напрямую, работает, но представляется как «no build metadata» в /version, на странице «О программе» и в метрике gotcha_build_info — вы теряете возможность проверить, что развёрнуто именно то, что вы думаете.

Если хотите обновить только образ без пересборки из исходников (например, вы используете готовый образ из реестра, а не собираете из git) — используйте docker compose pull + docker compose up -d.

Что означает автоматическое применение миграций

По умолчанию (GOTCHA_AUTO_MIGRATE_ENABLED=true) при каждом старте приложение проверяет версию схемы в базе и, если она отстаёт от версии, «зашитой» в бинарник, применяет недостающие миграции автоматически, прежде чем открыть порт. Это удобно для типовой установки «один сервер, один процесс» — обновление сводится к трём командам выше.

Раздельное применение миграций (несколько реплик приложения)

Если вы запускаете несколько процессов gotcha одновременно (например, отдельно --mode=ingest и --mode=web, или несколько реплик за балансировщиком — сценарий продвинутой эксплуатации, выходящий за рамки штатного docker-compose.yml), автоматические миграции при старте каждой реплики опасны: несколько процессов могут попытаться применить миграции одновременно. Для этого случая:

  1. Установите GOTCHA_AUTO_MIGRATE_ENABLED=false для всех реплик.

  2. Перед запуском реплик выполните миграции один раз — флагом --migrate-only:

    docker compose run --rm --no-deps gotcha --migrate-only
    

    Этот запуск применяет схему и завершается с кодом 0, не открывая HTTP-порт и не поднимая ни приёма, ни аптайма, — то есть годится как init-job. Флаг сам включает применение миграций, поэтому GOTCHA_AUTO_MIGRATE_ENABLED=false в окружении реплик ему не мешает. С --mode=probe он несовместим и об этом скажет: проба вообще не открывает соединение с базой, и молча выйти нулём означало бы соврать, что схема применена.

  3. После этого запустите (или перезапустите) все реплики с GOTCHA_AUTO_MIGRATE_ENABLED=false — они проверят, что схема БД совпадает со встроенной версией, и откажутся стартовать, если это не так (это защита от «тихого» приёма данных в устаревшую схему — лучше явный отказ при старте, чем молчаливые ошибки на каждой вставке).

Для штатного docker-compose.yml из этого репозитория (один сервис gotcha в режиме all) раздельные миграции не нужны — используйте обычный сценарий обновления выше.

Если миграция оборвалась (dirty)

Если процесс убит во время миграции (отключение питания, docker kill, OOM), инструмент миграций оставляет схему помеченной dirty: «миграция N началась и не подтвердила завершение». С этого момента приложение отказывается стартовать — намеренно: оно не может знать, применилась миграция N наполовину или целиком, — и ошибка старта называет застрявшую версию:

docker compose logs gotcha
# ... база в состоянии dirty на версии N ...

Сначала посмотрите, что миграция N на самом деле сделала со схемой (файлы миграций лежат в репозитории в internal/db/migrations/, по номерам). Затем снимите флаг с той версией, которая соответствует реальности:

# Миграция N успела примениться (или вы доделали её руками) — остаёмся на N:
docker compose run --rm gotcha --migrate-force=N

# Вы откатили миграцию N руками — шаг назад на N-1:
docker compose run --rm gotcha --migrate-force=N-1

Для схемы ClickHouse флаг называется --migrate-force-ch=N; какая из баз застряла — говорит текст ошибки старта. Принимаются только эти два номера: всё остальное трактуется как опечатка, потому что молча сдвинуло бы точку отсчёта всех будущих миграций.

--migrate-force не доделывает миграцию — он только снимает признак «не завершена». Если миграция N применилась наполовину, а вы сняли флаг на N не проверив, следующий старт спокойно поедет по схеме без половины миграции N — и каждая вставка в недостающую часть будет падать. Сначала проверить схему, потом снять флаг, потом docker compose up -d — оставшиеся миграции (N+1 и дальше) приложение применит само.

Откат назад

Миграции схемы в Gotcha написаны как «вперёд»: откатывать саму схему назад продукт не умеет. А вот откатить бинарь, оставив схему как есть, можно — если миграции, применённые новой версией, были аддитивными.

Каждая миграция несёт признак обратной совместимости, и при её применении признак записывается в базу (таблица schema_compat). Старая версия читает его при старте и решает сама:

Что применяла новая версияЧто делает старый бинарь при старте
Только аддитивные миграции (новые таблицы, колонки с умолчанием, индексы)стартует и работает; в лог идёт предупреждение с перечнем версий
Есть хотя бы одна ломающая (удаление или переименование колонки, смена типа)отказывается стартовать и называет версию, которая мешает
О версии нет записи в schema_compatотказывается стартовать: признак неизвестен, а рисковать данными нельзя

Порядок действий:

  1. Откатите .env вместе с бинарём. Обязательно для версий ниже v0.34.0: этот релиз переименовал семнадцать серверных переменных (раздел выше), и старый бинарь новых имён не знает — он не откажется стартовать, а тихо возьмёт дефолт по переменной, которой в .env для него уже нет. Самые опасные из семнадцати:

    • GOTCHA_REGISTRATIONGOTCHA_REGISTRATION_MODE — старый бинарь не видит запрет и открывает публичную регистрацию;
    • GOTCHA_SCRUB_KEYSGOTCHA_SCRUB_DENY_KEYS — скрабинг PII в логах и событиях перестаёт действовать;
    • GOTCHA_RETENTION_DAYSGOTCHA_EVENT_RETENTION_DAYS — ретеншен молча падает к дефолту 90 дней, и более старые данные начинают удаляться;
    • GOTCHA_ADDRGOTCHA_LISTEN_ADDR — адрес прослушивания уезжает на дефолт.

    Полный список переименований всех трёх волн (v0.23.0, v0.34.0, агентские и compose-переменные) — internal/envcontract/renamed.go в репозитории.

  2. Откатите приложение — переключитесь на предыдущий коммит/тег и пересоберите:

    git checkout <предыдущий-тег-или-коммит>
    make up-rebuild
    
  3. Посмотрите лог старта. Строка вида «схема версии N впереди встроенной M; версия … помечена обратно-совместимой, работаем на ней» означает, что откат удался и инстанс работает на более новой схеме. Это штатный режим, но временный: доведите ситуацию до конца — либо вернитесь на новую версию, либо восстановите базу из бэкапа, снятого перед обновлением.

  4. Если старт прерван с сообщением о несовместимой схеме — откат бинаря невозможен: восстановите резервную копию, снятую перед обновлением (см. Резервное копирование и восстановление), и поднимите на ней предыдущую версию.

Пункт 4 предполагает, что вы увидите именно это сообщение. У бинарей, выпущенных до версии 1.0, при настройках по умолчанию (GOTCHA_AUTO_MIGRATE_ENABLED=true — так у всех, кто не выключал автомиграцию вручную) это не так: автомиграция запускается раньше проверки на опережение схемы, а библиотека миграций на схеме впереди своего набора не завершается тихо — падает с ошибкой вида no migration found for version N: read down for version N ... file does not exist. Итог тот же: откат бинаря невозможен, нужен бэкап, — но вместо внятного сообщения о несовместимости вы увидите сырую ошибку библиотеки, где нет ни слова ни про схему, ни про откат, ни про бэкап. Начиная с версии 1.0 проверка на опережение схемы выполняется до попытки автомиграции, и сообщение — то, что описано в пункте 4.

Признаки совместимости появились не с первого выпуска — какой именно их принёс, указано в CHANGELOG.ru.md репозитория. Откатиться через то обновление нельзя: версии схемы, применённые более ранними выпусками, признака не несут, и старт на них будет запрещён.

Второе ограничение — то, о чём schema_compat вообще не знает: секреты. Начиная с 0.25.0 всё, что шифруется в базе (клиентские секреты SSO-провайдеров, секреты каналов оповещения — токен Telegram-бота и HMAC-ключ вебхука, заголовки HTTP-мониторов), хранится в конверте enc:v2:<id-ключа>:…, и бэкфилл в этот формат выполняется при каждом старте: всё, что бинарь смог прочитать, пересохраняется под текущим ключом. Из-за этого откатываться ниже версии 0.25.0 нельзя вовсе — не «не рекомендуется», а именно нельзя: после первого же старта 0.25.0 или новее признак совместимости такой откат не запрещает (гейт видит номер версии схемы, а не формат данных в её колонках), старый бинарь стартует как ни в чём не бывало, но конверт enc:v2: он не распознаёт как шифротекст — считает значение секретом в открытом виде и отдаёт наружу как есть. Доставка алертов и вход через SSO ломаются молча, без единой строки в логе старта. Единственный путь ниже v0.25.0 — восстановление бэкапа, снятого до обновления: откатом одного бинаря это не сделать.

Именно поэтому шаг «сделайте бэкап перед обновлением» в начале этой страницы остаётся обязательным: часть откатов возможна без него, но не все.

Агенты на хостах обновляются отдельно

Обновление инстанса не трогает gotcha-agent, установленные на серверах: они продолжают слать метрики старой версией. Карточка хоста покажет бейдж «Есть обновление» с готовой командой — обновление это та же команда установки без переменных окружения, выполненная на хосте (см. Хосты). Старый агент остаётся совместимым: имена метрик и протокол не менялись, обновление нужно ради исправлений в самом агенте. Переименование переменных окружения самого агента этим обновлением описано в предыдущем разделе.

Проверка после обновления

docker compose ps
curl -sf http://localhost:59080/readyz

docker compose ps — все контейнеры должны быть Up (healthy), включая gotcha. /readyz должен вернуть {"status":"ready",…} с кодом 200. Дополнительно посмотрите логи на предмет ошибок при старте:

docker compose logs --tail=100 gotcha

Строка applying migrations, за которой не следует сообщение об ошибке — миграции прошли успешно. Затем откройте интерфейс в браузере и убедитесь, что вы видите свои организации, проекты и данные.

Что дальше