Log Management and Analytics

Explore the full capabilities of Log Management and Analytics powered by SolarWinds Loggly

View Product Info

FEATURES

Infrastructure Monitoring Powered by SolarWinds AppOptics

Instant visibility into servers, virtual hosts, and containerized environments

View Infrastructure Monitoring Info

Application Performance Monitoring Powered by SolarWinds AppOptics

Comprehensive, full-stack visibility, and troubleshooting

View Application Performance Monitoring Info

Digital Experience Monitoring Powered by SolarWinds Pingdom

Make your websites faster and more reliable with easy-to-use web performance and digital experience monitoring

View Digital Experience Monitoring Info

Blog JSON

Best Practices for Client-Side Logging and Error Handling in React

By Michael Auderer 11 Oct 2024

Logging is an essential part of development. While working on React projects, logging provides a way to get feedback and information about what’s happening within the running code. However, once an app or website is deployed into production, the default console provides no way to continue benefiting from logs.

Since these logs are client-side, all errors experienced by users will be lost in their own browsers. This means that the only way we can find out about errors in our application is by user reports, which often don’t happen; many users will simply leave rather than filling out a bug report. Therefore, good user experience relies on errors being reported without user intervention, which is a feature that the default console lacks. In this article, we will discuss ways to handle logging for capturing usage and error data in a React app.

Setting up custom logging in a new React project

Integrating logging into your React application is not simply good practice; it is essential for bug-free code and monitoring performance. However, instead of the excessive use of `console.log` calls within your code, a better error-handling technique is using custom logging. This method provides you with an efficient error-catching capability while keeping track of user interactions. However, despite its benefits, custom logging comes with its challenges and shortfalls. Some of these include the inability to ensure a consistent log format, log levels along components, and handle performance overhead.

Below is a detailed and clear step-by-step example guide on setting up logging in a new React project.

Step 1: Setting up the React.js environment

We will be setting up our React application using the Vite package.

Start by opening your terminal.

Next, go to your preferred directory in your terminal and type the following:

`$ npm create vite@latest`

The `create vite@latest` command will set up a development environment and direct you to a prompt on how to set up your application. Follow the prompt to create your React application.

Step 2: Creating custom logging utility

Create a file with the name `logger.jsx`. In this file, you will build the custom logger. This will then be imported into our components to catch and monitor errors and performance.

Next, we will create the logging component. Here is how you can do this:

`

// logger.js

const logger = {

 log: (message) => {

 if (process.env.NODE_ENV !== "production") {

 console.log(`[LOG] ${new Date().toISOString()}: ${message}`);

 }

 },

 error: (message) => {

 if (process.env.NODE_ENV !== "production") {

 console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);

 }

 },

 warn: (message) => {

 if (process.env.NODE_ENV !== "production") {

 console.warn(`[WARN] ${new Date().toISOString()}: ${message}`);

 }

 },

};

export default logger;

`

This code defines a logging utility with three methods: `log`, `error`, and `warning`. Each logging method accesses the `NODE_ENV` environment variable if it is not set for production. The method logs messages with timestamps if the condition is met and the application is not running in a production environment.

Step 3: Using custom logging in a component

After creating our logging utility, here is how we utilize our custom logging in a React component within our `Usage.jsx` file:

`

// Usage in a component

import React from "react";

import logger from "./logger";

function App() {

 logger.log("App component rendered");

 return (

 <div className="App">

  <h1> Passed Test!</h1>

 </div>

 );

}

export default App;

`

The code example above defines the `App` React component, which logs a message when it renders. At the top, we imported React and the custom utility from the logger component we had earlier created. The “App component rendered” log message will display within our `App` component if our application is not in production mode. The component will render and return the div with the `H1` element, and the text “Passed Test!” will appear.

Custom logging wrapper

The logger wrapper is a layer between your existing code and the underlying library. By creating a logger wrapper around the console object, you can extend logging functionality uniformly and make it reusable.

