Pull to refresh

LogRock: Тестирование через логирование

Reading time7 min
Views13K
LogRock

LogRock: Тестирование через логирование


Уже более 2-х лет мы работаем над своим проектом Cleverbrush. Это софт для работы с векторной графикой. Работа с графическим редактором подразумевает огромное количество вариантов использования приложения. Мы пытаемся экономить деньги и время, поэтому оптимизируем все, в том числе тестирование. Покрывать тест кейсами каждый вариант это слишком дорого и нерационально, тем более что все варианты покрыть невозможно.

В ходе разработки был создан модуль для React JS приложений — LogRock (github).

Этот модуль позволяет организовать современное логирование приложения. На основании логов мы производим тестирование. В этой статье я расскажу Вам о тонкостях использования данного модуля и как организовать тестирование через логирование.

В чем проблема?


Если сравнивать программу с живым организмом, то баг в ней — это болезнь. Причиной возникновения этой «болезни» может стать целый ряд факторов, в том числе и окружение конкретного пользователя. Это особенно актуально если мы рассматриваем веб-платформу. Иногда причинно-следственная связь очень сложная, и баг, который нашли при тестировании — результат целого ряда событий.

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

Что же делать?


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

Для того, чтобы наша программа сама нам могла сообщить что у неё «болит», мы возьмем модуль LogRock (github) и свяжем его с ElasticSearch, LogStash и Kibana.

image

ElasticSearch — мощная система полнотекстового поиска. Можете посмотреть тутор по ElasticSearch здесь.
LogStash — система сбора логов из всевозможных источников, которая умеет отправлять логи в том числе и в ElasticSearch.
Kibana — веб-интерфейс к ElasticSearch с большим количеством дополнений.

Как это работает?


image

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

image

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

В данной статье я НЕ буду рассматривать настройку ElasticStack!

Создание системы логирования


Для примера мы будем интегрировать систему логирования в одностраничном JS приложении, написанном на React. В действительности не важно на каком фреймворке будет написано ваше приложение. Я постараюсь описать сам подход построения лог системы.

1. Клиент


1.0 LogRock. Установка


Ссылка на LogRock

Для установки необходимо выполнить:

npm install logrock
или
yarn add logrock

1.1 LogRock. Настройка приложения


Для начала обернем наше приложение в компонент

import { LoggerContainer } from "logrock";

<LoggerContainer>
    <App />
</LoggerContainer>

LoggerContainer – это компонент который реагирует на ошибки вашего приложения и формирует стек.

Стек – это объект с информацией о операционной системе пользователя, браузере, какая кнопка мыши или клавиатуры была нажата и конечно же подмассив actions, где записаны все действия юзера, которые он совершил в нашей системе.

LoggerContainer имеет ряд настроек, рассмотрим часть из них

<LoggerContainer
    active={true|false}
    limit={20}
    onError={stack => {
        sendToServer(stack);
    }}
>
    <App />
</LoggerContainer>

active – включает или отключает логгер

limit – задает лимит на количество сохраняемых последних действий юзером. Если юзер совершит 21 действие, то первое в данном массиве автоматически удалится. Таким образом, мы будем иметь 20 последних действий, которые предшествовали ошибке.

onError – колбек, который вызывается в момент возникновения ошибки. В него приходит объект Стека, в котором сохранена вся информация об окружении, действиях пользователя и т.д. Именно из этого колбека нам необходимо отправлять эти данные в ElasticSearch или бекенд, или сохранять в файл для дальнейшего анализа и мониторинга.

1.2 LogRock. Логирование


Для того, чтобы произвести качественное логирование действий пользователя, нам придется покрыть наш код лог-вызовами.

В комплекте модуля LogRock идет логгер, который связан с LoggerContainer

Предположим у нас есть компонент

import React, { useState } from "react";

export default function Toggle(props) {
    const [toggleState, setToggleState] = useState("off");
    function toggle() {
        setToggleState(toggleState === "off" ? "on" : "off");
    }
    return <div className={`switch ${toggleState}`} onClick={toggle} />;
}

Для того, чтобы его правильно покрыть логом, нам нужно модифицировать метод toggle

function toggle() {
    let state = toggleState === "off" ? "on" : "off";
    logger.info(`React.Toggle|Toggle component changed state ${state}`);
    setToggleState(state);
}

Мы добавили логгер, в котором информация разделена на 2 части. React.Toggle показывает нам, что данное действие произошло на уровне React, компонента Toggle, а далее мы имеем словесное пояснение действия и текущий state, который пришел в этот компонент. Подобное разделение на уровни не обязательно, но с таким подходом будет понятнее, где конкретно выполнился наш код.

Также мы можем использовать метод “componentDidCatch”, который введен в React 16 версии, на случай возникновения ошибки.

2. Взаимодействие с сервером


Рассмотрим следующий пример.

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

Во-первых, так как у нас клиентское приложение, все запросы идущие на сервер будут проходить в рамках одной сессии юзера, без перезагрузки страницы. Для того, чтобы связать действия на клиенте с действиями на сервере, мы должны создать глобальный SessionID и добавлять его в хедер к каждому запросу на сервер. На сервере же мы можем использовать любой логгер, который будет покрывать нашу логику подобно примеру с фронтенда, и в случае возникновения ошибки отправлять эти данные с прикрепленным sessionID в Elastic, в табличку Backend.

1. Генерируем SessionID на клиенте

