Logging in Browser
Using a structured logger in your frontend project
You will not find a Node.js application that does not use some level of logging to communicate program progress. However, when we look at the frontend applications, we rarely see any logging. 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.)
I guess for the reasons above, it didn’t surprise me when another frontend developer asked me how are we going to log errors in our React project:
I’m wondering about the best practices of logging in a browser. 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.
A summary of our conversation is as follows:
- 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 implementconsole.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.
Given all of the above, what is the best way to log in a frontend application?
Writing your Logger
The first thing to know is that outside of ad-hoc logging for debugging purposes, 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);
};
You would pass-around and use MyLogger
function everywhere in your application. 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 log severity:
type LogLevelType =
'debug' |
'error' |
'info' |
'log' |
'trace' |
'warn';const MyLogger = (logLevel: LogLevelType, ...args) => {
console[logLevel](...args);
};
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 devtools and inject custom to log writer to access logs.
globalThis.myLoggerWriteLog = (logLevel, ...args) => {
console[logLevel](...args);
};
Earlier 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 makes it useable: smaller interface means consistent use of the API and enables smarter transformations, e.g. all my loggers require log level, a single text message, and a single, serializable object describing all supporting variables.
Using a Logging framework
Finally, before you head out to implement your own abstraction, I suggest to evaluate if Roarr logger (about which I have blogged earlier) meets your requirements. Roarr requires no initialisation, it works in Node.js and browser, it allows structured logs and it decouples transports.
Starting to use Roarr is simple and it comes with several nifty features, including a variation of those described in this article.
import log from 'roarr';log('Hello, I am your first structured log.');
Proceed to Roarr documentation to learn my motivation for creating Roarr and how to get started.