OpenClaw Swarm

Logging

Structured logging for the main process

Overview

The main process uses a transport-agnostic logging system defined in electron/logger/. All code in electron/ uses the injected Logger interface — direct console.* calls are banned by ESLint.

The logger is designed to be portable. When the backend is extracted into a standalone package, the Logger interface and LoggerImpl class travel with it. Only the transports need to change per environment.

Logger interface

interface Logger {
  debug(msg: string, data?: unknown): void
  info(msg: string, data?: unknown): void
  warn(msg: string, data?: unknown): void
  error(msg: string, data?: unknown): void
  child(namespace: string): Logger
}

Child loggers inherit all transports and join namespaces with : separators (e.g. gw:conn).

Using the logger

Classes receive the logger via constructor injection:

class GatewayManager {
  private readonly logger: Logger

  constructor(logger: Logger) {
    this.logger = logger
    this.logger.info('manager started')
  }

  private initConnection(gw: StoredGateway) {
    const conn = new GatewayConnection({
      // ...
      logger: this.logger.child('conn'),
    })
  }
}

For pure-function modules that cannot accept constructor injection, use a module-level setter:

// protocol.ts
let _logger: Logger | null = null
export function setProtocolLogger(logger: Logger): void {
  _logger = logger
}

// In a function:
_logger?.warn('validation failed', details)

The logger is also available in oRPC procedures via context.logger:

export const exampleRouter = {
  doSomething: p.handler(({ context }) => {
    context.logger.info('handling request')
  }),
}

Log levels

LevelUse for
debugVerbose trace info (WS frames, state transitions). Only written in dev.
infoNormal operational events (connected, restored gateways).
warnRecoverable issues (validation fallbacks, unexpected frame types).
errorFailures (connection errors, timeouts, handler errors).

In production builds, minLevel is set to info — debug entries are dropped before reaching any transport.

Transports

Three transports are active simultaneously:

TransportWhen activeOutput
ConsoleTransportDev only (VITE_DEV_SERVER_URL set)Colored ANSI output to terminal
FileTransportAlwaysNDJSON to rotating log files
MemoryTransportAlwaysRing buffer (1000 entries) for in-app viewer

File transport

Writes one JSON object per line to app.getPath('logs'):

  • macOS: ~/Library/Logs/<appName>/swarm.log
  • Windows: %APPDATA%/<appName>/logs/swarm.log
  • Linux: ~/.config/<appName>/logs/swarm.log

Rotation: max 5 MB per file, 5 files total (25 MB cap). Files are named swarm.log, swarm.1.log, ..., swarm.4.log.

Each line is a JSON object:

{"ts":1740230400000,"level":"info","ns":"gw:conn","msg":"handshake SUCCESS"}

Memory transport

A fixed-capacity ring buffer supporting cursor-based tailing. The renderer polls logs.tail with afterCursor from the previous response to get only new entries since the last poll.

oRPC procedures

The logs router exposes three procedures:

ProcedureDescription
logs.tailRead recent entries from the memory ring buffer. Supports limit, afterCursor, level, and ns filters.
logs.exportOpens a native save dialog and copies the current log file to the chosen location.
logs.logPathReturns the absolute path of the current log file.

Initialization

The root logger is created in main.ts during app.whenReady():

const logger = createRootLogger({
  logDir: app.getPath('logs'),
  isDev: !!VITE_DEV_SERVER_URL,
})

setProtocolLogger(logger.child('gw:protocol'))
gatewayManager = new GatewayManager(logger.child('gw'))

Namespace conventions

NamespaceSource
mainRoot logger (main.ts, oRPC errors)
gwGatewayManager
gw:connGatewayConnection instances
gw:protocolWS frame encoding/decoding

On this page