Skip to main content

Logging

HomeCore uses tracing for structured logging. Three independent outputs can run simultaneously, each with its own format and level filter.

Outputs at a glance

OutputDefaultBest for
stderrenabled, pretty formatDevelopment, systemd journal
filedisabledProduction, post-mortem analysis, log aggregators
syslogdisabledCentralised aggregation (Graylog, Loki, rsyslog)

Quick level override

The fastest way to change log verbosity is the RUST_LOG environment variable. It always takes highest precedence over config:

# Default — info from all crates
cargo run -p homecore

# Debug from the rule engine only
RUST_LOG=info,hc_core=debug cargo run -p homecore

# Debug from rule engine + MQTT client
RUST_LOG=info,hc_core=debug,hc_mqtt_client=debug cargo run -p homecore

# Deep dive — rule evaluation loop + action executor
RUST_LOG=info,hc_core::engine=debug,hc_core::executor=debug cargo run -p homecore

# Everything (very noisy — broker internals included)
RUST_LOG=trace cargo run -p homecore

# Silence all but errors
RUST_LOG=error cargo run -p homecore

Configuration reference

All settings live under [logging] in config/homecore.toml.

[logging] — global defaults

[logging]
# Global default level for all crates
# error | warn | info | debug | trace
level = "info"

# Timestamp timezone
# "local" — local system time with UTC offset (default)
# "utc" — UTC with Z suffix
time_display = "local"

[logging.targets] — per-crate overrides

[logging.targets]
hc_core = "debug" # rule engine, scheduler, state bridge, action executor
hc_api = "info" # HTTP/WebSocket handlers
hc_auth = "warn" # JWT, password hashing
hc_state = "info" # device registry, history
hc_mqtt_client = "debug" # MQTT connection, topic routing
hc_broker = "warn" # embedded broker (noisy at debug)
hc_topic_map = "debug" # ecosystem profile matching
hc_notify = "info" # notification channels
hc_scripting = "warn" # Rhai script execution

Sub-module granularity:

[logging.targets]
"hc_core::engine" = "debug" # rule evaluation loop only
"hc_core::executor" = "debug" # action execution only
"hc_core::bridge" = "debug" # MQTT→EventBus state bridge only

[logging.stderr] — console output

[logging.stderr]
enabled = true
format = "pretty" # "pretty" | "compact" | "json"
ansi = true # set false for systemd journal / Docker

[logging.file] — rolling log file

[logging.file]
enabled = false
dir = "logs" # created automatically
prefix = "homecore"
rotation = "daily" # "daily" | "hourly" | "weekly" | "never"
max_size_mb = 100 # rotate when file exceeds this (0 = size-only rotation off)
compress = true # gzip rotated files immediately after rotation
prune_after_days = 5 # delete rotated logs older than N days (0 = never prune)
format = "json" # "json" | "compact" | "pretty"

Active file is always <prefix>.log (never compressed). Rotated files follow:

  • Daily: homecore.2026-03-27.log.gz
  • Hourly: homecore.2026-03-27_14.log.gz

[logging.syslog] — remote syslog

[logging.syslog]
enabled = false
transport = "udp" # "udp" | "tcp"
host = "192.168.1.100"
port = 514
protocol = "rfc5424" # "rfc5424" | "rfc3164"
facility = "daemon"
app_name = "homecore"
level = "warn" # optional override; defaults to global [logging].level

Common recipes

Development — verbose rule engine, quiet broker

[logging]
level = "info"
time_display = "local"

[logging.targets]
hc_core = "debug"
hc_mqtt_client = "debug"
hc_broker = "warn"

[logging.stderr]
enabled = true
format = "pretty"
ansi = true

Production — structured file + remote warnings

[logging]
level = "info"

[logging.stderr]
enabled = false # no console when running as a service

[logging.file]
enabled = true
dir = "/var/log/homecore"
rotation = "daily"
format = "json"
compress = true
prune_after_days = 7 # auto-delete logs older than 7 days

[logging.syslog]
enabled = true
transport = "udp"
host = "192.168.1.50"
port = 514
level = "warn" # only warnings and above go to remote

systemd — journal-friendly

[logging.stderr]
enabled = true
format = "compact"
ansi = false # no ANSI codes in journal

