Переход на JSON-логи обычно делают за день, а потом год живут с полями, которые невозможно запрашивать. Несколько правил, которые нам помогли.

Правило 1: стабильное имя, изменчивое значение

Плохо: {"user_42_action": "login"}. Хорошо: {"event": "login", "user_id": 42}. Имя поля — часть схемы, значение — данные. Если имя зависит от данных, индекс превращается в свалку.

Правило 2: одно событие — одна строка

Многострочные записи разваливаются при конкурентной записи в stdout. Стек-трейс кладите в поле строкой с \n, а не отдельными строками.

logger.error({
  event: 'order.create.failed',
  order_id: order.id,
  user_id: user.id,
  reason: 'payment_declined',
  provider_code: error.code,
  stack: error.stack,        // одной строкой, не построчно
});

Правило 3: единицы измерения в имени

latency — это что, секунды или миллисекунды? Через полгода никто не вспомнит. latency_ms вопрос снимает.

Правило 4: не логируйте то, что уже есть

Регион, ревизия, идентификатор инстанса, метод и путь добавляются платформой. Дублирование раздувает объём и расходится при рефакторинге.

Минимальная схема

{
  "ts": "2026-07-18T09:14:22.108Z",
  "level": "error",
  "event": "order.create.failed",
  "msg": "платёж отклонён провайдером",
  "trace_id": "9f1c4b2ae8d34c1f",
  "user_id": 8412,
  "order_id": "ord_9a21",
  "reason": "payment_declined",
  "duration_ms": 842
}

Что запрашивать потом

# все отказы платежей за сутки, сгруппированные по причине
vlone logs --since 24h \
  --filter 'event == "order.create.failed"' \
  --group-by reason --count

# конкретная трасса целиком, включая соседние сервисы
vlone logs --trace 9f1c4b2ae8d34c1f

Чего не делать

  • Не пишите персональные данные: логи хранятся дольше, чем вы думаете.
  • Не логируйте тела запросов целиком — только размер и хеш.
  • Не используйте level: "info" для всего подряд: если всё важное, значит, ничего не важно.