Как дать ИИ-агенту доступ к проду и не поседеть: безопасный production-дебаг через MCP

от автора

Всем привет!

Хотел поделится интересным паттерном взаимодействия, который удалось выстроить и он довольно неплохо себя показал между ИИ агентом и сервером

Итак дано

  1. Есть продакшен среда внутреннего корпоративного сервиса, который я развиваю, в нем 7 контейнеров на докере. Это такая база всех исследований и наработок по продукту и беклог гипотез. Количество пользователей около 40 человек.

  2. Стек классический — React, Nest.js, postgre, redis и тд. Из необычного там есть RAG и есть еще ИИ чат.

  3. Бой работает на coolify (это такой красивый docker swarm) на отдельном сервере.

  4. При ее доработке, периодически требовалось делать debug, например что-то отвалилось и тд. Прод упал/тормозит/один конкретный пользователь жалуется. Я иду в ssh, в конкретный контейнер, делает docker logspsql, грепаю и тд.

В один момент времени просто надоело подключаться к каждому контейнеру и смотреть логи, а когда их семь, это прям нудно и родилась идея, а почему-бы не прикрутить такой технический mcp, ограничить область видимости. прикрутить авторизацию, так чтобы написать в курсоре, «пользователь увидел вот такое сообщение об ошибке …., изучи его и логи и пришли предложение по исправлению» или «зависло приложение на экране Клиентская аналитика, посмотри что случилось».

Сказано сделано:)

что сделал, я дал ИИ-агенту (Cursor/Claude) возможность дебажить наш прод: читать логи контейнеров, трейсить HTTP-запросы по request_id, смотреть активность пользователя. Но не через SSH и не через shell — а через узкий набор read-only MCP-тулов, обёрнутый в семь слоёв защиты: capability-скоупы, ролевые гейты, жёсткие лимиты, редакцию секретов, rate-limiting, несмываемый аудит с алертами в Telegram и машиночитаемые ошибки, которые направляют агента.

Немного прелюдии

Как только вы даёте агенту shell на проде, вы получаете три ужаса (на эту тему очень много мемов в интернете):

  1. Разрушение. Агент «галлюцинирует» команду и делает docker restartDROPrm и вы можете получить обнуление прода, просто потому что ему кажется это эффективней.

  2. Утечка PII. Агент читает чужие персональные данные «на всякий случай», без следов. Это прям ужасно. Друзья из ИБ, сразу придут, скажут ата-та

  3. Отравление контекста модели. В логах есть Authorization: Bearer ..., и токены утекают в LLM-провайдера вместе с контекстом, ну и потом их можно встретить где-нибудь в неожиданном месте.

Получается просто дать доступ в bash — значит дать всё сразу. Но мне нужен был минимально достаточный доступ с аудитом. Здесь идеально ложится MCP (Model Context Protocol): вместо одного всемогущего инструмента — набор узких, типизированных, заранее ограниченных тулов.

Архитектура

Когда родилась идея сделать такие debug tools в формате MCP, сначала возник вопрос, как это сделать. И возможно ли технически?

Начал раскручивать с конца от самого докера. Оказалось у Docker’a есть Docker Engine Api, который по HTTp может принимать запросы, и он делает ровно то что нужно для дебага, возвращает логи. Для этого используется библиотека dockerode . Я правда никогда его не использовал, слышал о нем, но ничего с ним не делал. Как вы наверное догадались, родилась мысль его подергать.

Я подумал отлично, теперь нужно этот API обернуть в разные сценарии:
1. посмотри такой лог
2. посмотри такой-то лог и тд

Я запил DockerServiceLog, который принимает обращения к Докеру и передают их туда и сделал список таких сценариев, фактически просто поместил мой пользовательский путь (пусть не идеальный) в MCP:

  1. logs_tail — Последние N строк логов одного контейнера

  2. logs_search — Полнотекстовый поиск по логам нескольких контейнеров

  3. request_trace — Один HTTP-запрос по X-Request-Id + связанные логи

  4. user_lookup — Поиск пользователя по email/имени

  5. user_activity_timeline — Таймлайн HTTP-запросов пользователя

