Логирование во фронтенд-приложениях. Опыт Звука
Базовая реализация модуля заняла несколько строк кода (далее — пример кода):
public interface DataSource {
long open(DataSpec dataSpec) throws IOException;
int read(byte[] buffer, int offset, int length) throws IOException;
void close() throws IOException;
void addTransferListener(TransferListener transferListener);
@Nullablee
Uri getUri();
}
// Читабельные логи
const prettyTransport: pino.LoggerOptions["transport"] = { target: "pino-pretty" };
const prettyOptions = isPrettyEnabled ? { transport: prettyTransport } : {};
// Ecs форматированные логи
const ecsOptions = isEcsEnabled ? ecsFormat.({ convertErr: false }) : {};
return pino({
level: level || "trace",
...prettyOptions,
...ecsOptions,
...restOptions,
});
};
export { loggerFactory };
2. Модуль конфигурируется в каждом сервисе и затем подключается в другие файлы для создания единых экземпляров логгера в клиентской и в серверной частях приложения:
import { loggerFactory } from '@zvuk-web/logger';
import { IS_DEV_MODE } from 'config/constants/common';
import { LOG_LEVEL } from 'config/env/common';
export default loggerFactory({
level: LOG_LEVEL,
isPrettyEnabled: IS_DEV_MODE,
isEcsEnabled: !IS_DEV_MODE,
});
В данном примере отсутствует код для отправки клиентских логов, однако это можно легко реализовать, передав соответствующий колбэк в параметр browser.write. При этом у Pino есть очевидный минус: он не логирует модуль, в котором выполняется запись лога, по умолчанию. Для исправления этой проблемы важно использовать метод pino.child (logger, {file: «moduleA»}) в корне каждого файла, чтобы путь к исходному файлу был частью лога.
Мы используем модуль логирования в каждом сервисе таким образом:
— обработанные ошибки регистрируем через этот модуль, добавляя соответствующие данные в зависимости от контекста (здесь мы рассчитываем поймать ошибку, так как потенциально знаем ее причину);
— для необработанных исключений мы используем Sentry, чтобы сохранить трейс запроса, дополнительные данные и иногда воспроизведение сессии;
— в то же время мы все еще логируем сам факт возникновения исключения и стэйк трейс, чтобы соблюдать стандарт и хранить все логи в одном месте.
Все это позволяет нам разделить области ответственности и избежать излишнего наполнения логов, что экономит место на диске.
Пример кода инициализации Sentry в одном из сервисов:
import { init } from '@sentry/nextjs';
import parentLogger from 'src/core/logger';
const logger = parentLogger.child({ module: ‘sentry.client.config.ts’ });
init({
// Общий конфиг sentry
...,
// Перехватываем исключение перед отправкой в sentry
beforeSend: (event, hint) => {
const exception = hint.originalException || hint.syntheticException;
if (exception) {
logger.error(exception, '[UNHANDLED_EXCEPTION] Sentry beforeSend');
}
return event;
},
});
С учетом всего вышесказанного получилась следующая структура:
Структурное расположение модуля логирования в веб-приложении
Важно покрыть логированием участки кода, которые могут вызывать ошибки. Например, большинство клиентских ошибок мы обрабатываем в реакт ErrorBoundary. Ошибки при серверном рендеринге мы также логируем, оборачивая соответствующие блоки кода в try/catch — и в случае необработанных исключений отправляем их в Sentry через метод beforeSend. Помимо этого, мы логируем события инициализации приложения, плеера, авторизации и других критически важных модулей приложения с уровнем INFO.
Не стоит логировать чувствительные и персональные данные. Для этого полезно использовать функцию redaction в Pino, чтобы явно указать, какие данные необходимо скрыть в логах. Также не рекомендуется логировать в продакшн-режиме отладочные логи (debug и trace), чтобы не загромождать место хранения логов лишней информацией. Управление уровнем логирования через переменные окружения может помочь в этом.
Как мы работаем с логами
Мы используем модуль логирования для дебага в Dev-режиме с помощью модуля pino-pretty, который выводит логи в удобном формате с понятным стеком трейса. Логи также используются для исследования инцидентов и решения проблем клиентов при обращениях в техническую поддержку. Для этого удобно логировать некий идентификатор (например, хэш лога), который отображается пользователям во всплывающем сообщении об ошибке и позволяет быстро установить причину проблемы.
Еще один сценарий использования логирования — мониторинг веб-сервисов. Для каждого сервиса у нас реализовано несколько дашбордов с разной степенью детализации. На верхнем уровне мы мониторим ключевые показатели сервиса, такие как события проигрывания контента и показы страниц, а также общее количество серверных и клиентских ошибок и исключений.
Верхнеуровневые дашборды по логам
Дашборды нижних уровней включают основные показатели, а также возможность фильтрации данных по различным критериям, таким как географическое расположение, браузер, устройство и URL страницы. Особенно полезен дашборд, показывающий топ-10 ошибок, что позволяет быстро определить основные причины повышения уровня ошибок.
Кейс Звука по работе с логами
Один из последних кейсов, когда пришлось изучать логи, выглядел так:
У нас сработал алерт на процент валидных входящих запросов (меньше 95%). Проблему было видно сразу на 3 дашбордах — rps по кодам ответа, процент валидных входящих запросов и количестве серверных ошибок:
Пример дашбордов с возникшей проблемой
Аналогично возрастание фона ошибок можно увидеть в Kibana. Там же быстро находим детализацию по исследуемой проблеме:
Пример вывода лога с ошибкой в Kibana
Ошибка была идентифицирована вне нашего приложения, вероятно, на уровне проксирования запросов в Kubernetes. После совместного анализа с инженерами SRE было выяснено, что проблема связана с высвобождением памяти в Kong Ingress (у нас это легаси). Решением стало переведение соответствующего узла на Istio Ingress, что позволило быстро устранить ошибку.
В данном примере ошибка не зависела от наших веб-сервисов. Однако, когда ошибки или аномалии в показателях возникают из-за работы сервиса, полезно получать дополнительные инсайты, анализируя трейсы и профайлы. Современные инструменты для помощи — ELK/LGTM стек, SigNoz, New Relic и другие. Они помогают анализировать узкие места производительности, проблемы безопасности и находить инсайты. Мы также планируем внедрить подобный подход и рассматриваем логирование как часть единой платформы наблюдаемости веб-сервисов.
Подведем итоги
Сегодня, исходя из нашей практики логирования и поиска наиболее эффективных решений, можно выделить несколько полезных советов.
Рассматривайте логирование как часть наблюдаемости и мониторинга веб-сервисов.
Выстраивайте общую инфраструктуру для сбора логов с настройкой ротации.
Храните клиентские и серверные логи сервиса в едином месте.
В новых проектах лучше используйте библиотеку Pino как основу для модуля логирования.
Не стоит использовать Sentry в качестве инструмента логирования; его назначение — отлавливать и работать с исключениями.
Покрывайте логами только основные события и участки кода, избегайте логирования чувствительных данных.
Используйте логи для сквозного мониторинга сервисов — это ускоряет и упрощает поиск проблемных мест.
Спасибо, что прочитали! Если у вас есть вопросы — будем рады ответить на них в комментариях.