I’ve written many log libraries since the 80s with messages going to devices ranging from an oscilloscope to a cell phone. My javascript logger library has evolved over 15+ years.  A simplified version is about 330 lines in 4 modules adding up to about 9KB.  It provides

These modules are in my examples repository 


The only thing required to create log messages is a Logger.  In an HTML page

    <script type="module"> 
        import { Logger } from "../common/log/logger.js";
        const log = new Logger("MyPage"); 
        log.debug(`this is a debug message `);

Or from another module

import { Logger } from “../common/log/logger.js”;

const log = new Logger(“MyModule”);

log.debug(`this is a debug message `);

The result is log messages are written to the console.  Not very useful since you could just as easily write

console.log(“this is a debug message”);

The first way to add value is with LogLevel

import { Logger, LogLevel } from”../common/log/logger.js”;

const log = new Logger(“MyModuleA”, LogLevel.INFO);

    log.debug(`this is a debug message`);

    log.info(`this is a info message`);

In this example, the debug message will not be written.  If you want DEBUG level messages from MyModuleA, just change the constructor parameter to LogLevel.DEBUG.  Or hide INFO messages with LogLevel.WARN.  You can also change the level at runtime


Every module can have its own Logger at different LogLevels.  You can even have multiple loggers in a modules or create a logger function.


I’ve seen many names for place where log messages go or the code that does the work:  appenders, sinks, outputs, writers.  I’ve used LogDesitination for decades and kept it as this library evolved.  So, my logging system needs one or more LogDestination created in order to do anything. 

By default, a ConsoleDestination is created the first time a log message is written.  It writes all log messages to the javascript console using a default format. 

If your web page contains an element with the ID “LOG-CONTAINER”, an additional default destination will be created which writes all log messages inside that container.

<div id=”LOG-CONTAINER”></div>

This provides an easy way to see messages without a javascript console and is especially useful for debugging phone or tablet applications.

If another destination is created before logging anything, the default will not be created.   For example, the HTML page could contain

<script type=”module”>

import { Logger, LogLevel } from”../common/log/logger.js”;

import {ConsoleDestination} from”../common/log/log-destination.js”;

const consoleDestination = new ConsoleDestination(LogLevel.WARN);



All log messages will use this ConsoleDestination which only writes messages at levels WARN and ERROR (and special level ALWAYS)

Or, you can create a DOMDestination and not have a ConsoleDestination.


import { Logger, LogLevel } from”../common/log/logger.js”;

import {ConsoleDestination, DOMDestination} from”../common/log/log-destination.js”;

const domDestination = new DOMDestination(document.getElementById(“dom-log-container”),LogLevel.DEBUG);


Creating DOMDestination directly instead of using the default allows you to specify the DOM container element.

You can also extend the base class LogDestination and write to an API or IndexDB, or any other location you desire.   The only required method is writeLine.

If you wanted to write all log messages to the console backward, you can do

class MyDestination extends LogDestination {

    writeLine(text, logMessage) {




new MyDestination();

Once it’s created, it will be used by all Logger messages.  That’s not very useful, but you can see how the same technique can be used to do more useful things like POST the message to the server or share it with customer support over a peer-to-peer chat.

The logMessage parameter of writeLine has all the details of the message created by the Logger





The “text” parameter is a formatted version of the message.  The default format looks like

02:54:53.387 | INFO | Application: this is a info message 1

Your writeLine() implementation can use the formatted version, or create it from the parts.


Every LogDestination has a LogFormatter.  If none is provided, the default is used which provides output like

03:07:13.322 | DEBUG | MouseMoveModule : mouseMove (12,801)

03:07:13.329 | DEBUG | MouseMoveModule : mouseMove (12,802)

03:07:13.339 | ERROR | MouseModule     : mouseOut (12,803)

03:07:13.342 | INFO  | MouseModule     : mouseLeave (12,803)

03:07:13.390 | ERROR | MouseModule     : mouseOver (17,798)

03:07:13.392 | INFO  | MouseModule     : mouseEnter (17,798)

The default formatter lets you set maximum module name and message lengths (and truncates if longer).

A simple formatter to just write the message text (no time, level, or module) can be created with

class PlainFormatter extends LogFormatter {

    constructor() {



    format(logMessage) {  return logMessage.getText();}


const consoleDestination = new ConsoleDestination(LogLevel.DEBUG,new PlainFormatter());

Or you may want to send a JSON message to the server with ISO time

class JSONFormatter extends LogFormatter {
    constructor() {

    combine(time,level,module,message) {
      const json = {time,level,module,message};
      return JSON.stringify(json,null,2);

    formatTime(time) {
      return new Date(time).toISOString();

const consoleDestination = new APIDestination(LogLevel.DEBUG,new JSONFormatter());


These simple modules provide most of the flexibility I’ve used in any language or framework.  My complete logging system has additional functionality for filtering, exceptions, message building, and other things.  Most of those are not needed often and can be built by extending the Logger, LogDestination, or LogFormatter base classes.