Привет, Хабр! Меня зовут Евгений Лабутин, я разработчик в МТС Digital. Расскажу вам о том, как мы на нашем проекте МТС Твой бизнес собираем логи с клиентских веб‑приложений. А еще обсудим вспомогательный микросервис логирования, который мы вывели в Open source, и поговорим о том, как устроено логирование в принципе.

Для чего мы логируем?

Логируем мы для решения двух очень важных задач.

Первая — мы хотим понимать, как ведет себя ПО на клиентских устройствах. Мы, конечно, тестируем на большом количестве устройств весь функционал, который выводим в продакшн. Но такое тестирование — это все равно капля в море по сравнению с разнообразием устройств и браузеров клиентов.

В клиентских логах мы встречали ошибки заблокированного localStorage и добавили в код обработку недоступного хранилища. Встречали ошибку отсутствующего API браузера — добавляли полифил или поднимали версию минимально поддерживаемого браузера для отображения заглушки устаревшего браузера. Встречали ошибки в сценариях, не предусмотренные логикой и тестированием, — исправляли их.

Попадались нам ошибки в расширениях браузера и даже вирусы на клиентских устройствах, ошибки заблокированной аналитики, ошибки потери интернет‑соединений. Все эти инциденты мы отрабатывали. Самая странная ошибка, которая у нас была — это ошибка во внешнем скрипте. Воспроизводится она только в mi‑браузерах у некоторых клиентов. Если знаете, что это за внешний скрипт — дайте знать в комментариях.

Логирование этих ошибок позволяет значительно повысить качество разрабатываемого ПО и фиксить проблемы до того, как пользователи их заметят.

Вторая задача — обработка инцидентов. Каждый раз, когда у клиента возникает ошибка в ПО, он звонит на горячую линию МТС и сообщает о проблеме. Оператор заводит инцидент во внутренней системе, далее информация попадает к нам в команду и мы начинаем разбор проблемы по логам. По ним мы можем воспроизвести цепочку действий пользователя и найти ошибку. Был даже случай, когда человек перестал пользоваться сервисом, но забыл отключить подписку. При обращении в колл‑центр он сообщил об этом и сказал, что не пользовался услугами. Мы проверили логи и действительно не нашли активности пользователя после даты списания. Деньги клиенту вернули.

Так логирование помогает значительно повысить качество ПО и лучше разбираться в ошибках, возникающих у пользователей.

Что мы логируем?

На стороне веб‑приложения логируются обычно исключения внутри приложения, глобальные исключения, важные события в логике (например, оплата). В набор этих логов входят: текст ошибки, стектрейс, локация ошибки, навигатор, идентификатор пользователя.

Никакие персональные данные не логируются по соображениям безопасности. Достаточно вспомнить взлом системы логирования Kibana (ELK), который позволял загружать на сервер чужой код и, в том числе, воровать чужие логи. При отсутствии логирования персональных данных украсть их становится значительно сложнее. А для идентификации логов с целью обработки инцидентов достаточно идентификатора пользователя.

Как логировать правильно?

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

Кажется, что самый простой способ логирования выглядит так:

console.log("Это самое правильное логирование!");

Но есть проблема, такой код можно прочитать только на клиентском устройстве. Можно, конечно, подменить API браузерной консоли для отправки на сервер, но это антипаттерн, который называется манки‑патчинг и мы к нему прибегать не будем.

Kibana и Loki не имеют своих модулей логирования в веб‑приложениях. Но можно найти известный аналог под названием Sentry. Это готовая, удобная, и наглядная система логирования для клиентских ошибок. Тогда может логировать через Sentry?

import * as Sentry from "@sentry/browser";

Sentry.captureMessage("Вот теперь точно лучшая система логирования!!!");

Но нет, при таком способе мы не увидим логи локально, сделаем вендор лок на систему Sentry, не сможем корректно настроить уровни логирования.

Логируем правильно!

