Files
transcriber/docs/conventions/config.md
T
av 31ce520c5b документы: сведены расхождения, найденные сверкой канона
- один факт — один дом: рецепт локального входа, правило чтения
  X-Forwarded-For, уровень строки журнала и опись опор изъятия сведены к
  своим домам, копии заменены ссылками
- форма [auth.test_headers] выровнена по образцу конфига в семи местах;
  сценарии intake и archive перестали ссылаться на сессию, которой сервис
  не выдаёт
- поправлены протухшие факты: ключ объекта строит ULID, а не UUID; сверку
  адреса пира зовут трое, а не двое; обзор capability access знает о
  задаче 2026-08-23
2026-08-23 16:41:06 +03:00

16 KiB
Raw Blame History

Конфигурация

Конвенция: как устроена и грузится конфигурация transcriber (TOML). Правила оформления кода (How), не спецификация поведения.

Взято из проекта jellybit. Расхождения с сегодняшним кодом названы по месту. Главные: комментариями снабжена половина полей; единого места проверки на старте нет: у секций [auth], [pipeline] и [storage] свой Validate() в точке входа, а пустые ключи [yandex] ловит конструктор распознавателя.

Механизировано: запрет os.Getenvforbidigo в .golangci.yml (go-linters.md, «Механизировано»). Он держит правило «настройки приезжают из TOML»; godotenv в cmd/transcriber по-прежнему загружает .env, но кладёт его в окружение процесса, а не в настройки приложения.

Принципы

  • Конфигурация — только TOML. Переменные окружения для конфигурации не используем: окружение наследуется дочерними процессами и видно через /proc/<pid>/environ — для секретов это слабее файла под 0600. Расхождение: cmd/transcriber зовёт godotenv.Load() и молча продолжает без файла.
  • Грузим один раз при старте в одну типизированную структуру Config (под-структуры по секциям). Дальше по коду читаем только её — чтения файла в прикладном коде нет, только загрузчик internal/config.
  • Конфиг неизменяем после старта; смена параметров — перезапуск процесса.

Файл и поиск

  • Имя конфига по умолчанию — config.toml, ищется в рабочем каталоге процесса.
  • Путь переопределяется опцией -c path или --config=path.
  • Образец в репозитории — config.example.toml (см. ниже); реальный config.toml не коммитится.

config.example.toml — самодокументируемый образец

config.example.toml коммитим как единый справочник по конфигу: все секции и все поля. Каждое поле снабжаем комментарием, из которого ясно:

  • зачем поле — что оно меняет в поведении;
  • допустимые значения — перечисление или границы;
  • единицы измерения, если применимы — секунды, байты, доля 01.
[server]
port                   = <N>          # порт HTTP-сервера
shutdown_timeout       = <N>          # ждать мягкой остановки сервера, секунды
force_shutdown_timeout = <N>          # ждать остановки воркеров, секунды

Значения намеренно заменены плейсхолдерами: предмет конвенции — форма комментария, а числа, совпадающие с фактом до цифры, от факта неотличимы и начинают врать молча при смене умолчания. Действующие умолчания и их смысл живут одним домом — таблица «Настройки с числовым значением» в ../database.md; config.example.toml — источник истины по составу полей.

Секретные поля оставляем пустыми — значение приходит из выкладки (см. «Секреты»).

Расхождение: перечень доверенных адресов в секции [auth] образца заполнен примером — подсетью docker, — а не оставлен пустым: пустое значение не говорит, какой формы значение здесь ждут, а сервис с пустым перечнем не поднимается вовсе. Секретов в этой секции больше нет: они ушли 2026-08-22 вместе с собственным входом.

Там же, комментарием под секцией, стоит рецепт локального входа одним связным блоком, а не тремя комментариями по месту: правки связаны между собой, и применённая порознь любая из них роняет старт либо оставляет сервис никого не узнающим. Рабочей строкой в образце стоит боевое значение — перечень с адресом прокси и debug = false, — а секция имитации закомментирована целиком: образец описывает боевую выкладку, а локальный вход — способ до неё дойти, и два рабочих значения в одном файле читались бы как выбор без указания, какое из них чьё.

Расхождение: петлевые адреса в рецепте названы парой127.0.0.1 и ::1, — а не одним значением, хотя правило секции требует от образца только формы значения. Причина в цене: браузер разрешает localhost в IPv6 не реже, чем в IPv4, и перечень без ::1 даёт неузнанный запрос там, где человек ждёт входа.

Поля по дискриминатору type

Когда набор полей секции зависит от поля-дискриминатора type (выбор одного из бекендов или внешних сервисов), обязательность и опциональность полей определяет значение type, а не фиксированный список секции.

  • Проверка — по type. Для каждого поддерживаемого значения свой набор обязательных полей; поля других значений не требуются. Неизвестное значение — ошибка на старте с перечислением поддерживаемых.
  • Образец — по type. В config.example.toml:
    • основное (умолчательное) значение предзаполнено рабочими значениями;
    • альтернативные — блоками-комментариями ниже, каждый со своим описанием полей (зачем, границы, единицы — как у обычных полей);
    • так из примера видны все варианты и поля каждого, не открывая код.

