Logging in a Browser - Genics Blog

Full width home advertisement

Funny Tricks

Technology on the go!

Post Page Advertisement [Top]

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

  • Frontend developers already get a lot of feedback through the UI.
  • The `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.)
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 dev tools, 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);
};
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);
};
Our application is built using many modules. I use a namespace to identify which module is producing logs, as well as to separate different domain logs (e.g. "authentication", "graphql", "routing"). Meanwhile, the log level allows toggling log visibility in dev tools.

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);
  }
};
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 dev tools and inject custom to log writer to access logs.
globalThis.myLoggerWriteLog = (logLevel, ...args) => {
  console[logLevel](...args);
};
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.
  • Indeed, the console object has not been standardized 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.

No comments:

Post a Comment

Please be polite to others while commenting. Spam content may be removed by the authors. Please be authentic in your reviews and opinion.

Bottom Ad [Post Page]