Логирование во фронтенд-приложениях. Опыт Звука

Базовая реализация модуля заняла несколько строк кода (далее — пример кода):

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

Пример вывода лога с ошибкой в Kibana

Ошибка была идентифицирована вне нашего приложения, вероятно, на уровне проксирования запросов в Kubernetes. После совместного анализа с инженерами SRE было выяснено, что проблема связана с высвобождением памяти в Kong Ingress (у нас это легаси). Решением стало переведение соответствующего узла на Istio Ingress, что позволило быстро устранить ошибку.

В данном примере ошибка не зависела от наших веб-сервисов. Однако, когда ошибки или аномалии в показателях возникают из-за работы сервиса, полезно получать дополнительные инсайты, анализируя трейсы и профайлы. Современные инструменты для помощи — ELK/LGTM стек, SigNoz, New Relic и другие. Они помогают анализировать узкие места производительности, проблемы безопасности и находить инсайты. Мы также планируем внедрить подобный подход и рассматриваем логирование как часть единой платформы наблюдаемости веб-сервисов.

Подведем итоги

Сегодня, исходя из нашей практики логирования и поиска наиболее эффективных решений, можно выделить несколько полезных советов. 

  • Рассматривайте логирование как часть наблюдаемости и мониторинга веб-сервисов.

  • Выстраивайте общую инфраструктуру для сбора логов с настройкой ротации.

  • Храните клиентские и серверные логи сервиса в едином месте.

  • В новых проектах лучше используйте библиотеку Pino как основу для модуля логирования.

  • Не стоит использовать Sentry в качестве инструмента логирования; его назначение — отлавливать и работать с исключениями.

  • Покрывайте логами только основные события и участки кода, избегайте логирования чувствительных данных.

  • Используйте логи для сквозного мониторинга сервисов — это ускоряет и упрощает поиск проблемных мест.

Спасибо, что прочитали! Если у вас есть вопросы — будем рады ответить на них в комментариях. 

© Habrahabr.ru