window.SESSION_ID = `sessionid-${Math.random().toString(36).substr(3, 9)}`;

2. Мы должны установить SessionID для всех запросов на сервер. Если мы используем библиотеки для запросов, это сделать очень просто, объявив SessionID для всех запросов.

let fetch = axios.create({...});
fetch.defaults.headers.common.sessionId = window.SESSION_ID;

3. В LoggerContainer есть специальное поле для SessionID

<LoggerContainer
    active={true|false}
    sessionID={window.SESSION_ID}
    limit={20}
    onError={stack => {
        sendToServer(stack);
    }}
>
    <App />
</LoggerContainer>

4. Сам запрос (на клиенте) будет выглядеть так:

logger.info(`store.getData|User is ready for loading... User ID is ${id}`);

getData('/api/v1/user', { id })
    .then(userData => {
        logger.info(`store.getData|User have already loaded. User count is ${JSON.stringify(userData)}`);
    })
    .catch(err => {
        logger.error(`store.getData|User loaded fail ${err.message}`);
    });

Как это все будет работать: у нас записывается лог, перед запросом на клиенте. По нашему коду мы видим, что сейчас начнется загрузка данных с сервера. Мы прикрепили SessionID к запросу. Если у нас покрыт бекенд логами с добавлением этого SessionID и запрос завершился ошибкой, то мы можем посмотреть что случилось на бекенде.

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

3. Тестировщик


Работа с тестировщиком заслуживает отдельного описания процесса.

Так как у нас стартап, формальных требований мы не имеем и иногда в работе не все логично.

Если тестировщик не понимает поведение — это случай, который как минимум нужно рассмотреть. Также, зачастую, тестировщик просто не может повторить одну ситуацию дважды. Так как шаги, приведшие к некорректному поведению, могут быть многочисленными и нетривиальными. К тому же, не все ошибки приводят к критическим последствиям, таким как Exception. Некоторые из них могут лишь менять поведение приложения, но не трактоваться системой как ошибка. Для этих целей на стейджинге можно добавить кнопку в хедере приложения для принудительной отправки логов. Тестировщик видит, что что-то работает не так, нажимает на кнопку и отправляет Стек с действиями на ElasticSearch.

image


Если все-таки критическая ошибка произошла, мы должны блокировать интерфейс, чтобы тестировщик не кликал дальше и не заходил в тупик.

Для этих целей, мы выводим синий экран смерти.

Мы видим вверху текст со Стеком этой критической ошибки, а внизу — действия, которые ей предшествовали. Еще мы получаем ID ошибки, тестировщику достаточно его выделить и прикрепить к тикету. В последствии эту ошибку легко можно будет найти в Kibana по этому ID.

image

Для этих целей в LoggerContainer есть свои свойства

<LoggerContainer
    active={true|false}
    limit={20}
    bsodActive={true}
    bsod={BSOD}
    onError={stack => {
        sendToServer(stack);
    }}
>
    <App />
</LoggerContainer>

bsodActive – включает/отключает BSOD (отключение BSOD применимо к продакшен коду)

bsod – это компонент. По умолчанию, он выглядит как выше приведенный скриншот.

Для вывода кнопки в UI LoggerContainer, мы можем использовать в context

context.logger.onError(context.logger.getStackData());

4. LogRock. Взаимодействие с пользователем


Вы можете выводить логи в консоль или показывать их юзеру, для этого нужно использовать метод stdout:

<LoggerContainer
    active={true|false}
    limit={20}
    bsodActive={true}
    bsod={BSOD}
    onError={stack => {
        sendToServer(stack);
    }}
    stdout={(level, message, important) => {
          console[level](message);

          if (important) {
              alert(message);
          }
    }}
>
    <App />
</LoggerContainer>

stdout – это метод, который отвечает за вывод сообщений.

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

logger.log('Something was wrong', true);

Продвинутое логирование


Если вы используете Redux, или подобные решения с одним Store, вы можете в Middleware обработки ваших Actions поставить logger, тем самым, все значимые действия будут проходить через нашу систему.

Для эффективного логирования можно оборачивать ваши данные в Proxy-объект, и ставить логгеры на всех действиях с объектом.

Для покрытия логированием сторонних методов (методов библиотек, методов Legacy кода), вы можете использовать декораторы — “@”.

Советы


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

Не забудьте указать о сборе логов в лицензионном соглашении.

НЕ логируйте пароли, банковские данные и прочую личную информацию!

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

Альтернативы


В качестве альтернативных подходов я выделяю:

  • Rollbar хорошо настраиваемый. Позволяет логировать 500 тысяч ошибок за 150$ в месяц. Рекомендую использовать, если вы разрабатываете приложение с нуля.
  • Sentry более прост в интеграции, но менее кастомизируем. Позволяет логировать 1 миллион событий за 200$ в месяц.

Оба сервиса позволяют делать почти тоже самое и интегрироваться в бекенд.

Что дальше


Логирование — это не только поиск ошибок, это еще и мониторинг действий пользователя, сбор данных. Логирование может быть хорошим дополнением к Google Analytics и проверкой User Experience.

Выводы


Когда вы выпускаете приложение, для него жизнь только начинается. Будьте ответственны за свое детище, получайте отзывы, следите за логами и улучшайте его. Пишите качественный софт и процветайте :)

P.S. Если хотите помочь с разработкой модулей для Angular, Vue и т.д. буду рад пулл реквестам — здесь.
Tags:
Hubs:
+7
Comments1

Articles