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.
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 functionAfter 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
BaiPing Technology
Official account of the BaiPing app technology team. Dedicated to enhancing human productivity through technology. | DRINK FOR FUN!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
