Log-Driven Development
Tags: javascript, react, testing, webdev
Published: 2021-02-02 12:54:04

If we compare the application with the alive organism the bug could be compared with a disease. The cause of this "disease" can be a number of factors, including the environment of a particular user. This is really relevant when we are talking about web platform. Sometimes the reason is very complicated and the bug that was found through testing - the result of a number of actions. 

As with human illnesses, no one can explain their symptoms better than a patient, any tester can tell what happened, better than the program itself.

## What to do?

To understand what is happening, we need to have a history of the actions that the user performed in our application.

In order for our program to tell us that it hurts, we will take the [logrock](https://github.com/AlexSergey/logrock) module and link it to ElasticSearch, LogStash, and Kibana for further analysis.

## LogRock
The [logrock](https://github.com/AlexSergey/logrock) module was born when we started working on the [Cleverbrush](https://www.cleverbrush.com/) product. This is software for working with vector graphics. Working with a graphics editor implies a huge number of application use cases. We are trying to save money and time, so we optimize everything, including testing. Covering each option with test cases is too expensive and irrational, especially since it is impossible to cover all options.

This module can organize modern logging approach for your application. Basing on the logs we test our application. In this article, I am going to tell you about how you can organize your logging system for searching bugs.

## ElasticStack
![ElasticStack](http://www.natrube.net/logrock/article_dev_to/img-1.png)

- **ElasticSearch** is a powerful full-text search engine.
- **LogStash** is a system for collecting logs from various sources that can send logs to ElasticSearch as well.
- **Kibana** is a web interface for ElasticSearch with many addons.

## How does it work?

![App logging system](http://www.natrube.net/logrock/article_dev_to/img-2.png)

In case of an error (or just on demand), the application sends logs to the server where they are saved to a file. Logstash incrementally saves data to ElasticSearch - to the database. The user logs to Kibana and sees the saved logs.

![Kibana](http://www.natrube.net/logrock/article_dev_to/img-3.png)

Above you see a well set up Kibana. It displays your data from ElasticSearch. That can help you to analyze your data and understand what happened.

In this article, I am **NOT** considering setup ElasticStack!

## Creating logging system

For example, we are going to integrate a logging system to single page application based on React.

### Step 1. Installation:

```sh
npm install logrock --save
```

### Step 2. Setup React Application

We need to wrap up the application with a component

```jsx
import { LoggerContainer } from "logrock";

<LoggerContainer>
  <App />
</LoggerContainer>
```

*LoggerContainer* is a component that reacts to errors in your application and forms a stack.

A stack is an object with information about the user's operating system, browser, which mouse or keyboard button was pressed, and of course, the actions subarray, where all the user actions that he performed in our system are recorded.

*LoggerContainer* has settings, consider some of them.

```jsx
<LoggerContainer
  active={true|false}
  limit={20}
  onError={stack => {
    sendToServer(stack);
  }}
>
  <App />
</LoggerContainer>
```
- **active** enables or disables the logging.
- **limit** sets a limit on the number of recent actions saved by the user. If the user performs 21 actions, then the first one in this array will be automatically deleted. Thus, we will have the last 20 actions that preceded the error.
- **onError** is a callback that is called when an error occurs. The Stack object comes to it, in which all information about the environment, user actions, etc. is stored. It is from this callback that we need to send this data to ElasticSearch or backend or save it to a file for further analysis and monitoring.

## Logging

In order to produce high-quality logging of user actions, we will have to cover our code with log calls.

The [logrock](https://github.com/AlexSergey/logrock) module comes with a logger that is linked to the *LoggerContainer*.

For instance, we have a component:

```jsx
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} />;
}
```

In order to correctly cover it with a log, we need to modify the toggle method:

```jsx
import React, { useState } from "react";
import logger from "logrock";

export default function Toggle(props) {
  const [toggleState, setToggleState] = useState("off");
  
  function toggle() {
    let state = toggleState === "off" ? "on" : "off";
    
    logger.info(`React.Toggle|Toggle component changed state ${state}`);
    
    setToggleState(state);
  }
  
  
  return <div className={`switch ${toggleState}`} onClick={toggle} />;
}
```

We have added a logger in which the information is divided into 2 parts. **React.Toggle** shows us that this action happened at the level of React, the Toggle component, and then we have a verbal explanation of the action and the current state that came to this component. This division into levels is not necessary, but with this approach, it will be clearer where exactly our code was executed.

We can also use the *"componentDidCatch"* method, which was introduced in React 16, in case an error occurs.

## Interaction with the server

Consider the following example.

Let's say we have a method that collects user data from the backend. The method is asynchronous, part of the logic is hidden in the backend. How to properly add logging to this code?

Firstly, since we have a client application, all requests going to the server will pass within one user session, without reloading the page. In order to associate actions on the client with actions on the server, we must create a global **SessionID** and add it to the header for each request to the server. On the server, we can use any logger that will cover our logic like the example from the frontend, and if an error occurs, send this data with the attached sessionID to ElasticSearch, to the Backend plate.

### Step 1. Generating **SessionID** on the client:

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

### Step 2. Requests.

We need to set the **SessionID** for all requests to the server. If we use libraries for requests, it is very easy to do this by declaring a SessionID for all requests.

```js
let fetch = axios.create({...});

fetch.defaults.headers.common.sessionId = window.SESSION_ID;
```

### Step 3. Connect **SessionID** to Log stack.

The *LoggerContainer* has a special field for **SessionID**:

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

### Step 4. Interaction with backend.

The request (on the client) will look like this:

```js
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}`);
  });
```

How it works:

We write a log, before the request on the client. From our code, we can see that the download of data from the server will start now. We have attached the **SessionID** to the request. If our backend logs are covered with the addition of this **SessionID** and the request fails, then we can see what happened on the backend.

Thus, we monitor the entire cycle of our application, not only on the client but also on the server.

## QA Engineer

Working with a QA engineer deserves a separate description of the process.

As we are startup, we have no formal requirements and sometimes not everything is logical.

If the tester does not understand the behavior, this is a case that at least needs to be considered. Also, often, a tester simply cannot repeat the same situation twice. Since the steps leading to the incorrect behavior can be numerous and non-trivial. In addition, not all errors lead to critical consequences such as Exception. Some of them can only change the behavior of the application, but not be interpreted by the system as an error. For these purposes, at staging, you can add a button in the application header to force the sending of logs. The tester sees that something is wrong, clicks on the button, and sends a *Stack* with actions to **ElasticSearch**.

![BSOD button](http://www.natrube.net/logrock/article_dev_to/img-4.png)

In case, a critical error has occurred, we must block the interface so that the tester does not click further and get stuck.

For these purposes, we display the blue screen of death.

![BSOD](http://www.natrube.net/logrock/article_dev_to/img-5.png)

We see above the text with the Stack of this critical error, and below - the actions that preceded it. We also get the error ID, the tester just needs to select it and attach it to the ticket. Later this error can be easily found in Kibana by this ID.

For these purposes, the *LoggerContainer* has properties:

```jsx
<LoggerContainer
  active={true | false}
  limit={20}
  bsodActive={true}
  bsod={BSOD}
  onError={stack => {
    sendToServer(stack);
  }}
>
  <App />
</LoggerContainer>
```
- **bsodActive** enables / disables BSOD (disabling BSOD applies to production code)
- **bsod** is React component. By default, it looks like the above screenshot.

To display the button in the UI LoggerContainer, we can use the hook:

```jsx
const { getStackData, triggerError } = useLoggerApi();

triggerError(getStackData());
```

## User interaction

Some logs are useful to the user. To output the user needs to use the stdout method:

```jsx
<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** is the method that is responsible for printing messages.

In order for the message to become *"important"* it is enough to pass true to the logger as the second parameter. Thus, we can display this message to the user in a pop-up window, for example, if the data loading has failed, we can display an error message.

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

### Tips and tricks

- Log applications, including in production, because no tester will find bottlenecks better than real users.

- **DO NOT** forget to mention the collection of logs in the license agreement.

- **DO NOT** log passwords, banking details, and other personal information!

- Redundancy of logs is also bad, make messages as clear as possible.

## Conclusion

When you release an app, life is just beginning for it. Be responsible for your product, get feedback, monitor logs, and improve it.