How to Sync Log4js Logs Across PM2 Processes with pm2-intercom-log4js

This article explains how to create the open‑source pm2-intercom-log4js tool to reliably synchronize log4js output in PM2’s multi‑process mode, covering log4js’s sync mechanism, PM2 process management, IPC communication, and alternative messaging approaches with practical code examples.

BaiPing Technology
BaiPing Technology
BaiPing Technology
How to Sync Log4js Logs Across PM2 Processes with pm2-intercom-log4js

This article describes how we built the open‑source tool pm2-intercom-log4js to handle log4js log synchronization in PM2’s multi‑process mode, and what you can gain from it:

A log4js log synchronization handling tool – pm2-intercom-log4js;

Understanding of log4js log synchronization principles;

Basic knowledge of PM2 process management;

Insights into the development history of the log synchronization tool;

Some process communication methods.

Preface

We are preparing to open‑source a tool that solves log4js log synchronization issues when running under PM2’s multi‑process mode. By simply invoking the default function exported by pm2-intercom-log4js before any logs are printed, you can prevent log loss and file‑write conflicts across processes.

const pm2Intercom = require('@takin/pm2-intercom-log4js');

async function init() {
  await pm2Intercom();
  log4js.configure({
    // Make sure logs4js has PM2 mode enabled.
    pm2: true,
  });
  log4js.getLogger().info('Hello, Billion Bottle!');
}

log4js is a Node.js logging library supporting console, file, email, etc. We need a synchronization tool because multiple PM2 workers write to the same file, which can cause loss. Although log4js officially supports PM2 mode, it requires the pm2 install pm2-intercom plugin, which often fails, so we built our own solution.

log4js’s Log Synchronization Principle

Why does log loss occur without a plugin when pm2: true is enabled, and how does the plugin prevent concurrent file writes?

const receiver = (worker, message) => {
  if (message && message.topic && message.topic === "log4js:message") {
    sendToListeners(logEvent);
  }
};
configuration.addListener(config => {
  if (isPM2Master()) {
    process.on("message", receiver);
  }
});
module.exports = {
  send: msg => {
    if (isMaster()) {
      sendToListeners(msg);
    } else {
      process.send({ topic: "log4js:message", data: msg.serialise() });
    }
  },
};

The code shows that the master process listens for message events and forwards log events to its listeners, while workers send logs via process.send, which the master then processes.

Exploring process.send and process.on('message')

We experimented with direct IPC in a single Node process, which fails because process.send is undefined without an IPC channel. Using the cluster module, we created a master‑worker setup where the master listens on the worker’s message event and the worker sends messages via process.send.

process.on('message', (message) => {
  console.log(message);
});
process.send('Hello, Billion Bottle!');
// TypeError: process.send is not a function

After consulting the Node.js documentation, we learned that process.send only exists when the process is spawned with an IPC channel.

If a Node.js process is spawned with an IPC channel, process.send() can be used to send messages to the parent. Otherwise, process.send is undefined.

Using cluster, we successfully achieved bidirectional communication:

// Worker sends, master receives
if (cluster.isMaster) {
  const worker = cluster.fork();
  process.on('message', (msg) => console.log(msg));
} else if (cluster.isWorker) {
  process.send('Hello, Billion Bottle!');
}

PM2 Process Management

log4js determines the master process by checking process.env[pm2InstanceVar] === "0". PM2 creates processes by incrementing an instance index, so the process with index 0 is treated as the master.

const isPM2Master = () => pm2 && process.env[pm2InstanceVar] === "0";
const isMaster = () => disabled || (cluster && cluster.isMaster) || isPM2Master();

PM2’s internal God.injectVariables assigns an instance number, and God.nodeApp forks the worker using cluster.fork, storing the child in a map keyed by its PM2 ID.

God.injectVariables = function injectVariables(env, cb) {
  var instanceKey = process.env.PM2_PROCESS_INSTANCE_VAR || env.instance_var;
  var instanceNumber = typeof instances[0] === 'undefined' ? 0 : instances[0] + 1;
  env[instanceKey] = instanceNumber;
  return cb(null, env);
};

Design of the Log Synchronization Tool

The tool follows these steps:

Listen to log messages from all non‑zero workers.

In the zero‑index master, listen to other workers’ log messages.

Use the master’s PM2 instance API to send its own logs via process.on('message').

Ensure the master is ready before workers start logging.

PM2 API Usage

We use PM2’s bus to receive messages from any process and sendDataToProcessId to forward messages to a specific worker.

process.on('message', (raw) => {
  console.log('Cluster message received from worker:', JSON.stringify(raw));
});
(async () => {
  const connect = promisify(pm2.connect.bind(pm2));
  const launchBus = promisify(pm2.launchBus.bind(pm2));
  await connect();
  const bus = await launchBus();
  bus.on('process:msg', (packet) => {
    const { raw, process: { pm_id: processId } } = packet;
    pm2.sendDataToProcessId(processId, raw, () => {});
  });
  process.send({ topic: 'test', data: 'Hello, Billion Bottle!' });
})();

Although the snippet throws an error when run directly, it works correctly when started with pm2 start xxx.js.

Other Communication Methods

Beyond Node’s native IPC, we can use child_process streams, sockets, message queues, or Redis Pub/Sub. Simple examples of child_process stdin/stdout and socket communication with the axon library are provided.

// main.js using child_process
const childProcess = require('child_process');
const worker = childProcess.spawn('node', ['./worker.js']);
worker.stdout.setEncoding('utf8');
worker.stdin.write('Hello, worker!');
worker.stdout.on('data', (message) => console.log(message));
// worker.js
process.stdin.setEncoding('utf8');
process.stdin.on('data', (message) => {
  console.log(message);
  process.stdout.write('Hello, main!');
});
// emitter.js using axon
const axon = require('axon');
const sock = axon.socket('push');
sock.bind(1901);
setInterval(() => sock.send('Hello, Billion Bottle!'), 1000);
// receiver.js using axon
const axon = require('axon');
const sock = axon.socket('pull');
sock.connect(1901);
sock.on('message', (msg) => console.log(msg));

Conclusion

If you encounter failures installing pm2 install pm2-intercom and cannot use log4js’s PM2 mode, consider using our pm2-intercom-log4js tool, which we will continue to iterate based on user feedback.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendNode.jsloggingpm2Process Communicationlog4js
BaiPing Technology
Written by

BaiPing Technology

Official account of the BaiPing app technology team. Dedicated to enhancing human productivity through technology. | DRINK FOR FUN!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.