Electron
lograil is built for Electron's two-process model and works out of the box: just import the default logger and it behaves correctly in both the main and renderer processes. You usually don't need createLogger or any runtime configuration.
See also: Web Runtime · Node Runtime — the three runtimes share the same API surface; this page covers Electron-specific behaviors.
Zero-config (recommended)
Main process
// main.ts
import { app, BrowserWindow } from 'electron';
import { logger } from 'lograil';
app.whenReady().then(() => {
const win = new BrowserWindow({ /* ... */ });
win.loadFile('index.html');
logger.info('app ready'); // → console + daily rotating file
});That's all. The main process logs to the console and a daily rotating file under the app's logs directory (app.getPath('logs'), namespaced by the app name). Renderer logs arrive over IPC and are written to a separate daily rotating file so the two processes never mix:
main.{YYYY-MM-DD}.log— entries from the main processrenderer.{YYYY-MM-DD}.log— entries from renderer processes
The log files use appName main / renderer; they live under the app's logs directory so they stay isolated per installed app.
Renderer process
// renderer.ts
import { logger } from 'lograil';
logger.warn('something happened in the UI');
// → local console, AND forwarded to the main process over IPCThe default renderer logger writes to the local console (visible in DevTools) and forwards entries to the main process, where they land in a dedicatedrenderer.{YYYY-MM-DD}.log file — separate from the main process's main.{YYYY-MM-DD}.log.
No setup, no createLogger, no wiring required.
⚠️ Requirement: the renderer must be able to reach Electron
For renderer → main forwarding to work, the renderer process must be able to access the electron module (require('electron')).
Modern Electron ships with nodeIntegration disabled and contextIsolation enabled by default, so the renderer's main world has no require and no Node globals (including process). In that case lograil can't detect Electron in the renderer and silently falls back to the Web runtime — logs print to the console only and never reach the main process.
Enable Node in the renderer (simplest):
new BrowserWindow({
webPreferences: {
nodeIntegration: true,
// contextIsolation: false, // required alongside nodeIntegration in some setups
},
});Security note:
nodeIntegration: truereduces isolation. For production, prefer apreloadscript that exposes onlyipcRendererand a custom bridge, or enable Node integration only for trusted content. The library degrades gracefully — without access toelectronit just logs locally and drops the IPC send.
Secure setup (preload + contextIsolation)
With the recommended Electron defaults (nodeIntegration: false, contextIsolation: true), the renderer's main world has no require and no process, so the built-in ElectronIpcTransport (which calls require('electron')) cannot reach ipcRenderer. The fix is a preload that exposes a thin send function through contextBridge, then a small custom transport that uses it. No library changes are needed — only the public API (addTransport + LOGRAIL_CHANNEL).
preload.ts (runs in the privileged context, has require):
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronLogger', {
send: (channel: string, data: unknown) => ipcRenderer.send(channel, data),
});Point the window at the preload:
import { app, BrowserWindow, path } from 'electron';
app.whenReady().then(() => {
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js'),
},
});
});main.ts — the default logger already listens for renderer entries via createElectronMainRuntime({ receiveFromRenderer: true }) (the default). No manual registerIpcReceiver call is needed unless you want to override the IPC channel:
// Only needed if you change the renderer's channel:
import { createLogger, createElectronMainRuntime, registerIpcReceiver } from 'lograil';
const log = createLogger({
runtime: createElectronMainRuntime({ receiveFromRenderer: false }),
});
registerIpcReceiver((entry) => log.ingestEntry(entry), { channel: 'my-app:log' });renderer.ts — build the renderer logger with the preload bridge injected as the ipcRenderer. This is the first-class way; no manual transport needed:
import { createLogger, createElectronRendererRuntime } from 'lograil';
declare global {
interface Window {
electronLogger?: { send: (channel: string, data: unknown) => void };
}
}
// The bridge only exists if the preload ran; fall back to a plain (console-only)
// logger when it is absent.
const log = window.electronLogger
? createLogger({
runtime: createElectronRendererRuntime({ ipcRenderer: window.electronLogger }),
})
: createLogger();
log.warn('UI event'); // → console + forwarded to main over the preload bridgeIf you would rather keep the default logger singleton, you can instead register the bridge as a transport:
import { logger, LOGRAIL_CHANNEL, type Transport } from 'lograil';
const bridge = window.electronLogger;
if (bridge) {
const ipcTransport: Transport = {
name: 'ipc-preload',
write(entry) {
bridge.send(LOGRAIL_CHANNEL, entry);
},
};
logger.addTransport(ipcTransport);
}Both approaches preserve a console log in the renderer (visible in DevTools) while satisfying Electron's secure defaults.
Customizing (advanced)
Only reach for createLogger + an explicit runtime when you want to change the log path, disable the file, or stop receiving renderer logs.
// main.ts — disable file or stop receiving renderer logs
import { createLogger, createElectronMainRuntime } from 'lograil';
const log = createLogger({
runtime: createElectronMainRuntime({
// log files use appName main/renderer; default dir: app.getPath('logs')
// (override via fileTransportOptions: { dir })
receiveFromRenderer: true, // default — receive renderer logs over IPC
}),
});// renderer.ts — keep logs local only, or change the channel
import { createLogger, createElectronRendererRuntime } from 'lograil';
const log = createLogger({
runtime: createElectronRendererRuntime({
forwardToMain: true, // default — forward to main over IPC
// channel: 'my-app:log',
}),
});| Factory | Option | Effect |
|---|---|---|
createElectronMainRuntime | fileTransportOptions | Forwarded to FileTransport (mode rotate-time) |
createElectronMainRuntime | disableFile | Console only (no file) |
createElectronMainRuntime | receiveFromRenderer | Receive renderer logs over IPC (default true) |
createElectronRendererRuntime | forwardToMain | Forward to main over IPC (default true) |
createElectronRendererRuntime | channel | Override the IPC channel |
The IPC channel
Both sides communicate on one channel, exported as a constant:
import { LOGRAIL_CHANNEL } from 'lograil'; // 'lograil:log'If you override channel, set the same value on both the renderer runtime and the main receiver. For full control, skip the runtime helpers and call registerIpcReceiver yourself:
import { registerIpcReceiver, createElectronMainRuntime } from 'lograil';
const log = createLogger({ runtime: createElectronMainRuntime({ receiveFromRenderer: false }) });
const off = registerIpcReceiver((entry) => log.ingestEntry(entry), { channel: 'my-app:log' });
// off() stops listeningEnd-to-end recap
renderer ──ElectronIpcTransport──▶ IPC ──▶ main: registerIpcReceiver
│
▼
log.ingestEntry(entry)
│
▼
main: ConsoleTransport + FileTransport- Renderer logs never touch disk directly — the main process persists them.
- Main-process logs and ingested renderer logs share one pipeline, one set of transports, and one rotating file.
- Level filtering and plugins apply uniformly to both.
Cross-process level sync
The renderer → main direction is automatic: when a renderer calls setLevel(), the command is forwarded to the main process over the IPC channel and applied to the main logger's level — no extra wiring required. The main → renderer direction is manual: setLevel() on the main process does not broadcast (each renderer owns its own local console sink and level), but logger.broadcastLevelToPeers() will push the main process's current level to every connected renderer.
// main.ts — auto direction (renderer → main)
import { logger } from 'lograil';
// a renderer's setLevel() is applied here automatically.
// main.ts — manual direction (main → renderer)
logger.broadcastLevelToPeers(); // push this process's level to every renderer// renderer.ts — auto direction (renderer → main)
import { logger } from 'lograil';
logger.setLevel('trace'); // main process automatically receives thisWhen a peer sends a level command, getLevel() on the receiving side returns the peer's level — so cross-process queries stay consistent.
This works for both the default logger singleton and loggers created with createLogger(). If you need to intercept or reject level commands before they are applied, register a custom callback:
import { createLogger } from 'lograil';
const log = createLogger();
log.setOnLevelCommand((level) => {
// inspect or reject before applying
if (level < 30) return; // only allow info and above
log.setLevel(level);
});