Skip to main content
Hiro League uses a single logging standard across all components: the hirocli server, channel plugin subprocesses, and the gateway. Every process writes its own rotating log file. An optional foreground mode aggregates all logs into one terminal view for development and debugging.

Design goals

GoalHow it is achieved
Single standardAll components call hiro_channel_sdk.log_setup.init() once at startup
Per-process filesEach component writes to its own <component>.log under its log directory
Automatic rotationRotatingFileHandler — 5 MB per file, 5 rotated backups kept
Consistent formatISO-8601 UTC timestamps on every line, same across all components
Configurable levelsRoot level and per-logger overrides stored in ~/.hirocli/config.json
Live aggregated viewhirocli start --foreground streams server + all plugin logs to the terminal

Log file locations

hirocli server and plugins

All hirocli components write to the same directory (default ~/.hirocli/logs/):
~/.hirocli/logs/
  server.log          ← hirocli core (channel_manager, comm_manager, agent_manager, …)
  plugin-devices.log  ← devices channel subprocess
  plugin-telegram.log ← telegram channel subprocess (if installed)
  plugin-echo.log     ← echo channel subprocess (if installed)
The directory is created automatically on first start.

Gateway

~/.hirogateway/logs/
  gateway.log

Log format

Every line follows this format:
2026-03-02T10:00:00.123Z [INFO    ] hirocli.agent_manager: Agent reply enqueued thread=devices:u1 content_length=42
2026-03-02T10:00:01.456Z [WARNING ] hiro_channel_sdk.transport: Disconnected from hirocli. Reconnecting in 5s…
2026-03-02T10:00:02.789Z [DEBUG   ] hirocli.comm_manager: Inbound [channel=telegram sender=123456 content_type=text]
FieldDescription
TimestampISO-8601 UTC with milliseconds, always Z suffix
LevelLeft-padded to 8 characters (INFO , WARNING , DEBUG , ERROR )
Logger namePython dotted module path — identifies the component and file
MessageStructured key=value pairs for machine-readability, human-readable prose

Configuration

Log directory

Add log_dir to ~/.hirocli/config.json to override the default:
{
  "log_dir": "/var/log/hirocli"
}
Leave it empty (or omit it) to use the default ~/.hirocli/logs/. For the gateway, pass --log-dir on the command line:
hirogateway --log-dir /var/log/hirogateway

Per-logger level overrides

Add log_levels to ~/.hirocli/config.json to set DEBUG on specific loggers without raising the root level for everything:
{
  "log_levels": {
    "hirocli.agent_manager": "DEBUG",
    "hiro_channel_devices.plugin": "DEBUG"
  }
}
Valid level strings: DEBUG, INFO, WARNING, ERROR, CRITICAL. Changes take effect on the next server restart.

Viewing logs

Live — foreground mode

Run the server in the foreground to see all logs in the terminal as they happen:
hirocli start --foreground
# or
hirocli start -f
In this mode:
  • The server process runs in the current terminal (Ctrl+C to stop).
  • Server logs stream directly to stdout.
  • Each plugin’s log file is tailed and its new lines are also printed to stdout.
  • All lines share the same format so they can be read together by timestamp.

File — detached mode

In normal detached mode (hirocli start), all output is written to log files only.
# Tail server log
Get-Content "$env:USERPROFILE\.hirocli\logs\server.log" -Wait -Tail 50

# Tail devices plugin log
Get-Content "$env:USERPROFILE\.hirocli\logs\plugin-devices.log" -Wait -Tail 50

Merging all log files

Because every line starts with an ISO-8601 UTC timestamp in the same format, any tool that can merge sorted text streams works:
# Merge and sort by timestamp
sort -m ~/.hirocli/logs/*.log | less

# Or with multitail
multitail ~/.hirocli/logs/server.log ~/.hirocli/logs/plugin-devices.log

The log_setup module

All log initialisation lives in hiro-channel-sdk at hiro_channel_sdk/log_setup.py so that every component — including third-party channel plugins — can use the same standard without depending on hirocli.

init()

Call this once, as the very first thing at process start, before any other logging calls.
from pathlib import Path
from hiro_channel_sdk import log_setup

log_setup.init(
    "plugin-mytelegram",          # → ~/.hirocli/logs/plugin-mytelegram.log
    Path.home() / ".hirocli" / "logs",
)
ParameterTypeDefaultDescription
componentstrrequiredLog-file stem, e.g. "server", "plugin-devices", "gateway"
log_dirPathrequiredDirectory for the rotating log file. Created if absent.
levelstr"INFO"Root logger level string
foregroundboolFalseIf True, also writes to stdout
log_levelsdict[str, str]NonePer-logger level overrides applied after the root level

Naming convention

Componentcomponent argumentLog file
hirocli server"server"server.log
Channel pluginf"plugin-{plugin.info.name}"plugin-<name>.log
Gateway"gateway"gateway.log

Writing a channel plugin that logs correctly

The plugin entry point receives --log-dir from ChannelManager automatically. Use it to initialise logging before the transport connects:
import asyncio
from pathlib import Path
import typer
from hiro_channel_sdk import PluginTransport, log_setup
from .plugin import MyChannel

_DEFAULT_LOG_DIR = str(Path.home() / ".hirocli" / "logs")

app = typer.Typer()

@app.command()
def run(
    hiro_ws: str = typer.Option("ws://127.0.0.1:18081", "--hiro-ws"),
    log_dir: str = typer.Option(_DEFAULT_LOG_DIR, "--log-dir"),
) -> None:
    plugin = MyChannel()
    log_setup.init(f"plugin-{plugin.info.name}", Path(log_dir))
    transport = PluginTransport(plugin, hiro_ws)
    asyncio.run(transport.run())
The plugin then uses the standard Python logging module throughout:
import logging
logger = logging.getLogger(__name__)

class MyChannel(ChannelPlugin):
    async def on_start(self) -> None:
        logger.info("MyChannel started.")

    async def on_stop(self) -> None:
        logger.info("MyChannel stopped.")
Do not call logging.basicConfig() in a plugin. The log_setup.init() call in the entry point configures the root logger for the whole process.

How ChannelManager passes --log-dir

ChannelManager._spawn_one appends the log directory to every plugin command automatically, using the same resolve_log_dir(config) value as the server itself:
hiro-channel-devices --hiro-ws ws://127.0.0.1:18081 --log-dir ~/.hirocli/logs
This means plugin log files always land alongside the server log unless the user has explicitly overridden log_dir in config.json.

File reference

FileRole
hiro-channel-sdk/src/hiro_channel_sdk/log_setup.pyShared init() function — the only place log format, rotation, and handler setup is defined
hirocli/src/hirocli/config.pyConfig.log_dir, Config.log_levels, resolve_log_dir()
hirocli/src/hirocli/_server_process.pyCalls log_setup.init("server", …) at the top of _main()
hirocli/src/hirocli/cli.pyhirocli start --foreground flag
hirocli/src/hirocli/channel_manager.pyAppends --log-dir to every plugin subprocess command
gateway/src/hirogateway/main.pyCalls log_setup.init("gateway", …, foreground=True)
channels/*/src/*/main.pyEach plugin entry point calls log_setup.init(f"plugin-{name}", log_dir)