megahertz / electron-log

Simple logging module Electron/Node.js/NW.js application. No dependencies. No complicated configuration.
MIT License
1.32k stars 128 forks source link

Add a more verbose example of customizing log output #365

Closed MikeRatcliffe closed 1 month ago

MikeRatcliffe commented 1 year ago

I've been playing around with 5.0.0-beta.25 and found very little about customizing the output in this version. After going through the source, I have created a more verbose example to help others.

The only strange thing is that your toJSON() method in electron-log/src/main/transforms/object.js returns an object, rather than JSON. It removes cyclic references so that JSON.stringify() can safely handle them, but the method name should probably be changed.

Renderer

This is my logger.js file. It initializes the log and keeps the format simple. Most importantly with this setup, the output from console.log(), console.error() etc. in the renderer process appear in both DevTools and the terminal output:

import log from 'electron-log/renderer';

log.transports.console.format = '{text}';

// Echo console.log() etc. to the terminal using electron logger.
Object.assign(console, log.functions);

export { log };

Main Process

The log customization in the main process is more verbose, but simple to customize.

global.logHome and global.logLevel need to be set first:

const chalk = require('chalk');
const log = require('electron-log/main');
const { maxDepth, toJSON } = require('electron-log/src/main/transforms/object');

async function initLogs() {
  const color = {
    error: chalk.bgRed.white.bold,
    warn: chalk.yellow.bold,
    info: chalk.blue.bold,
    verbose: (t) => t,
    debug: (t) => t,
    silly: (t) => t,
  };

  log.transports.file.resolvePathFn = () => path.join(global.logHome, 'debug_last.log');
  log.transports.file.level = global.logLevel;

  // Change the console output to just the text we create in our hook.
  log.transports.console.format = '{text}';

  log.hooks.push((message, transport) => {
    let text = null;

    if (transport !== log.transports.console) {
      return message;
    }

    // Clone message and data because they are shared by the different
    // transports.
    const newMessage = Object.assign({}, message);
    const { data, date, level } = newMessage;
    const dataClone = [...data];

    if (typeof dataClone[0] === 'string') {
      text = dataClone[0];
    } else {
      // Deal with objects, arrays etc.
      // Step 1: Ensure the object is not deeper the 6 levels.
      let safeObj = maxDepth({ data: dataClone[0] });

      // Step 2: This 'toJSON' method actually removes cyclic references so that
      // JSON.stringify() can safely handle them.
      safeObj = toJSON({ data: safeObj });

      // Step 3: JSON.stringify() the safe object
      text = JSON.stringify(toJSON({ data: safeObj }));
    }

    // Personal tweak to highlight messages starting with 'XXXXX'
    if (text.startsWith('XXXXX')) {
      text = chalk.bold(text);
    }

    // Build strings ready for output
    const colorize = color[level];
    const lvl = ('[' + level + ']').padStart(9, ' ');
    const formattedTime = date.toTimeString().substring(0, 8);

    // Tag entries with their process type:
    //   - M: main
    //   - R: renderer
    const processType = newMessage.variables.processType === 'main' ? 'M' : 'R';

    // Colorize the beginning of the output
    const prefix = colorize(`${formattedTime} ${processType} ${lvl}`);

    // Add the final string back to the clone of the data array and save it to
    // newMessage.data
    dataClone[0] = `${prefix} ${text}`;
    newMessage.data = dataClone;

    // Return the newly constructed message
    return newMessage;
  });

  log.initialize({ preload: true });

  Object.assign(console, log.functions);
}
megahertz commented 1 year ago

I agree, it makes sense to make an example of output processing. In v5 a new property transport.transforms was introduced for that. I have added it to type definitions. But I want to play a bit more with it and add the corresponding documentation when I'm sure the public interface is good enough.

As for toJSON function, it just follows the standard convention when it returns a JSON representation of some object, it doesn't have to be a string.

frankz61 commented 1 year ago

It really helped me. Sometimes, it may be necessary to add chalk.level = 3 to make colorization work fine.