To do this, you need first to define the logger class.

Step 1: Define the logger class

In your `src` directory, create a new file named `logger.js` and define a logger class to wrap around the console method.

`

class Logger {

 constructor() {

 this.levels = ["debug", "info", "warn", "error"];

 this.currentLevel = "debug";

 }

 setLevel(level) {

 if (this.levels.includes(level)) {

 this.currentLevel = level;

 } else {

 console.error(`Invalid log level: ${level}`);

 }

 }

 log(level, message) {

 const levelIndex = this.levels.indexOf(level);

 const currentLevelIndex = this.levels.indexOf(this.currentLevel);

 if (levelIndex >= currentLevelIndex) {

 const timestamp = new Date().toISOString();

 console[level](`[${level.toUpperCase()}] ${timestamp}: ${message}`);

 }

 }

 debug(message) {

 this.log("debug", message);

 }

 info(message) {

 this.log("info", message);

 }

 warn(message) {

 this.log("warn", message);

 }

 error(message) {

 this.log("error", message);

 }

}

const logger = new Logger();

export default logger;

`

The code example above defines a logger with its different log levels, such as debug, info, error, and warning, and methods to set the current log level. Messages are logged to the console based on the current log level, including the timestamps. The class methods (debug, info, error, and warning) then streamline logging at respective levels. Finally, the logger instance is exported to be used in other components.

Step 2: Exporting the logger

After successfully setting up your logger wrapper, you can import and use the custom logger in your application. Below is an example of how to use the custom logger in your React application.

`

import React, { useEffect } from "react";

import logger from "./logger";

function App() {

 useEffect(() => {

 logger.debug("App component rendered");

 logger.info("Info level log");

 logger.warn("Warning level log");

 logger.error("Error level log");

 }, []);

 return (

 <div className="App">

 <h1>Hello, World!</h1>

 </div>

 );

}

export default App;

`

The code example above defines a React component named `App`, which utilizes a custom offer to log debug, info, warning, and error messages when the element is rendered. The `useEffect` hook triggers the log messages, ensuring they run once after the component is mounted.

Step 3: Setting the log level

You can change the log level dynamically based on your working environment.

`

import logger from "./logger";

if (process.env.NODE_ENV === "production") {

 logger.setLevel("warn");

} else {

 logger.setLevel("debug");

}

`

Sending JSON Logs with Loglevel

One way to improve our logging capabilities is by using a client-side logging library such as loglevel. Loglevel replaces the typical console.log with features for level-based logging and filtering, which provide a lot more control over your logs.

Furthermore, by using a plugin such as loglevel-plugin-remote, these advanced logs can be sent remotely to a server for storage, analysis, and alerts. By sending JSON data with this method, rather than plain text, we can maintain a data structure with our logs that can easily be sorted and organized.

Here’s a simple example of using loglevel with loglevel-plugin-remote to send logs containing JSON data including the message, level, and stacktrace:

import log from 'loglevel';
import remote from 'loglevel-plugin-remote';

const customJSON = log => ({
 msg: log.message,
 level: log.level.label,
 stacktrace: log.stacktrace
});

remote.apply(log, { format: customJSON, url: '/logger' });

log.enableAll();

log.info('Message one');
log.warn('Message two');

This example will POST all logged data to the “/logger” endpoint on your application URL, requiring you to set up a back-end server to handle this endpoint. There are many other configuration options available in loglevel-plugin-remote, such as intervals for POSTing and API tokens if applicable. The only downside of this method is that you must set up your server to receive, store, and organize these logs, which may require writing a lot of additional code on the back end. Alternatively, you can use a logging service to handle the back end for you.

Exploring logging frameworks

Using third-party logging frameworks, such as `Log4js` and `Winston`, is another efficient way of applying logging capability in production.