Grafana Loki / Graylog via file

[logging.file]
enabled = true
dir = "/var/log/homecore"
format = "json" # structured fields are required for parsing
rotation = "hourly"

Log target reference

CrateTarget nameWhat it covers
hc-corehc_coreRule engine, scheduler, state bridge, executor, timer/switch/mode managers
hc-apihc_apiHTTP handlers, WebSocket stream, auth middleware
hc-authhc_authJWT issuance/validation, password hashing
hc-statehc_stateDevice registry (redb), time-series history (SQLite)
hc-mqtt-clienthc_mqtt_clientMQTT connection, subscriptions, topic routing
hc-brokerhc_brokerEmbedded rumqttd broker internals
hc-topic-maphc_topic_mapEcosystem profile matching, payload transforms
hc-notifyhc_notifyEmail, Pushover, Telegram notification channels
hc-scriptinghc_scriptingRhai script execution

Log level API

Change log levels at runtime without restart (Admin required):

# Temporarily enable debug for rule engine
curl -s -X POST http://localhost:8080/api/v1/system/log-level \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"target":"hc_core","level":"debug"}'

Live log stream (WebSocket)

# Tail all logs at info+
websocat "ws://localhost:8080/api/v1/logs/stream?token=$TOKEN"

# Filter to warn and above
websocat "ws://localhost:8080/api/v1/logs/stream?token=$TOKEN&level=warn"

# Filter to a specific module
websocat "ws://localhost:8080/api/v1/logs/stream?token=$TOKEN&module=hc_core"

The log stream replays the last 100 buffered log lines on connect, then streams new lines in real time.

Plugin logging

Each hc-* plugin manages its own log file and rotation independently of the homeCore server. All plugins share the same [logging] config schema, configured in the plugin's own config/config.toml:

[logging]
level = "info" # stderr log level; RUST_LOG overrides this
rotation = "daily" # daily | hourly | weekly | never
max_size_mb = 100 # rotate when file exceeds this MB (0 = time-only)
compress = true # gzip rotated files in a background thread
prune_after_days = 5 # delete rotated logs older than N days (0 = never)
log_forward_level = "info" # min level forwarded to core over MQTT
OptionDefaultDescription
level"info"Stderr log level when RUST_LOG is not set. Accepts any tracing filter directive, e.g. "debug" or "hc_hue=debug,rumqttc=warn".
rotation"daily"Time-based rotation strategy: daily, hourly, weekly, or never.
max_size_mb100Size-based rotation threshold. Combined with rotation as "whichever comes first". Set 0 to disable size-based rotation.
compresstrueGzip-compress rotated files in a background thread immediately after rotation. The active log file is never compressed.
prune_after_days0Delete rotated .log and .log.gz files older than this many days. Runs at startup and after each rotation. 0 disables pruning.
log_forward_level"info"Minimum log level forwarded to the HomeCore broker over MQTT. Plugin logs at or above this level appear in the admin UI's Activity page and the /api/v1/logs/stream WebSocket. Set to "off" to disable forwarding entirely.

MQTT log forwarding

Plugins forward their tracing logs to HomeCore over MQTT, making them visible in the admin UI alongside core logs. The forwarding uses QoS 0 (fire-and-forget) to avoid stalling the logging pipeline.

Topic: homecore/plugins/{plugin_id}/logs

Payload format (same as the core's LogLine):

{
"timestamp": "2026-04-07T12:00:00Z",
"level": "INFO",
"target": "hc_yolink::bridge",
"message": "Device registered",
"fields": {"device_id": "yolink_front_door"}
}

The core's state bridge receives these messages and injects them into the log stream broadcast channel, so they appear in:

  • The /api/v1/logs/stream WebSocket (live tail)
  • The admin UI Activity page (filtered by source = "log")

Plugin log files follow the same naming convention as the server:

FileDescription
logs/hc-{plugin}.logActive log (always uncompressed)
logs/hc-{plugin}.2026-03-27.log.gzRotated daily file (compressed)
logs/hc-{plugin}.2026-03-27_14.log.gzRotated hourly file (compressed)
logs/hc-{plugin}.2026-03-27.1.log.gzSecond size-triggered rotation in the same period

Defaults preserve the previous behavior if the [logging] section is absent from a plugin config.