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
| Goal | How it is achieved |
|---|
| Single standard | All components call hiro_channel_sdk.log_setup.init() once at startup |
| Per-process files | Each component writes to its own <component>.log under its log directory |
| Automatic rotation | RotatingFileHandler — 5 MB per file, 5 rotated backups kept |
| Consistent format | ISO-8601 UTC timestamps on every line, same across all components |
| Configurable levels | Root level and per-logger overrides stored in ~/.hirocli/config.json |
| Live aggregated view | hirocli 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
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]
| Field | Description |
|---|
| Timestamp | ISO-8601 UTC with milliseconds, always Z suffix |
| Level | Left-padded to 8 characters (INFO , WARNING , DEBUG , ERROR ) |
| Logger name | Python dotted module path — identifies the component and file |
| Message | Structured 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",
)
| Parameter | Type | Default | Description |
|---|
component | str | required | Log-file stem, e.g. "server", "plugin-devices", "gateway" |
log_dir | Path | required | Directory for the rotating log file. Created if absent. |
level | str | "INFO" | Root logger level string |
foreground | bool | False | If True, also writes to stdout |
log_levels | dict[str, str] | None | Per-logger level overrides applied after the root level |
Naming convention
| Component | component argument | Log file |
|---|
| hirocli server | "server" | server.log |
| Channel plugin | f"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
| File | Role |
|---|
hiro-channel-sdk/src/hiro_channel_sdk/log_setup.py | Shared init() function — the only place log format, rotation, and handler setup is defined |
hirocli/src/hirocli/config.py | Config.log_dir, Config.log_levels, resolve_log_dir() |
hirocli/src/hirocli/_server_process.py | Calls log_setup.init("server", …) at the top of _main() |
hirocli/src/hirocli/cli.py | hirocli start --foreground flag |
hirocli/src/hirocli/channel_manager.py | Appends --log-dir to every plugin subprocess command |
gateway/src/hirogateway/main.py | Calls log_setup.init("gateway", …, foreground=True) |
channels/*/src/*/main.py | Each plugin entry point calls log_setup.init(f"plugin-{name}", log_dir) |