Logging frameworks are employed to facilitate logging in our React application. Since the frameworks are not built within React, you must integrate them into the application. As an upside, they come with structured logs, centralized management, a log rotation, and robust error-handling abilities. Each framework has unique features, strengths, and weaknesses that will favor your application structure. We will explore the `Log4js` and `Winstonjs` logging frameworks.

Log4js logging framework

Log4js is a JavaScript logging framework influenced by the Apache `log4js` library. It helps developers to manage output and format log messages. It also helps categorize the logs into various locations, files, consoles, and distant servers.

To integrate the `Log4js` framework into our React application, follow the steps below.

Step 1: Install and configure Log4js

Install Log4js.

`

npm install log4js

`

Create a configuration for the `Log4js` library. Name the file ` log4js-config.js` and place it in the root of your `src` directory.

`

// src/log4js-config.js

import log4js from "log4js";

log4js.configure({

 appenders: {

 console: { type: "console" },

 file: { type: "file", filename: "app.log" },

 },

 categories: {

 default: { appenders: ["console", "file"], level: "info" },

 },

});

const logger = log4js.getLogger();

export default logger;

`

You can then create the logger instance using ` log4js.getLogger()`. This is exported as a default module export, allowing the rest of the application to import the logger. This can now be used in other components within our application.

Step 2: Use the logger in your React components

After successfully configuring your `Log4js`, you can import the logger into your React components.

`

// src/App.js

import React from "react";

import logger from "./log4js-config";

function App() {

 logger.info("App component rendered");

 return (

 <div className="App">

  <h1>Hello, World!</h1>

 </div>

 );

}

export default App;

`

Here, we have imported a logger, logged an info message when the App component was rendered, and defined the app component to display “Hello, World!” in an h1 element. Finally, it exports the App component as the default export.

Winston logging framework

The Winston logging framework is a versatile logging library. It supports multiple transports, enabling one to quickly log in to places, such as remote servers, consoles, and files.

Step 1: Install and configure Winston

Install Winston.

`

npm install winston winston-browser

`

To configure the Winston library, create a logging configuration file, such as logger.js.

`

import { createLogger, format, transports } from "winston";

import { Console } from "winston/lib/winston/transports";

const { combine, timestamp, printf } = format;

const logFormat = printf(({ level, message, timestamp }) => {

 return `${timestamp} [${level}]: ${message}`;

});

const logger = createLogger({

 level: "info",

 format: combine(timestamp(), logFormat),

 transports: [new Console()],

});

export default logger;

`

Handling Errors in React with Error Boundaries

Error boundaries are components which catch errors in your JavaScript application to activate handler logic and render a fallback UI instead of crashing the entire component tree. These were introduced in React 16 and provide a way to handle errors much more effectively. Below is an example of a React error boundary implementing loglevel. When rendered, this component will catch all errors from its entire child tree and send logs to the /logger URL, which should be received by your server.

import React, { Component } from 'react';
import log from 'loglevel';
import remote from 'loglevel-plugin-remote';

const customJSON = log => ({
msg: log.message,
level: log.level.label,
stacktrace: log.stacktrace
});

remote.apply(log, { format: customJSON, url: '/logger' });
log.enableAll();

class ErrorBoundary extends Component {
 constructor(props) {
  super(props);
  this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
  // Update state so the next render will show the fallback UI.
  return { hasError: true };
}

componentDidCatch(error, info) {
  // log the error to our server with loglevel
  log.error({ error, info });
}

render() {
 if (this.state.hasError) {
  // You can render any custom fallback UI
  return <h1>Something went wrong.</h1>;
 }

 return this.props.children;
}
}

export default ErrorBoundary;

More Efficient Logging with Loggly

While the previous examples are great for rolling your own logging service, they require a lot of extra work on the back end to effectively use your logs. SolarWinds® Loggly® is a powerful platform for managing all of your across apps and users. Loggly provides tools for effectively sorting through logs, monitoring your apps with alerts, creating custom dashboards for your specific needs, and much more.