Так я мог отследить действия любого пользователя и вытащить его логи, а дальше уже на уровне cursor’a локально посмотреть код, если что добавить инструментарий в лог и повторно его запросить. Поскольку инструмент получился довольно мощный, я добавил ролевую модель и ввел отдельную роль «developer», которому доступны эти логи, а также ограничил scope видов запросов. То есть у тула нет параметра «команда» поэтому агент физически не может выразить деструктивное действие — его нет в словаре.

Но самое интересное даже не это. Когда я первый раз набросал прототип, я сделал «как все»: примонтировал /var/run/docker.sock прямо в backend-контейнер. Оно работало. А потом я поймал себя на неприятной мысли.

Docker-сокет — это, по сути, root на всём хосте. Кто получил доступ к сокету, тот может поднять контейнер с примонтированным корнем хоста и сделать там что угодно. А мой backend — это контейнер, который торчит наружу десятками HTTP-эндпоинтов, ходит в LLM, принимает вебхуки. Если его однажды поломают, я не хочу, чтобы вместе с ним отдавался весь сервер. Монтировать сокет в самый «уличный» контейнер — это как повесить ключ от серверной на гвоздик у входной двери.

Поэтому между бэкендом и сокетом я поставил прослойку — docker-socket-proxy. Маленький отдельный контейнер, единственная задача которого — проксировать Docker API. Только у него примонтирован сокет, причём read-only, и наружу он не выставлен вообще — живёт во внутренней docker-сети, доступен только бэкенду по имени.

nestjs-backend:  environment:    DOCKER_HOST: http://docker-socket-proxy:2375   # никакого сокетаdocker-socket-proxy:  image: tecnativa/docker-socket-proxy:0.1.2  volumes:    - /var/run/docker.sock:/var/run/docker.sock:ro  # сокет живёт только здесь  expose:    - "2375"     # только внутри сети, без ports наружу

А dockerode дальше сам разбирается, куда подключаться. Логика простая: если задана переменная DOCKER_HOST — идём по HTTP на прокси, если нет (например, у меня на ноуте в дев-режиме) — падаем на локальный сокет напрямую. Один и тот же код работает и на проде, и локально:

function createDockerClient(): Docker {  const dockerHost = process.env.DOCKER_HOST;  if (!dockerHost) return new Docker({ socketPath: '/var/run/docker.sock' });  const parsed = new URL(dockerHost);  return new Docker({    protocol: parsed.protocol.replace(':', '') as 'http' | 'https',    host: parsed.hostname,    port: Number(parsed.port || 2375),  });}

За ддосить он тоже не сможет поскольку я ввел лимит на количество таких запросов. Получилась вот такая вот архитектура (см. картинку), состоящая из 7 слоев. Пожалуй, главное в архитектуре. В docker-compose.yml backend получает не сокет, а HTTP-адрес прокси, А dockerode в DockerLogsService сам выбирает транспорт по DOCKER_HOST

Архитектура Debug-tools

Архитектура Debug-tools

Дальше всё прозаично: прокси отдаёт нам логи в «сыром» докеровском формате — это мультиплексированный поток, где каждый кусок предваряется 8-байтовым заголовком (тип потока — stdout или stderr — и длина). Мы его разбираем руками в массив строк и тут же, до того как что-либо уйдёт агенту, прогоняем каждую строку через редактор секретов. Логика «сначала вычисти, потом отдай» здесь не опциональна: в логах прода рано или поздно окажется Authorization: Bearer ..., и я совсем не хочу, чтобы этот токен уехал в контекст языковой модели.

Из мелочей, которые я добавил, чтобы прокси не страдал: резолв «имя сервиса → контейнер» кешируется на десять секунд, а сами обращения к Docker API выстроены в последовательную очередь — чтобы, если агент в азарте дёрнет несколько тулов разом, мы не устроили прокси шторм запросов. Давайте разберем по слоям эту архитектуру, начнем с конца

