Инженер включает подробное логирование, чтобы понять, почему ИИ-агент дал неверный ответ. Через день он находит в системе наблюдения не только ошибку, но и сообщение клиента, заголовок Authorization, фрагмент внутреннего договора и полный ответ модели. Теперь журнал сам стал ещё одним хранилищем чувствительных данных. Разберём, какие события действительно нужны для расследования, как связать их по trace ID и где остановить утечку до записи в лог.
Журнал – это карта исполнения, а не стенограмма
Агент обычно проходит несколько шагов: принимает сообщение, проверяет права, обращается к поиску по базе знаний, вызывает модель, предлагает действие, иногда ждёт одобрения и запускает инструмент. Для поиска неисправности важно знать порядок шагов, использованные версии правил и результат каждого вызова. Полный текст вопроса и ответа часто не нужен.
Например, обращение к поддержке звучит так: «Агент ответил по старому тарифу». Чтобы начать разбор, инженеру нужны время, версия источника, идентификаторы найденных документов, версия политики выбора ответа и факт успешного вызова модели. По этим данным можно проверить, был ли подключён старый индекс, прошёл ли фильтр актуальности и какая версия документа действительно считалась разрешённой. Копировать весь разговор «на всякий случай» для первого этапа не требуется.
Здесь важно различить три вида записей. Трассировка показывает длительность и связи вызовов в одном обращении. Технические события отмечают переходы и ошибки: документ найден, доступ отклонён, инструмент завершился. Аудит действий отвечает, кто разрешил операцию, какой политикой она проверялась и чем закончилась. У них могут быть разные хранилища, читатели и сроки хранения. OWASP отдельно замечает, что операционные, аудиторские и связанные с безопасностью журналы могут обслуживать разные цели.
Схема события начинается с вопросов расследования
Перед добавлением поля в журнал полезно спросить: какое решение можно принять по этому полю? Если ответа нет, данные, вероятно, собирать не следует. Для одного события обычно достаточно времени, типа, имени компонента, результата, кода причины и идентификаторов связи. OWASP формулирует основу как «когда, где, кто и что», причём сведения о пользователе не обязательно означают его имя или контакт.
Минимальный пример для условного агента, который нашёл документ:
{
"time": "2026-09-24T10:15:31Z",
"event": "knowledge.retrieval.completed",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"component": "retriever",
"status": "success",
"source_version": "pricebook-2026-09",
"document_refs": ["doc-217", "doc-284"],
"result_count": 2,
"policy_version": "retrieval-v7"
}
Это иллюстративная схема, не формат журнала конкретного продукта. В ней нет поискового запроса, текста найденных фрагментов, имён клиентов или тела запроса к модели. При этом она позволяет спросить: не использовался ли старый справочник? Не изменились ли правила отбора? Вернул ли поиск вообще что-нибудь?
Идентификатор документа тоже нужно оценивать: если он сам содержит ФИО, номер договора или прямую ссылку с токеном, это уже содержательные данные. Лучше выдавать внутренний непрозрачный идентификатор и хранить соответствие в системе с ограниченным доступом. Категории success, denied, timeout, not_found полезнее свободного текстового поля error, куда библиотека может положить произвольное сообщение сервера.
![]()
Trace ID связывает шаги, но не раскрывает личность
В распределённом сценарии один запрос проходит через шлюз, оркестратор, сервис поиска и инструмент. У каждого шага может быть собственный span_id, а общий trace_id связывает их в одну цепочку. Модель данных OpenTelemetry поддерживает TraceId и SpanId в записи лога; стандарт W3C Trace Context описывает передачу контекста между сервисами.
Асинхронный шаг требует внимания. Задача может уйти в очередь и выполниться позже в другом процессе. Тогда нужен явный способ связать событие постановки с событием обработки без копирования исходного сообщения в поле message. Само наличие общего идентификатора ещё не означает, что можно бесконечно сохранять всю историю одной сессии: срок и цель такой связи следует определить отдельно.
Не путайте трассу с аудитом. Трассы могут собираться выборочно ради производительности и расходов на хранение. Важное решение о правах или подтверждённом действии должно записываться как отдельное минимальное аудиторское событие по своей политике, иначе пропущенная трасса оставит дырку в истории действия.
Белый список полей безопаснее поздней «чистки»
Распространённый подход выглядит так: записать объект целиком, а потом заменить телефон и пароль регулярным выражением. Он ломается, когда пользователь пишет секрет в неожиданном формате, документ содержит новый вид идентификатора, а вложенный объект инструмента появляется под другим ключом. Надёжнее заранее определить, какие поля вообще могут покинуть приложение в сторону логирования.
Для распространённых вопросов расследования можно заранее подобрать замену содержательному полю. Чтобы узнать, почему поиск не помог, запишите количество кандидатов и код причины filtered_by_access или no_match, а не текст поискового запроса. Чтобы проверить отказ инструмента, запишите имя операции из закрытого справочника и класс ошибки, а не тело ответа внешнего API. Чтобы оценить медленный ответ, запишите длительности этапов, а не все промежуточные сообщения модели. Когда нужен факт одобрения человеком, сохраняйте идентификатор решения и версию правила в аудите, но не копируйте туда присланный на одобрение документ. Такой набор полей нужно испытать на настоящих вопросах поддержки: слишком скупая схема тоже оставит команду без ответа.
Псевдокод ниже показывает идею «сначала схема, потом отправка». Он не является готовой библиотекой безопасности:
ALLOWED_EVENTS = {
"agent.access.denied",
"knowledge.retrieval.completed",
"tool.execution.completed",
}
ALLOWED_STATUS = {"success", "denied", "timeout", "error"}
def emit_event(kind, context, *, status, reason_code=None,
duration_ms=None, source_version=None):
if kind not in ALLOWED_EVENTS or status not in ALLOWED_STATUS:
raise ValueError("unknown event category")
record = {
"time": utc_now(),
"event": kind,
"trace_id": validate_opaque_id(context.trace_id),
"component": "agent-orchestrator",
"status": status,
"reason_code": validate_enum(reason_code),
"duration_ms": validate_nonnegative(duration_ms),
"source_version": validate_version(source_version),
}
telemetry_sink.write(drop_empty(record))
Функция принимает конкретные скалярные поля и проверяет допустимые значения. У неё нет универсального параметра payload, который соблазняет передать целый объект запроса. Отказ неизвестного типа события лучше заметить при разработке, чем разрешить ему произвольную сериализацию. В реальном проекте добавятся контроль длины, кодирования и формата полей, а также защита от символов перевода строки и управляющих последовательностей: OWASP предупреждает об инъекциях в лог через недоверенные данные.
Даже после такой схемы полезен второй рубеж на коллекторе телеметрии: удалить непредусмотренные атрибуты перед экспортом. Документация OpenTelemetry описывает процессоры удаления и редакции чувствительных полей. Но коллектор не исправит копию секрета, уже попавшую в локальный файл приложения или консоль контейнера. Поэтому фильтрация на источнике – основная, а очистка перед отправкой – дополнительная мера.
Особая ловушка: автоматические трассы модели
Разработчик может аккуратно записывать собственные события и всё равно получить полный диалог в системе наблюдения. Причина – инструментирование SDK, прокси или платформы модели, которое добавляет входные сообщения, системные инструкции, аргументы вызова инструмента и ответы как атрибуты трассы. В семантических соглашениях OpenTelemetry для генеративного ИИ такие поля описаны и прямо отмечены как потенциально содержащие чувствительные сведения.
Перед запуском нужно проверить фактический экспорт: один тестовый диалог с искусственными маркерами в сообщении, найденном документе, аргументах инструмента, URL и ошибке. Ищите эти маркеры не только в видимом интерфейсе мониторинга, но и в сыром потоке коллектора, отладочном выводе SDK, файловом логе, очереди экспорта и резервных копиях. Нельзя считать систему безопасной только потому, что экран мониторинга скрывает поле.
Маскирование по шаблону полезно как дополнительная страховка, но не как способ разрешить запись полного промпта. Простой хеш телефона или числового ID тоже не делает данные анонимными: малое пространство значений позволяет подобрать исходник перебором. OpenTelemetry отдельно предупреждает об этой границе хеширования. Если связь с конкретным человеком действительно нужна, решите задачу через контролируемый реестр соответствий или ключевую псевдонимизацию с отдельным управлением ключом и сроком хранения.
Сырой текст исключения заслуживает отдельного теста. Некоторые клиентские библиотеки включают в него URL с параметрами, фрагмент запроса или ответ сервера. Замена logger.error(exception) на заранее определённые dependency, error_class и retryable часто даёт достаточную картину для дежурного инженера. Подробный стек при необходимости включают по отдельной процедуре и проверяют его состав до экспорта. Даже название поля не гарантирует безопасность: document_id может оказаться номером паспорта, если его составили из данных клиента. Поэтому проверяйте и значения, и происхождение каждого поля.
Сколько хранить и кто может читать
Правило «храним всё год, вдруг пригодится» не отвечает ни цели, ни объёму доступа. Для каждой группы событий задайте назначение, владельца, читателей, срок, механизм удаления и способ проверки, что удаление дошло до архивов, экспортов и копий. Срок выбирают из реальных потребностей расследования, договорных и применимых нормативных требований; универсального числа здесь нет. OWASP отдельно указывает, что временные debug-логи, копии и выгрузки тоже участвуют в политике удаления.
Доступ к обычной диагностике может быть у команды эксплуатации, но расширение прав на аудиторские события требует отдельного решения. Возможность скачать тысячи записей отличается по риску от просмотра одного trace ID по обращению поддержки. Разделите просмотр, массовый экспорт, изменение настройки логирования и удаление. Доступ к журналу также следует фиксировать. На передаче данных в сторонний сервис проверьте его собственную политику хранения и географию обработки, если это существенно для проекта.
Нужен и план на утечку секретов в уже собранный журнал. Остановите источник, найдите все копии и экспорты, ограничьте доступ, удалите запись согласно процедуре и смените скомпрометированный ключ или токен. Одного скрытия поля в интерфейсе недостаточно. Проверять эту процедуру можно на искусственном секрете, не подвергая риску рабочие учётные данные.
![]()
Расследование без сырых реплик: что можно и чего нельзя
Предположим, клиент пожаловался на неверную стоимость. Сначала по времени обращения и выданному номеру инцидента находят нужную трассу. Затем сопоставляют события: проверка доступа прошла, поиск вернул doc-217 версии pricebook-2026-09, модель вызвана с версией инструкции reply-v12, ответ ушёл в канал. По журналу можно обнаружить, что источник устарел или правило маршрутизации выбрало неверный справочник. Сырые реплики для этого не понадобились.
Но такая схема не докажет дословно, что агент написал клиенту. Это осознанный предел минимального журнала. Если требуется разбор точной формулировки, уполномоченный сотрудник обращается к первичному каналу переписки по отдельной процедуре и с соответствующим основанием доступа. Если исходное сообщение нигде не сохраняется, восстановить его из метаданных нельзя. Не обещайте заказчику судебно точную реконструкцию по одним trace ID и кодам событий.
Практическая проверка перед релизом занимает не отчёт, а несколько действий:
- Составьте пять вопросов расследования и оставьте в журнале только поля, которые помогают на них ответить.
- Для каждого компонента перечислите события, версии и допустимые коды причин; запретите произвольные тела объектов.
- Пройдите путь искусственного секрета от входа агента до всех экспортов и убедитесь, что его нигде нет.
- Отключите или ограничьте автоматическую запись содержимого запросов модели, документов и инструментов; проверьте фактическую конфигурацию SDK.
- Разделите права на диагностику и аудит, задайте сроки и испытайте удаление вместе с копиями.
- Проведите один разбор жалобы только по метаданным. Отметьте, в какой момент понадобился доступ к первичному разговору и кто вправе его дать.
Журнал полезен, когда помогает объяснить маршрут решения и найти сломанное правило. Начните с одного критического сценария агента и проверьте, можете ли вы восстановить последовательность событий, не открывая полный диалог. Если для ответа на любой технический вопрос приходится просматривать переписку клиента, схему событий стоит переделать.
Комментарии