With Loggly, you can also easily combine logs from multiple points in your infrastructure. This unification is essential for managing apps at scale. This is useful for more than just error handling; with Loggly, it’s easy to capture usage data from your users for marketing, conversion tracking, and learning what your users like or dislike about your app. You can also track site traffic without ad blocker interference and respond to capacity problems as traffic scales.

Using Loggly in React

Loggly provides a simple SDK for interacting with their logging platform. Similar to loglevel, you need to drop in the new logger and start logging with it. Start by creating a separate file such as “logger.js” to initialize a new instance of the Loggly tracker. Then, use this logger in the error boundary we created earlier to start sending logs to Loggly. By keeping the instance in a separate file, it can easily be imported across your application. In this example, we have installed the SDK via npm instead of adding it as a script tag in the HTML, as the Loggly documentation shows. This allows us to benefit from the package management features of npm.

Another great alternative to the Loggly SDK is the SolarWinds® Observability system, a platform built to give you visibility into your cloud-native, on-premises, and hybrid custom and commercial applications.

Logger.js

import { LogglyTracker } from 'loggly-jslogger';

const logger = new LogglyTracker();

logger.push({ 'logglyKey': 'YOUR CUSTOMER TOKEN HERE' });

export default logger;

Error Boundary Component

import React, { Component } from 'react';
import logger from './logger';

class ErrorBoundary extends Component {
 constructor(props) {
  super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
  // Update state so the next render will show the fallback UI.
  return { hasError: true };
}

componentDidCatch(error, info) {
  // log the error to loggly
  logger.push({ error, info });
}

render() {
if (this.state.hasError) {
  // You can render any custom fallback UI
  return <h1>Something went wrong.</h1>;
 }

 return this.props.children;
 }
}

export default ErrorBoundary;

Example: Tracking Page View Time

Besides tracking errors with error boundaries, it’s also useful to know which pages are being viewed and interacted with in your application. Using the same logger.js import we created before, we can easily drop this into component lifecycle methods like componentDidMount and componentWillUnmount to see when components are being loaded and removed. This would handle routes, pop ups, and other components which a user brings into view, but for external links you’ll need to use onClick handlers.

For example, let’s assume we have a page listing service for our product, and we want to see how long users are spending on this page. By creating a simple time calculation on mount and logging it on componentWillUnmount, we can keep track of this information:

import React, { Component } from 'react';
import logger from './logger';

let startDate;

class ServicesPage extends Component {

 componentDidMount() {
   // initialize the start date on page load
   startDate = new Date();
}

 componentWillUnmount() {
   // calculate the time since we loaded this page
   const timeSinceLoad = (new Date().getTime() - startDate.getTime()) / 1000

   // log it to Loggly!
   logger.push({
   tag: 'pagetime', // add a custom tag for sorting
   pageName: 'Services' // the name of this page for sorting
   timeSpentInSec: timeSinceLoad
  });
}

render() {
  return …
 }

}
Page time

This chart shows average page view time of multiple pages on a website. After including the “pageName” field in the JSON log data, we set “Split by” to “json.pageName” to get a color-coded chart showing a comparison of all pages.

Conclusion

Logging is essential for building a successful application with a great user experience. In this article, we have discussed methods of improving the default console logger, sending logs to a server, building error boundaries in React, and integrating all of these concepts with Loggly for a robust logging solution. Once you’ve initialized Loggly’s simple SDK, you can use it across your React app to easily keep track of errors and user interaction. Sign up for a free 14-day trial of SolarWinds Loggly and start analyzing client-side logs and usage patterns in your application.


Last updated: October 2024
The Loggly and SolarWinds trademarks, service marks, and logos are the exclusive property of SolarWinds Worldwide, LLC or its affiliates. All other trademarks are the property of their respective owners.
Michael Auderer

Michael Auderer