Аудит-журнал, который переживёт проверку
Если коротко
- structlog — это дневник. Аудит — это свидетель в суде.
- Шесть полей, append-only, и поведение, которое не валит запрос пользователя при сбое.
- Один SQL-запрос на «покажите всё, что было с документом X».
Minimalist isometric illustration: a long horizontal ledger ribbon on a desk, new rows being added only at the rightmost end. A small eraser labeled "UPDATE" lies beside it, crossed out with a soft red mark. Calm violet-to-indigo gradient, no logos, no readable text, 16:9, 1600x900, professional editorial style.
Когда приходит инспектор, он не открывает Swagger. Он не верит вашему API. Он верит тому, что вы записали, когда не знали, что он придёт.
Это и есть аудит-журнал — отличающийся от обычных логов тем, что переживает диск, ротацию и неудобные вопросы.
structlog — это не аудит
structlog в файл — это операционный дневник. Полезный, но ненадёжный: его можно отротировать, потерять при падении диска, переписать с правами root, отфильтровать «лишнее» перед просмотром.
Аудит — это записи, которые:
- живут в базе с теми же бэкапами и репликацией, что и бизнес-данные;
- пишутся только
INSERT; - переживают удаление сущности, к которой относятся.
На вопрос «покажите всё, что было с документом X» у вас должен быть один SQL-запрос с ответом. Не «давайте поднимем логи за прошлый квартал».
Минимум полей — шесть
occurred_at— TIMESTAMPTZ с индексом DESC.actor— кто действовал. Формат:dashboard:<user>,api:<key-hash>(только префикс sha256, не сам ключ),worker:<name>.action— перечисление с CHECK-ограничением (document.uploaded,document.asked). Свободный текст здесь — будущая боль.document_idс FKON DELETE SET NULL— запись переживёт удаление документа.request_id— связь с трассировкой и обычными логами.details JSONB— короткий структурированный контекст (имя файла, тип ошибки). Не само содержимое — оно в основной БД.
Специфика LLM
Каждый вызов модели — событие. Не сам ответ (он длинный и часто чувствительный), а:
- кто спросил;
- к какому документу относился вопрос;
- режим поиска (keyword / vector / hybrid);
- сколько цитат вернулось;
- опционально — первые 200 символов вопроса для разбора жалоб.
Через год вы ответите на «кто пять раз спрашивал про этот контракт» одним SELECT. Без таких записей — будете строить ответ из обрывков логов и человеческой памяти. Память подведёт первой.
Append-only как инвариант кода
В коде не должно быть UPDATE или DELETE на этой таблице. Никогда.
На уровне БД — отзовите эти права у роли приложения. На уровне ревью — PR, в котором кто-то пытается «поправить запись задним числом», смотрится как красный флаг, а не как обычный коммит.
Инспектор не оценит «мы поправили опечатку». Он оценит «эта строка лежит там с момента события и не двигалась».
Аудит не должен валить запрос
База временно недоступна — пользователь всё равно получает свой ответ. Запись в аудит уходит в очередь и логируется как ошибка оператору.
Пропустить одну строку — неприятно. Уронить весь трафик ради того, чтобы записать одну строку — катастрофа. try/except вокруг AuditService.record() — обязательный.
- Добавьте таблицу
audit_eventsс шестью полями и шестью индексами. Готово за вечер. - Пройдитесь по коду и найдите три бизнес-действия, после которых надо звать
audit.record(). - Посмотрите живой пример журнала в демо ARA.