Мониторинг ошибок приложения
Мониторинг ошибок приложения в Наблюдателе работает без отдельного SDK. Агент стека читает stdout и stderr ваших контейнеров, находит в них исключения и записи уровня error, а Наблюдатель собирает одинаковые случаи в одну ошибку: со стеком, счётчиком повторов, версией приложения и ссылкой на трассу запроса.
Если вы искали аналог Sentry, который не требует менять код, это он: достаточно, чтобы приложение писало ошибки в стандартный вывод контейнера. Ниже — что распознаётся, как устроена вкладка «Ошибки» и как писать логи, чтобы ошибки разбирались хорошо.
Где работает мониторинг ошибок
| Окружение | Как собираются ошибки |
|---|---|
| Docker Compose | агент стека читает логи всех контейнеров своего compose-проекта |
| Kubernetes | агент 0.8.0 или новее читает логи контейнеров работающих подов (кроме istio-proxy и linkerd-proxy). Если контейнер упал и перезапустился, агент дочитывает лог упавшего запуска, поэтому стек паники не теряется |
| Сервер (Linux) | агент сервера логи приложений не читает — для ошибок нужен агент стека |
Как поставить агента — в статьях про Docker Compose и Kubernetes.
Какие ошибки распознаются
| Что в логе | Что получится |
|---|---|
Go: panic: со стеком горутины (полный стек — агент 0.8.0+) |
исключение со стеком |
Python: Traceback (most recent call last): … KeyError: 'sku' |
исключение со стеком; название — по самому исключению (агент 0.8.1+) |
Node.js: TypeError: … или Error [CODE]: … и строки at fn (/app/x.js:1:2) (агент 0.7.1+) |
исключение со стеком и свойствами ошибки (code, detail) |
JSON-лог с полем level = error или fatal (у pino — 50 и 60): pino, bunyan, winston, slog, zap и другие |
ошибка; текст — из msg/message и err/error, стек — из stack |
Текстовая строка со словом ERROR, FATAL или ERR |
ошибка без стека |
Шум отфильтровывается: обрывы соединения клиентом, предупреждения (WARN) со словом «error» внутри, сообщения об успехе.
Новая ошибка появляется на вкладке через 30–90 секунд: агент просматривает логи раз в 30 секунд, и каждый контейнер — не реже раза в три цикла. Полные логи в облаке не хранятся, только распознанные ошибки. Посмотреть свежие строки контейнера целиком можно в живых логах.
Как ошибки группируются
Повторы одной ошибки собираются в одну запись. Для исключения учитываются сервис, тип исключения, текст и верхние строки стека из вашего кода. Для ошибки без стека — сервис и текст. Числа и отметки времени в тексте при этом сглаживаются.
Отсюда главное правило: текст ошибки должен быть стабильным. Если в сообщение попадает id заказа, email или длительность запроса, каждый случай станет отдельной ошибкой. Такие значения выносите в отдельные поля JSON-лога.
Повтор той же ошибки в течение 5 минут увеличивает счётчик и не создаёт нового события.
Вкладка «Ошибки»
Вкладка «Ошибки» есть на странице каждого стека. Наверху — сводка: сколько ошибок открыто, без уведомлений, решено, сколько активны за последний час и сколько новых за сутки.
Ниже — список с фильтрами «Открытые», «Без уведомлений», «Решённые» и «Все», поиском по тексту или сервису и выбором сервиса. В каждой строке:
- текст ошибки и пометка, исключение это или запись лога;
- место в коде приложения по стеку, например
postProjectWebhook (outboundWebhooks/httpClient.js:74)— видно, что именно падает, даже если текст общий вроде «fetch failed»; - сервис, число повторов и когда ошибка была в последний раз;
- пометки «новая», «закрыта сама», «без уведомлений».
Цвет показывает, насколько ошибка «живая»: повторялась в последний час — красная, открыта, но затихла — жёлтая.
Статусы ошибки: открыта, решена, без уведомлений
| Статус | Когда ставить | Что будет дальше |
|---|---|---|
Открыта (open) |
ошибка в работе | видна в «Открытых» |
Решена (resolved) |
исправление уже выложено | уходит из открытых. Если ошибка повторится, Наблюдатель откроет её снова и пришлёт уведомление о повторе — так видно, что исправление не помогло |
Без уведомлений (muted) |
известный шум, который не будут чинить: отменённые клиентом запросы, ожидаемые ошибки внешнего API | остаётся в списке, повторы считаются, но уведомлений о ней больше нет |
Статус меняется кнопками на странице ошибки: «Решена», «Открыть снова», «Без уведомлений», «Вернуть уведомления». Для этого нужна роль редактора в проекте.
Отмечайте ошибку решённой после выкладки исправления, а не сразу после правки кода. Иначе следующий повтор со старой версии откроет её снова.
Автозакрытие. Открытая ошибка, которая не повторялась 7 дней, закрывается сама с пометкой «Закрыта сама». Если она вернётся, Наблюдатель откроет её и пришлёт уведомление, как после ручного закрытия. Ошибки «Без уведомлений» сами не закрываются. Ошибка, которая не повторялась 30 дней, удаляется из списка.
Страница ошибки
Клик по строке открывает страницу ошибки. На ней:
- Заголовок и статус: «Повторяется», «Открыта», «Решена», «Закрыта сама» или «Без уведомлений», место в коде, версия приложения в последнем повторе, когда ошибка появилась впервые.
- Плитки: повторов всего, последний раз, повторов за 24 часа в сравнении с прошлыми сутками, возвращалась ли ошибка после закрытия.
- Частота за 48 часов или 30 дней с отметками «Появилась», «Вернулась после закрытия», «Закрыта сама».
- Текст ошибки и стек последнего повтора. Строки вашего кода выделены, строки библиотек и рантайма приглушены. Длинный стек свёрнут, кнопка «Копировать» копирует его целиком.
- Лог рядом с ошибкой — до 5 строк того же контейнера до и после ошибки (агент 0.9.2+). Причина часто записана именно там: адрес запроса, id заказа, предыдущий сбой.
- Где возникает — в каких контейнерах, и Версии — в каких релизах встречалась.
- История и Повторы — у каждого повтора можно открыть его стек, лог рядом, трассу запроса и разбор инцидента контейнера.
Совет. Если на странице ошибки нет ни причины в логе рядом, ни понятного стека, приложение, скорее всего, пишет только
err.stack. Логируйте ошибку целиком (в Node.jsconsole.error(err)печатает иcause) и кладите адрес и параметры запроса в ту же запись.
Связь ошибки с трассой запроса
Если приложение подключено к трассировке, у повтора появляется ссылка «трасса» — запрос, в котором случилась ошибка, со всеми вызовами баз и сервисов. Для этого в записи об ошибке должен быть идентификатор трассы:
- в JSON-логе — поля
trace_id(32 шестнадцатеричных символа) иspan_id(16 символов); - в текстовом логе —
traceparent=00-<trace_id>-<span_id>-01.
Большинство логгеров умеют добавлять trace_id сами: pino — через @opentelemetry/instrumentation-pino, Python — через opentelemetry-instrumentation-logging, в Go его берут из trace.SpanContextFromContext. Трассы с ошибками хранятся 30 дней.
Как писать логи, чтобы ошибки разбирались хорошо
Пишите в stdout/stderr контейнера, а не в файлы внутри него.
Предпочитайте JSON-логи, одна запись — одна строка, уровень — в поле
level:json{"level":"error","time":"2026-10-30T12:00:00Z","msg":"payment failed","err":{"type":"TimeoutError","message":"upstream timeout","stack":"TimeoutError: upstream timeout\n at charge (/app/pay.js:42:11)"},"order_id":"A-1042","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7"}Держите текст стабильным: id, суммы и длительности — в отдельные поля.
Многострочные стеки в текстовых логах печатайте целиком, без префиксов на строках продолжения.
Не логируйте ожидаемое (404, ошибки валидации ввода) уровнем
error— используйтеwarnилиinfo.
Node.js (pino)
const pino = require('pino');
const logger = pino(); // JSON в stdout, level 50 = error
try {
await charge(order);
} catch (err) {
logger.error({ err, order_id: order.id }, 'payment failed'); // err попадёт в поле err со stack
}Python
Стандартный logging в stdout: logger.exception печатает Traceback, и ошибка придёт со стеком.
import logging, sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s")
log = logging.getLogger("payments")
try:
charge(order)
except Exception:
log.exception("payment failed") # ERROR + Traceback (most recent call last) ...Go (slog)
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
if err := charge(ctx, order); err != nil {
// {"time":"…","level":"ERROR","msg":"payment failed","err":"upstream timeout","order_id":"A-1042"}
logger.ErrorContext(ctx, "payment failed", "err", err, "order_id", order.ID)
}Паника с полным стеком горутины распознаётся сама — печатать её специально не нужно.
.NET (Serilog)
Чтобы уровень попал в поле level, задайте формат JSON через Serilog.Expressions:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(new ExpressionTemplate(
"{ {time: @t, level: @l, msg: @m, err: @x, ..@p} }\n"))
.CreateLogger();
Log.Error(ex, "payment failed {OrderId}", order.Id);Как проверить
Выведите в stdout любого контейнера стека строку {"level":"error","msg":"nabl test error"} или вызовите тестовую ошибку в приложении. Через 1–2 минуты она появится на вкладке «Ошибки».
Персональные данные в ошибках
С агентом 0.9.0 и новее текст ошибки, стек и строки лога рядом обезличиваются до отправки: email, телефоны, номера карт, ИНН и СНИЛС заменяются маской (i***@m***.ru), пароли и токены — на ***. Одна и та же ошибка у разных пользователей остаётся одной записью. Найти ошибку по email или телефону поэтому нельзя — в облаке их нет. Подробнее — в статье как работает агент.
Уведомления об ошибках
О новой ошибке и о её возврате после «решено» Наблюдатель пишет событие уровня «ошибка» в ленту стека и отправляет его в каналы проекта. Шумную ошибку, которую не будут чинить, переведите в «Без уведомлений», а не отключайте уведомления канала. Каналы настраиваются в статье каналы уведомлений.
Разбирать ошибки удобно и через ИИ-ассистента: он видит стек, строки лога рядом и связанную трассу, а с правами на изменение может закрыть ошибку после выкладки исправления.
Что дальше
- Трассировка OpenTelemetry — чтобы у ошибки была трасса запроса.
- Живые логи контейнеров — свежие строки лога целиком.
- Каналы уведомлений — куда придёт новая ошибка.
- ИИ-ассистент — разбор ошибки по стеку, логу и трассе.
Первый сервер или стек подключается за пару минут. Бесплатно в раннем доступе.
Нашли неточность или не хватает ответа? Напишите на hello@nablserver.ru