Простое управление настройками приложения в проекте на django
Расскажу про нашу библиотеку django-liveconfigs, которая, как и множество других решений, позволяет администратору настраивать сервис, но при этом, как мне кажется, делает это чуть красивей и более по-питоновски.
Про какие настройки речь?
Говорим тут только о бизнес-настройках приложения и немного о технических
Не говорим о большой массе технических настроек, которые должны лежать в переменных окружения
Не говорим о настройках пользователя
История и предпосылки
Когда-то давно, еще в 2019 году, мы писали для заказчика бота-ассистента-секретаря с обработкой естественного языка вот с такими вводными:
нас 3 бэкендера
фронта у проекта нет, фронтендера тем более
весь бэк состоит из нескольких контейнеров — django, celery, celery-beat, redis, postgres, nginx. При этом django, celery и celery-beat раскатываются из одного образа, кодовая база у них одна
языковые модели большие и работают из оперативной памяти
сервис рестартует около минуты, за это время пользователи начинают переживать
возможности сначала поднять копию сервиса рядом, а потом переключить на нее трафик нет — ограничения архитектуры и ограничения ресурсов.
Нам понадобился способ добавить себе быстрое включение-выключение фич и какие-нибудь числовые и строковые настройки, которые можно менять условно мгновенно, не перезапуская сервис.
Дополнительные соображения
Отдельный (микро)сервис не стоит делать, иначе за ним тоже нужно будет следить.
Пользователей относительно немного.
Эксперименты с выкаткой на часть пользователей нам не нужны
Управлять настройками должен администратор через обычную админку django. Это значит, что перед глазами у него должна быть документация, которая тоже как-то должна попадать в админку.
У нас несколько стендов и нет никакого желания добавлять настройки на них руками, поэтому настройки в админке должны появляться «сами», вместе с документацией и значением по-умолчанию. Значит, они должны быть в самом сервисе.
Добавлять и использовать настройки должен разработчик, причем для него это должно быть максимально легко.
Хорошо бы еще при попытке изменения значения настройки добавить проверку типа и самого значения, и также удобно описывать валидаторы.
Пример
Так мы пришли к тому, что решили описывать настройки в самом сервисе в виде атрибутов класса (с небольшой метаклассово-дескрипторной магией для работы с бд). На тот момент не было функционала Annotated, а теперь мы внимательно на него смотрим.
class Config (BaseConfig):
USE_NEW_FEATURE: bool = FalseUSE_NEW_FEATURE_DESCRIPTION = «Включена ли новая фича, которую мы разрабатывали два года»
SOME_VALUE: float = 42.1
SOME_VALUE_DESCRIPTION = «Новая настройка для старой фичи, по-умолчанию 42.1, значение должно быть больше 10»
SOME_VALUE_VALIDATORS = [greater_than (10)]
При обращении к Config.USE_NEW_FEATURE
наша магия по необходимости обновляет метаданные читаемой настройки на стенде — приводит описание и прочие атрибуты, кроме собственно значения, в соответствие с тем, что сейчас есть в исходниках.
Как это устроено внутри
Настройки описываются в классах, наследующихся от BaseConfig
. Для него же есть метакласс, который подменяет атрибуты класса на дескрипторы, которые, в свою очередь, и выполняют всю работу.
При чтении из дескриптора:
Подробнее можно посмотреть вот тут
А почему именно так?
Почему бы не value = get_value («some-value»)
Потому что при этом остаются следующие проблемы и вопросы:
можно ошибиться при наборе строки, как бы это смешно ни звучало
где хранится «источник» документации и кто за него отвечает?
какой тип у полученного значения? как об этом быстро узнать?
Почему не что-нибудь вроде USE_FEATURE = BooleanFlag (False, «Description»)
Теряем информацию о настоящем типе самого значения при разработке. В рантайме там будет boolean. Например, для поддержки подобного поведения полей в django у pycharm вообще должна быть платная версия.
Почему бы не хранить описание настроек в yml/json/где-то еще и не читать их в рантайме?
Теряем информацию о настоящих типах для линтера, есть возможность огрести в рантайме.
Почему бы не хранить описание настройки в docstring класса?
class UseNewFeature(BooleanConfig):
"”””Включена ли новая фича, которую мы делали-делали и, наконец, доделали"”””
default = False
class SomeValueConfig(FloatConfig):
"”””Очень важное значение, которое должно быть больше 10"”””
default = 42.1
validators = [greater_than(10)]
Непонятно, как такое использовать. Инстанцировать класс? Обращаться к атрибуту класса, например UseNewFeature.value? Выглядит не очень красиво.
Сейчас можно спокойно двигаться в сторону Annotated
Почему не unleash, flagsmith, growthbook или flipt
Это рабочие, хорошо зарекомендовавшие себя решения, но:
это отдельные сервисы, за которыми нужно следить
чтобы автоматически до них докатывать новые настройки, нужно дописывать скрипты выкатки. Многие просто заносят новые настройки руками через админку и это становится ручной частью каждой выкатки. Очень не хочется занимать этим команду.
они заточены на эксперименты и частичную раскатку нового функционала, а у нас такой потребности не было
кто отвечает за документацию значений и где источник правды?
как настраивать сложную валидацию?
у них местами очень странные клиенты. Вот, например, как из flipt нужно получать значение простого флага, если следовать документации:
boolean_flag = flipt_client.evaluation.boolean(
EvaluationRequest(
namespace_key="default",
flag_key="flag_boolean",
entity_id="entity",
context={"fizz": "buzz"},
)
)
Почему не django-waffle
На это есть множество причин, ниже приведу некоторые из них:
Это feature-flipper.
Он поддерживает только on/off для флагов.
В нем есть только boolean-значения.
Он, может ответить лишь «да» или «нет».
Он не может хранить int, float, string или произвольный json
Позволяет только включать и выключать фичи.
Почему не django-constance
Очень близкая к нам библиотека, но заточена она под «оживление» settings. Кроме того, синтаксис добавления настроек у нее менее удобный:
CONSTANCE_CONFIG = OrderedDict([
('SITE_NAME', ('My Title', 'Website title')),
('SITE_DESCRIPTION', ('', 'Website description')),
('THEME', ('light-blue', 'Website theme')),
('THE_ANSWER', (42, 'The answer')),
])
Итоги и выводы
Если настройки добавлять легко, их будут добавлять. Сервисом становится сильно легче управлять , многие решения можно перенести ближе к заказчику
Документировать настройки нужно сразу
Хранить описание настроек в самом сервисе — хорошо
Хранить описание настроек в виде кода — хорошо
Для настроек нужен нормальный поиск
Настройки нужно уметь выводить из эксплуатации
Писать велосипеды иногда полезно
Что еще хотим сделать:
«Заморозка» значений
Асинхронная работа
Перейти на Annotated — кажется, это то, что нам нужно, чтобы меньше плодить атрибутов в классе.