Logging in Browser

Gajus Kuizinas - Sep 7 '19 - - Dev Community

All Node.js applications use some level of logging to communicate program progress. However, we rarely see any logging in frontend code. This is primarily because:

  • Frontend developers already get a lot of feedback through the UI.
  • console object has a bad history of cross-browser compatibility (e.g. in IE8 console object was only available when the DevTools panel was open. Needless to say – this caused a lot of confusion.)

Therefore, it didn’t surprise me when a frontend developer asked me how are we going to log errors in our React project:

Should logs be freely used everywhere and leave it up to the bundler to handle removal of those? To reduce the size footprint perhaps? I read that some older browsers do not have console defined. So it’s advisable to remove them or handle its presence.

Writing a Logger

The first thing to know is that you mustn’t use console.log directly. Lack of a console standard aside (there is a living draft), using console.log restricts you from pre-processing and aggregating logs, i.e. everything that you log goes straight to console.log.

You want to have control over what gets logged and when it gets logged because once the logs are in your browser’s devtools, your capability to filter and format logs is limited to the toolset provided by the browser. Furthermore, logging does come at a performance cost. In short, you need an abstraction that enables you to establish conventions and control logs. That abstraction can be as simple as:

const MyLogger = (...args) => {
  console.log(...args);
};

Enter fullscreen mode Exit fullscreen mode

You would pass-around and use MyLogger function everywhere in your application.

Enforcing what gets logged

Having this abstraction already allows you to control exactly what/ when gets logged, e.g. you may want to enforce that all log messages must describe namespace and log severity:

type LogLevelType =
  'debug' |
  'error' |
  'info' |
  'log' |
  'trace' |
  'warn';

const MyLogger = (namespace: string, logLevel: LogLevelType, ...args) => {
  console[logLevel](namespace + ':', ...args);
};

Enter fullscreen mode Exit fullscreen mode

Our application is built using many modules. I use namespace to identify which module is producing logs, as well as to separate different domain logs (e.g. "authentication", "graphql", "routing"). Meanwhile, log level allows to toggle log visibility in devtools.

Toggling log visibility

Filtering logs using JavaScript function

You may even opt-in to disable all logs by default and print them only when a specific global function is present, e.g.

type LogLevelType =
  'debug' |
  'error' |
  'info' |
  'log' |
  'trace' |
  'warn';

const Logger = (logLevel: LogLevelType, ...args) => {
  if (globalThis.myLoggerWriteLog) {
    globalThis.myLoggerWriteLog(logLevel, ...args);
  }
};

Enter fullscreen mode Exit fullscreen mode

The advantage of this pattern is that nothing gets written by default to console (no performance cost; no unnecessary noise), but you can inject custom logic for filtering/ printing logs at a runtime, i.e., you can access your minimized production site, open devtools and inject custom to log writer to access logs.

globalThis.myLoggerWriteLog = (logLevel, ...args) => {
  console[logLevel](...args);
};

Enter fullscreen mode Exit fullscreen mode

Sum up

If these 3 features are implemented (enforcing logging namespace, log level and functional filtering of logs) then you are already up to a good start.

  • Log statements are not going to measurably affect the bundle size.
  • It is true that console object has not been standardised to this day. However, all current JavaScript environments implement console.log. console.log is enough for all in-browser logging.
  • We must log all events that describe important application state changes, e.g. API error.
  • Log volume is irrelevant*.
  • Logs must be namespaced and have an assigned severity level (e.g. trace, debug, info, warn, error, fatal).
  • Logs must be serializable.
  • Logs must be available in production.

I mentioned that log volume is irrelevant (with an asterisk). How much you log is indeed irrelevant (calling a mock function does not have a measurable cost). However, how much gets printed and stored has a very real performance cost and processing/ storage cost. This is true for frontend and for backend programs. Having such an abstraction enables you to selectively filter, buffer and record a relevant subset of logs.

At the end of the day, however you implement your logger, having some abstraction is going to be better than using console.log directly. My advice is to restrict Logger interface to as little as what makes it useable: smaller interface means consistent use of the API and enables smarter transformations, e.g. all my loggers (implemented using Roarr) require log level, a single text message, and a single, serializable object describing all supporting variables.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .