Билол Саидумаров
Все статьи

Аудит-журнал, который переживёт проверку

21 мая 2026 · 4 минуты · аудит, compliance, LLM
Если коротко
  • structlog — это дневник. Аудит — это свидетель в суде.
  • Шесть полей, append-only, и поведение, которое не валит запрос пользователя при сбое.
  • Один SQL-запрос на «покажите всё, что было с документом X».
Изображение · hero · 1600×900
Длинная лента-журнал на столе. Каждая строка добавляется в конец, ластик с надписью «UPDATE» лежит рядом — и нарисован крест-накрест.
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 с FK ON 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.