Слой 7 Только чтение и whitelist контейнеров

Логи читаем через dockerode, смонтировав сокет read-only (/var/run/docker.sock:ro). Никаких start/stop/exec — только logs и listContainers. И контейнеры — из явного списка:

Код docker-logs.service.ts

// docker-logs.service.tsconst SERVICE_WHITELIST = ['nestjs-backend', 'frontend', 'pipeline-service', 'postgres'] as const;const MAX_TAIL_LINES = 1_000;async tail(opts: { service: string; lines: number; sinceSec?: number; grep?: string }): Promise<LogLine[]> {  if (!DockerLogsService.isAllowed(opts.service)) {    throw new BadRequestException(      `Service "${opts.service}" not allowed. Allowed: ${SERVICE_WHITELIST.join(', ')}`,    );  }  const safeLines = clamp(Math.floor(opts.lines || 100), 1, MAX_TAIL_LINES);  const container = await this.findContainer(opts.service);  // ... container.logs({ stdout, stderr, tail: safeLines, follow: false })}

Даже если агент попросит логи postgres-with-secrets-backup — он получит BadRequestException, а не данные.

Отдельно стоит отметить, что ошибки доступа к сокету мы переводим в человекочитаемые подсказки — это пригодится при деплое, когда накатили, а пользователь не в той группе и тд:

function formatSocketError(raw: string): string {  if (raw.includes('EACCES')) {    return 'Docker socket: permission denied (EACCES). На хосте выполните ' +      '`stat -c "%g" /var/run/docker.sock`, задайте `DOCKER_GID=<число>` ...';  }  if (raw.includes('ENOENT') || raw.includes('connect')) {    return 'Docker socket: not mounted (ENOENT). Проверьте docker-compose ...';  }  return `Docker socket unavailable: ${raw}`;}

Слой 2. Capability: scope + роль владельца токена

Поскольку нам нужно ограничить видимость тулов, как для ИИ так и для человека, для каждого тула мы вводим scope и ролевой доступ, например доступ к чужой активности разрешён только администратору (то есть мне:) ), диагностика логов — admin и developer. Это двойной гейт.

Сначала scopes токена фильтруются по роли владельца ещё на этапе аутентификации:

// external-agent.service.tsexport function filterSystemScopesByRole(scopes: string[], ownerRole: string): string[] {  if (ownerRole === UserRole.ADMIN) return ['*'];  // developer видит только logs:read, но не users:debug  // analyst/product — системные scope срезаются полностью  // ...}

А затем — повторная проверка непосредственно перед вызовом тула, уже с привязкой к роли:

// mcp.service.tsprivate canUseSystemScope(scope: string, ctx?: McpExecutionContext): boolean {  const granted = ctx?.scopes ?? [];  if (!granted.includes(scope) && !granted.includes('*')) return false;  if (scope === 'system:users:debug')  return ctx?.userRole === 'admin';  if (scope === 'system:logs:read')    return ctx?.userRole === 'admin' || ctx?.userRole === 'developer';  if (scope === 'system:db:maintain')  return ctx?.userRole === 'admin';  return true;}

Таким образом получаем capability-based security: токен носит ровно те «ключи», которые ему выдали, а опасные операции дополнительно завязаны на роль. Если я грохну базу, потому что дал агенту полномочия, то значит это я сам виноват, но главное агент не сможет это сделать сам без моего ведома.

Слой 3. Жёсткие лимиты вместо доверия

Мы не знаем, сколько раз агент может вызвать тул. Это ведь может случится и 1000 раз, пока вы отошли за кофе, поэтому нужно какой-то лимит. Поэтому любой числовой параметр от агента проходит через clamp с потолком, а временные окна — узкие. Агент не может «вытащить всё»:

// runSystemDebugTool — диспетчер system-debug туловcase 'logs_search': {  const sinceMin = clamp(Number(args.since_minutes ?? 15), 1, 60);   // окно ≤ 60 минут  const limit    = clamp(Number(args.limit ?? 200), 1, 1000);  // ...}case 'user_activity_timeline': {  const sinceMin = clamp(Number(args.since_minutes ?? 30), 1, 120);  // ≤ 2 часов  const limit    = clamp(Number(args.limit ?? 200), 1, 500);  // ...}