Для логирования мы используем метод, принятый в корпоративной разработке. Мы используем объект, реализующий паттерн helper, выглядит он следующим образом:

import * as Sentry from "@sentry/browser";

export class Logger {

    public logLevel: LogLevels = LogLevels.Info;

    public constructor () {
        Sentry.init({
            dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
            maxBreadcrumbs: 50
          });
    }

    public log(message: string, data: unknown) {
        if (this.logLevel <= LogLevels.Info) {
            console.log(message, data);
            Sentry.captureMessage(message, "debug");
        }
    }

    public warning(message: string, data: unknown) {
        if (this.logLevel <= LogLevels.Warn) {
            console.warn(message, data);
            Sentry.captureMessage(message, "warning");
        }
    }

    public error (message: string, data: unknown, error: Error) {
        if (this.logLevel <= LogLevels.Error) {
            console.error(message, data, error);
            Sentry.captureMessage(message, "error");
            Sentry.captureException(error);
        }
    }

    public info/debug/trace (message: string, data: unknown) {...}
}

// singleton
export const logger = new Logger();

Вызывается такой объект‑помощник через DI, если он есть. Если DI нет — как уже созданный singleton объект logger.

Написав такой объект, мы создали абстракцию, которая позволяет избавиться от привязки к конкретной системе логирования. Теперь при логировании в коде мы везде используем этот объект и не боимся, что рано или поздно систему логирования надо будет поменять. Кроме того, мы получили возможность настраивать уровень логирования в зависимости от потребностей и в случае необходимости можем включить более детальные логи отдельному клиенту.

Внезапно мы потеряли Sentry, но обрели Kibana

По мере цифровой трансформации компании МТС нам понадобилось поменять контуры в которых хостятся наши приложения. В старом контуре была отлаженная система логирования, построенная на Sentry, но по не зависящим от нас причинам от него пришлось отказаться (надеюсь, что временно). Зато в новом контуре уже вовсю работала система логирования ELK, с панелью визуализацией на Kibana.

Оставался только вопрос: как логи веб‑приложения отправить в Kibana? Ведь она умеет работать только с серверными логами и не имеет модуля для веб‑приложений. А некоторые приложения (в том числе микрофронтенды) еще и опубликованы, как статичные файлы, и не имеют своего серверного приложения.

Ответ на вопрос оказался крайне прост — мы сделали микросервис, который принимает логи от веб‑приложения и выводит их в консоль контейнера. Далее контейнеры уже имеют функционал по считыванию логов и отправку их систему логирования: Logstash (часть ELK), Loki и другие.

Первым делом мы трансформировали код нашего логера, закомментировав там Sentry на случай, если он вернется. А еще добавили код который засылает логи на микросервис логирования.

Код принял следующий вид:

export class Logger {

    ...
  
    public log(message: string, data: unknown) {
        if (this.logLevel <= LogLevels.Info) {
            console.log(message, data);
            // Sentry.captureMessage(message, "debug");
            fetch("/logs/log/info", {
                method: "POST",
                body: JSON.stringify({
                    message: message,
                    data: data,
                    userAgent: navigator.userAgent,
                    location: location.href,
                    ...other data
                })
            });
        }
    }

    ...

}

Добавив всего 10 строчек в наш логер, мы научили его работать с новым микросервисом логирования. А в целом такая абстракция как логер помогла нам избавиться от правки тысяч строк кода, в которых происходит логирование.

Микросервис логирования

Микросервис мы создали по уже отлаженному процессу, о котором я писал ранее. Взяли фреймворк NestJS, создали всего один контроллер с логикой, закатали в контейнер и опубликовали. Сам микросервис мы вывели в Open source, код выложен на github, а готовый контейнер можно запустить прямо сейчас из dockerhub.

Вместо NestJS могло бы быть что‑то легковесное. Например, C# или Go, но тогда кроме меня их некому было бы поддерживать.