Дискриминатора в transcriber пока нет; правило записано на случай второго распознавателя.

Секреты

Секреты доставляет выкладка, рендеря их прямо в config.toml (transcriber: Ansible из pet-project-server). Приложение просто читает TOML — отдельного слоя секретов в коде нет. Источник истины секрета — внешнее хранилище выкладки, не репозиторий и не окружение.

  • Секретные поля transcriber: yandex.speech_kit_api_key, yandex.object_storage_access_key_id, yandex.object_storage_secret_access_key. Секрет клиента OIDC отсюда ушёл 2026-08-22 вместе с собственным входом.
  • Отрендеренный config.toml (с секретами) не коммитится; права 0600, владелец — пользователь процесса (1000:1000).
  • В config.example.toml секретные поля — пустые строки. Расхождение: сейчас там стоят подсказки вида your_..._here, а не пустые строки, и загрузчик их не отличает от настоящего значения.
  • Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит криво отрендеренный файл) — см. «Проверка и остановка на старте».
  • В логи секреты не попадают — см. logging.md, «Безопасность».
  • Отказ загрузки настроек не несёт содержимого файла. Текст такого отказа собирает библиотека разбора, и собирает она его из разбираемого куска: toml.ParseError кладёт в сообщение само значение («Invalid float value: %q»). Оборванная кавычка в строке секретного ключа — типовая поломка криво отрендеренного шаблона выкладки — уносит ключ в журнал контейнера целиком, а инвариант «секрет не покидает конфиг» помечен необратимым. Поэтому отказ разбора пересобирается своими словами: путь, строка, столбец и последний ключ, без сообщения библиотеки. Прочие отказы декодера (несовпадение типов, неподдерживаемый тип) собраны из имён ключей и типов, значений в них нет, и их текст остаётся как есть — иначе за разборчивость отказа платили бы там, где платить не за что.

Проверка и остановка на старте

Конфиг проверяем на старте, до приёма трафика. Негодный конфиг — лог ERROR и выход с ненулевым кодом, не стартуем наполовину.

Что проверяем:

  • обязательные поля заданы;
  • каталоги хранилища существуют и доступны на запись;
  • границы числовых полей соблюдены;
  • ключи внешних сервисов не пусты.

Расхождение: LoadConfig проверяет только существование файла и разбирает TOML. Пустые ключи Yandex ловятся в конструкторе распознавателя, и там процесс выходит с кодом 1. Единого места проверки нет.

Два ключа секции [telegram], стоявшие здесь исключением, ушли вместе с самим входом 2026-08-14: секции больше нет, и своей проверки у неё тоже.

Проверка, охватывающая две секции разом, живёт методом на корневой Config. Такая сегодня одна — ValidateTestHeaders: она судит [server] debug против [auth.test_headers], и ни в Validate() секции сервера, ни в Validate() секции входа не помещается — секция начала бы знать о чужой секции. Зовётся она из cmd/transcriber рядом с остальными. Имена принимаемых заголовков приходят ей доводом, а не читаются из пакета настроек: дом у них один — константы транспорта, — а internal/config транспорта не знает и знать не должен, иначе cmd/devtools, которому нужен один разбор конфига, линковал бы всю поверхность HTTP.

Секции [auth], [pipeline] и [storage] проверяют себя сами, и проверка стоит на старте: Validate() каждой зовётся из cmd/transcriber сразу после загрузки и роняет процесс с именем незаполненного ключа. У [storage] это ожидание занятой базы и число соединений читающего пула: ноль у первого отдаёт «база занята» первому же воркеру, ноль у второго означает пул без предела — то есть настройку, которой не управляют. Причина в цене умолчания: поднявшись с пустым перечнем доверенных адресов, сервис не узнавал бы никого, а узнать об этом было бы неоткуда — все адреса приложения просто отвечали бы отказом. Сообщение называет имя ключа; правило «значения в отказ не идут» остаётся в силе для прочих секций, где секреты есть.

Структура в коде

  • Весь разбор и проверка — в internal/config; наружу отдаётся готовая Config.
  • Одна корневая структура Config с под-структурами по секциям. Перечень секций и полей здесь не повторяем: источник истины по составу — config.example.toml, действующие числа — ../database.md, «Настройки с числовым значением». Каталог данных задаётся одним ключом [storage] data_dir (ADR).
  • Умолчания задаются в defaultConfig(), файл их перекрывает. Новое поле требует правки обоих мест.
  • Обязательное поле — поле, у которого умолчания нет намеренно. Умолчание у такого поля было бы угаданным намерением, и одна из двух ошибок стала бы тихой. Форма записи: умолчания нет ни в defaultConfig() (причина — строкой комментария у самого поля), ни по нулевому значению типа; присутствие ключа судит разборMetaData.IsDefined из toml.DecodeFile, — потому что значение отличить «не задано» от «задано нулём» не позволяет. В config.example.toml у поля стоит значение свежей установки. Первым таким полем был telegram.enabled; секция убрана 2026-08-14, и живого примера у правила сейчас нет.