Clamp выглядит так

function clamp(n: number, min: number, max: number): number {  if (!Number.isFinite(n)) return min;  return Math.max(min, Math.min(max, Math.trunc(n)));}

Плюс валидация формата там, где она дёшева и спасает от мусора:

case 'request_trace': {  const requestId = String(args.request_id ?? '').trim();  if (!/^[0-9a-f-]{8,64}$/i.test(requestId)) {    return mkSystemError('INVALID_REQUEST_ID', 'request_id must be a UUID-like string', {      next_action: 'fix_input',      user_hint_ru: 'request_id должен быть UUID (32-64 hex-символа).',    });  }  // ...}

Можно тут накрутить и посерьезнее, но я не стал.

Слой 4. Редакция секретов и PII

Нам нужна защита, чтобы случайно в агент не попали секреты и персональные данные пользователя и они не улетели в модель, это называется защита от «отравления контекста модели». Перед тем как строки логов уйдут агенту (а значит — в LLM-провайдера), мы вырезаем токены, куки, пароли и маскируем email:

export function redactSensitiveText(input: string | null | undefined): string {  if (!input) return '';  return input    .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]')    .replace(/\bAuthorization:\s*[^\s,;]+(?:\s+[^\s,;]+)?/gi, 'Authorization: [REDACTED]')    .replace(/\b(Set-Cookie|Cookie):\s*[^,\n\r]+/gi, '$1: [REDACTED]')    .replace(/\b(password|secret|token|api[_-]?key)=([^&\s]+)/gi, '$1=[REDACTED]')    .replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, (e) => maskEmail(e));}function maskEmail(email: string): string {  const [local, domain] = email.split('@');  return local && domain ? `${local[0]}***@${domain}` : '[REDACTED_EMAIL]';}function maskEmail(email: string): string {  const [local, domain] = email.split('@');  return local && domain ? `${local[0]}***@${domain}` : '[REDACTED_EMAIL]';}

Даже user_lookup возвращает email уже замаскированным (s***@corp.ru) — агенту хватает, чтобы выбрать нужного пользователя, но «сырой» PII в контекст не попадает. Тут можно спать спокойно.

Отдельно все вызовы MCP надо сохранять в лог (мало ли кто и что будет запрашивать) для этого аргументы вызова в аудит-лог пишутся без значений, только ключи и длины массивов, чтобы PII не утёк уже в нашу же БД аудита:

function summarizeArgs(args: Record<string, any>): Record<string, unknown> {  const out: Record<string, unknown> = {};  for (const k of Object.keys(args || {})) {    const v = args[k];    if (Array.isArray(v)) out[k] = `[array len=${v.length}]`;    else if (typeof v === 'object' && v !== null) out[k] = '[object]';    else if (typeof v === 'string' && v.length > 80) out[k] = `${v.slice(0, 80)}…`;    else out[k] = v;  }  return out;}

Слой 5.Rate limiting

Чтобы агент не устроил массовое сканирование пользователей, forensics-скоуп лимитирован по дням, per-token-per-scope:

// runSystemDebugTool, до выполненияconst dailyLimit = scope === 'system:users:debug'  ? settings.rate_limit_forensics_daily  : settings.rate_limit_logs_read_daily;const rl = this.systemRateLimiter.check({ agent_id: ctx?.agentId ?? null, scope }, dailyLimit);if (!rl.allowed) {  return mkSystemError('RATE_LIMIT_EXCEEDED',    `Daily rate limit exceeded (${rl.used}/${rl.limit} for scope ${scope}).`,    { next_action: 'wait_until_reset', resets_at: rl.resets_at });}

Сам лимитер пока in-memory, это пока MVP (все ограничения MVP— в конце статьи):

check(keyParts: { agent_id: number | null; scope: string }, dailyLimit: number) {  const today = new Date().toISOString().slice(0, 10);  const key = `${keyParts.agent_id ?? 'anon'}:${keyParts.scope}`;  let bucket = this.buckets.get(key);  if (!bucket || bucket.day !== today) { bucket = { day: today, count: 0 }; this.buckets.set(key, bucket); }  const allowed = bucket.count < dailyLimit;  if (allowed) bucket.count += 1;  // ... resets_at = завтра 00:00 UTC}

Слой 6. Несмываемый аудит + алерт в Telegram

Каждый вызов — успешный, заблокированный или упавший — пишется в system_audit_log. А доступ к чужим данным (forensics) дополнительно триггерит уведомление владельцу системы в Telegram. Это превращает «тихий доступ к PII» в видимое событие:

// system-audit.service.tsasync record(p: AuditCallParams): Promise<void> {  await this.repo.insert({    agent_id: p.agent_id, user_id: p.user_id, tool_name: p.tool_name,    scope_used: p.scope_used, target_user_id: p.target_user_id ?? null,    arguments_summary: p.arguments_summary ?? null, // уже без PII    result_summary: p.result_summary ?? null, ip: p.ip ?? null,  });  if (p.scope_used === 'system:users:debug') {    void this.telegram.notifyForensics({ /* tool, agent, owner, target_user_id, ip */ });  } else if (p.scope_used === 'system:logs:read') {    void this.telegram.notifyLogsRead({ /* tool, agent, owner */ });  }}

Важная деталь: forensics-результат может затрагивать несколько пользователей по user_id (например, user_lookup вернул 12 человек) — мы пишем аудит-запись на каждого, чтобы потом можно было ответить на вопрос «кто и когда смотрел этого пользователя и данные с ним».

Слой 7. Машиночитаемые ошибки + плейбук «сначала repro, потом forensics»

Для чего я все это затеял, чтобы получать человеческое описание и на нем давать команды. Поэтому нужно было сделать транслятор через ИИ, ведь это то, что отличает «API для человека» от «API для агента». Ошибка несёт какой-то код, человекочитаемое сообщение и вывод уже делает ИИ, по которому агент понимает, что делать дальше, без расспросов пользователя:

function mkSystemError(code: string, message: string, details = {}) {  return { error: { code, message, details } };}// примеры из диспетчера:return mkSystemError('USER_NOT_FOUND', `User #${userId} not found`, {  next_action: 'use_user_lookup_first',  user_hint_ru: `Пользователь #${userId} не найден. Сначала вызови user_lookup.`,});

А чтобы агент не лез в forensics с порога, при initialize MCP-сессии мы подмешиваем плейбук — но только если у токена есть соответствующие scope. Главный принцип плейбука:

«Сначала repro, потом forensics».Жалоба на конкретный URL → logs_tail по nestjs-backend за последние 5–15 минут.Есть request_id из ответа фронта → сразу request_trace.Только если шагов 1–2 мало → user_lookup → user_activity_timeline.

Вообще отдельно был сделан целый ADMIN Playbook (cм.ниже), как ИИ общаться через эти тулы, на уровне few-shot’ов, чтобы ИИ понимал какие запросы могут быть и как правильно можно общаться.

Итоговая «луковица» доступа к Docker

  1. MCP-аутентификация (ApiKeyGuard) → scope system:logs:read + роль admin/developer.

  2. Rate-limit + clamp + whitelist сервисов в runSystemDebugTool/DockerLogsService.

  3. Код вызывает только read-операции (.logs.listContainers).

  4. Сетевая изоляция: backend → docker-socket-proxy по HTTP, без прямого сокета.

  5. Сокет только у прокси, и только :ro, прокси не выставлен наружу.

  6. Редакция секретов в потоке логов перед отдачей агенту.

  7. Аудит + Telegram на каждый вызов.

Теперь я просто пишу в Cursor: «Аня жалуется, что не может создать продуктовую книгу, посмотри почему». И агент сам идёт по тому же пути, что прошёл бы я — сначала логи бэкенда, потом трейс по request_id, и только если иначе никак — аккуратно в активность пользователя. Возвращается с диагнозом: «RolesGuard требует admin, а у него product — вот строка лога, вот запрос». А мне в Telegram падает уведомление, что агент действительно лазил в данные — ровно один раз и по делу.

Заключение

Получилось, что прод-дебаг переехал из терминала в чат. И дело не в том, что «ИИ умеет читать логи» — читать логи и я умею. Дело в том, что доступ к бою перестал быть бинарным. Не «или у тебя root на хосте, или ты ничего не видишь», а узкий, заранее очерченный коридор: только чтение, только разрешённые контейнеры, с лимитами, с вычищенными секретами и с несмываемым следом. Такой доступ не страшно дать агенту — да и живому стажёру, честно говоря, тоже.

Терминал, конечно, никуда не делся — когда надо что-то поменять, я по-прежнему иду руками. Но «посмотреть, что происходит на проде» теперь — это вопрос в чате, а не сессия в SSH. И поседеть от того, что агент получил доступ к боевому серверу, у меня так и не вышло — что, кажется, и было целью.

Вообщем хотел вот про такой паттерн рассказать.

Подобного паттерна я не нашел у Antropic и OpenAI, поэтому просто назвал его #debug_tools_pattern

ADMIN DEBUG playbook (для внешнего агента)

1. Главный принцип: «сначала repro, потом forensics»

Перед тем как читать чужие данные через user_* тулы, попытайся воспроизвести проблему сам или проверь backend-логи:

  1. Если жалоба на конкретный URL/действие — logs_tail по nestjs-backend за последние 5–15 минут.

  2. Если есть request_id в ответе фронта — сразу request_trace.

  3. Только если шага 1–2 недостаточно — user_lookup → user_activity_timeline.

Forensics-тулы (user_*отправляют уведомление владельцу системы в Telegram. Это нормально для разовой диагностики и ненормально, если ты вызываешь их «на всякий случай».

2. Каталог тулов

2.1. Diagnostics (scope: system:logs:read)

Тул

Назначение

Лимиты

logs_tail

Последние N строк stdout/stderr из одного контейнера. Whitelist: nestjs-backendfrontendpipeline-servicepostgres.

lines ≤ 500, since_minutes ≤ 1440

logs_search

Полнотекстовый поиск по нескольким контейнерам в окне ≤ 60 минут.

query ≥ 3 chars, limit ≤ 1000

request_trace

Метаданные одного HTTP-запроса по request_id (UUID из заголовка X-Request-Id) + строки backend-логов, упомянувшие этот id.

<!—FORENSICS_ONLY—>

2.2. Forensics (scope: system:users:debug) — admin only

Тул

Назначение

Лимиты

user_lookup

Поиск пользователей по подстроке email/name.

query ≥ 2 chars, до 50 результатов

user_activity_timeline

Таймлайн HTTP-запросов одного пользователя за окно ≤ 2 часов.

limit ≤ 500

Каждый вызов попадает в audit-log с target_user_id и шлётся в Telegram.

<!—/FORENSICS_ONLY—>

3. Канонический сценарий — «slava не может создать книгу продукта»

Демонстрационный канонический пайплайн «диагностика 1 проблемы 1 пользователя».

Step 1 — найти пользователя.

// → call: user_lookup({ query: "slava" })// ← reply (укорочено):{ "matches": [    { "id": 42, "login": "slava", "email": "slava@finam.ru",      "role": "product", "status": "active",      "products": [{ "id": 7, "name_ru": "FinamX" }] }  ], "total": 1 }

Если найдено > 1 — уточни у пользователя, кого именно. Если 0 — спроси, не опечатка ли это.

Step 2 — таймлайн за последние 30 минут.

// → call: user_activity_timeline({ user_id: 42, since_minutes: 30 })// ← reply (укорочено):{ "user": { "id": 42, "login": "slava", "role": "product" },  "entries": [    { "ts": "...", "method": "GET",  "route": "/product-profiles/:id",      "path": "/product-profiles/12", "status": 200, "duration_ms": 87,      "request_id": "9b5c..." },    { "ts": "...", "method": "POST", "route": "/product-profiles",      "path": "/product-profiles",   "status": 403, "duration_ms": 21,      "request_id": "1e7f...",      "error_message": "ForbiddenException: Insufficient permissions" }  ],  "summary": { "total": 14, "errors_4xx": 1, "errors_5xx": 0,               "slowest_ms": 412, "avg_ms": 95,               "period": { "from": "...", "to": "..." } } }

Видим 403 на POST /product-profiles — диагноз почти на поверхности.

Step 3 — обогатить контекст backend-логом.

// → call: request_trace({ request_id: "1e7f..." })// ← reply:{ "request": { …, "status": 403, "error_message": "Insufficient permissions" },  "related_logs": [    { "ts": "...", "level": "stderr", "text": "[RolesGuard] denied: required=admin, actual=product" }  ] }

Step 4 — отчитаться пользователю.

Slava (product) пытается POST /product-profiles. RolesGuard требует admin. Это и есть «поля неактивные» — фронт прячет UI, потому что get-роль = product. Чинить нужно либо матрицу ролей, либо декоратор @Roles('admin') на этом endpoint.

4. Канонический сценарий — «всё медленно после 14:00»

Phase C, в плане. В Phase A можно эмулировать через logs_search на 5xx + ручной анализ.

// → call: logs_search({//     services: ["nestjs-backend", "postgres"],//     since_minutes: 30,//     query: "ERROR",//     limit: 200// })

В Phase C появятся http_perf_summary (p95 по роутам) и db_slow_queries (pg_stat_statements).

5. Анти-паттерны

Плохо

Почему

Хорошо

user_activity_timeline без жалобы юзера

Любой forensics вызов = Telegram-уведомление и audit-запись на чужого юзера. Это инструмент инцидента, не разведки.

Сначала спроси, на что жалоба.

logs_search с query: "user" или "id"

Слишком общая подстрока, сразу truncated: true.

Конкретный URL, имя класса, или код ошибки.

logs_tail с lines: 500, since_minutes: 1440 для всех контейнеров подряд

Это много данных — оверхед на обе стороны.

Сузь окно до 5–30 минут, выбери 1 сервис.

Повторять forensics-вызов для каждого юзера в списке

Rate-limit 100/день и каждый = Telegram.

Сначала собери user_lookup, выбери 1–2 кандидатов, потом user_activity_timeline для каждого.

6. PII / privacy

  • user_lookup возвращает emailnamelast_seen. Это — PII.

  • В отчёте пользователю не цитируй email и имя, если в этом нет нужды для постановки диагноза.

  • system_request_logs НЕ хранит request body / headers. Если для отладки нужен body — сообщи и попроси у админа отдельный scope system:logs:read_pii.

7. Ошибки и next_action

Все system-debug тулы возвращают bilingual ошибки (как data-join):

{ "error": {    "code": "RATE_LIMIT_EXCEEDED",    "message": "...",    "details": {      "next_action": "wait_until_reset",      "resets_at": "2026-04-25T00:00:00.000Z",      "user_hint_ru": "Превышен дневной лимит вызовов...",      "user_hint_en": "Daily call limit exceeded..."    } } }

error.code

Что делать

RATE_LIMIT_EXCEEDED

Дождаться resets_at или попросить admin поднять лимит.

INVALID_REQUEST_ID

UUID должен быть из заголовка X-Request-Id ответа.

INVALID_USER_ID

Сначала вызови user_lookup.

USER_NOT_FOUND

То же.

SERVICE_NOT_ALLOWED

Используй один из whitelist: nestjs-backendfrontendpipeline-servicepostgres.

DOCKER_UNAVAILABLE

На бэкенде не примонтирован /var/run/docker.sock. Сообщи админу.

INSUFFICIENT_SCOPE

У токена нет нужного scope. Сообщи user’у — пусть запросит у admin.

ссылка на оригинал статьи https://habr.com/ru/articles/1049782/