Вся логика контроллера выглядит следующем образом:


@Controller("logs")
export class LogController {

    @Post("log")
    public writeLog(@Body() body: object, @Headers() headers: object): void {
        console.log(
            JSON.stringify({
                ...body,
                logLevel: LogLevels.Info,
                time: Date.now(),
                userIp: headers["x-real-ip"],
                traceId: headers["x-trace-id"]
            })
        );
    }

}

Этот микросервис запускается в контейнере, логи с которого считывает драйвер логирования LogStash, в результате чего мы наблюдаем логи в Kibana.

Таким образом, вся трансформация системы логирования заняла всего пару часов. Они ушли на написание и деплой контейнера, а также на правки кода логера.

Что дает Kibana?

Kibana — это интерфейс для работы с логами, он позволяет фильтровать их, строить графики, делать свои дашбоарды и (самое полезное) смотреть сквозные логи. Можно построить сквозные логи по всей системе по идентификатору пользователя или другим параметрам, понять, какую кнопку клиент нажал на фронтенде и к каким последствиям это привело на бэкенде.

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

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

Комментарии (11)


  1. StanKondrat
    00.00.0000 00:00
    -1

    Fetch на каждый лог, а очередь отправки, что если приложение временно оффлайн?

    JSON парсится как у вас в тексте, не безопасно?


    1. LabEG Автор
      00.00.0000 00:00

      Это демонстрационная версия логгера, она упрощена для наглядности. В реальном логгере можно докручивать необходимый вам функционал, например обогащение информацией, обработку отсутствия сети, использование sendBeacon вместо fetch и т.п.


    1. LabEG Автор
      00.00.0000 00:00

      Атака на __proto__ и другие не эксплуатируема. Аналогичная уязвимость в Кибане так же закрыта. При попытке эксплуатации мы увидим код атаки и IP атакующего.


  1. tigmen
    00.00.0000 00:00

    Здравствуйте, отличная статья. Возможна ли реализация с записью логов на устройстве пользователя и их отправке на сервер при обращении в тех поддержку?


    1. LabEG Автор
      00.00.0000 00:00

      Чисто технически да. Но мы используем другой подход. Если у клиента случается ошибка то на экране всплывает оранжевый тоаст с кодом ошибки. Код представляет из себя последние четыре цифры хеша ошибки.

      Клиенту или тестировщику достаточно сообщить эти четыре цифры что бы их легко найти в логах.


  1. p0vidl0
    00.00.0000 00:00
    +1

    Спасибо за статью.

    Подскажите, а как решили вопрос авторизации запросов к этому сервису? Или любой может спамить логи?


    1. LabEG Автор
      00.00.0000 00:00

      В связи с тем что логи собираются в т.ч. с неавторизованных клиентов авторизацию прикрутить невозможно. Любая авторизация для анонимных клиентов невозможна ввиду того что весь код в вебе является опенсурсом и авторизация элементарно копируется. Единственный способ защиты это антиспам системы и другие системы защиты применяемые для обычного апи.


  1. romankspb
    00.00.0000 00:00
    +1

    Можно микросервис логирования выкинуть и использовать http input того же logstash


  1. marchenko_dev
    00.00.0000 00:00
    +1

    Вместо fetch для отправки логов стоит использовать sendBeacon API, он как раз предназначен для отправки метрик/логов


  1. masimoleblanc
    00.00.0000 00:00

    По тексту встречаю "Kibana или Loki". Что используется так и не понятно. Описанные логи, выглядят не слишком структурированными, и кажется что Loki (с которым используют для визуализации Grafana, а не Kibana) для этого не очень хорошо подходит, так как не является движком полнотекстового поиска и ограничен в лейблах(индексах).

    Может у вас все таки Kibana + Elastic Search ?


    1. LabEG Автор
      00.00.0000 00:00

      У нас стек ELK. Loki в названии для примера потому что таким способом можно собирать логи с любой системы серверной логирования.