Pull to refresh
1297.9
МТС
Про жизнь и развитие в IT

Собираем логи веб-приложений в Kibana/Loki

Reading time7 min
Views7.7K

Привет, Хабр! Меня зовут Евгений Лабутин, я разработчик в МТС 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 гораздо удобнее анализировать проблемы, чтобы понять в каких браузерах и на каких устройствах воспроизводится, пройтись, так сказать, по хлебным крошкам.

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 4: ↑4 and ↓0+4
Comments13

Articles

Information

Website
www.mts.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия