Logging And Troubleshooting
This guide covers logging setup and the most common runtime failure modes: settings loading, inventory files, plugin resolution, runner execution, and empty host selections.
Logging Model
Genja emits logs through the Rust log facade, but it does not initialize a
global logger. Applications own logger setup because logging is process-wide in
Rust and application-specific in Python.
The logging settings section is parsed and exposed for applications to use:
logging:
enabled: true
level: info
log_file: ./genja.log
to_console: false
file_size: 10485760
max_file_count: 10
Genja does not automatically create log_file, install console logging, or
apply log rotation. Read these values and initialize the logger that fits your
application.
Initialize Logging
For normal application startup, load settings first, then initialize logging
from settings.logging().
use genja::genja_core::Settings;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let settings = Settings::from_file("settings.yaml")?;
if settings.logging().enabled() {
env_logger::Builder::from_env(
env_logger::Env::default()
.default_filter_or(settings.logging().level()),
)
.init();
}
Ok(())
}
import logging
import genja as genja_lib
settings = genja_lib.Settings.from_file("settings.yaml")
if settings.logging.enabled:
logging.basicConfig(
level=getattr(logging, settings.logging.level.upper()),
)
Logs emitted while settings are loading can happen before settings-backed
logging is available. When troubleshooting settings loading itself, initialize a
temporary logger before calling Settings::from_file(...).
use genja::genja_core::Settings;
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("debug"),
)
.init();
let settings = Settings::from_file("settings.yaml")?;
println!("configured log level: {}", settings.logging().level());
Ok(())
}
import logging
import genja as genja_lib
logging.basicConfig(level=logging.DEBUG)
settings = genja_lib.Settings.from_file("settings.yaml")
print(f"configured log level: {settings.logging.level}")
Settings Load Failures
Settings::from_file(...) accepts only .json, .yaml, and .yml files.
Other extensions return an unsupported format error.
Common settings failures:
- unsupported extension: use
.json,.yaml, or.yml - unreadable file: check the path and permissions
- deserialize failure: check field names, field types, and YAML/JSON syntax
- SSH config failure:
ssh.config_fileexists but cannot be opened or parsed
If ssh.config_file is set, Genja validates that file during settings load. Set
the field to null or omit it when you do not want SSH config validation.
Inventory File Issues
The built-in FileInventoryPlugin reads paths from inventory.options.
inventory:
plugin: FileInventoryPlugin
options:
hosts_file: ./hosts.yaml
groups_file: ./groups.yaml
defaults_file: ./defaults.yaml
Common inventory failures:
- file does not exist or cannot be read
- unsupported inventory extension
- invalid JSON or YAML syntax
- unknown host or group fields
- transform plugin configured but not registered
- transform plugin name points to a plugin with the wrong type
Inventory files must use .json, .yaml, or .yml. Hosts and groups are maps
keyed by name. Defaults is one object.
If inventory loads but a task runs on no hosts, inspect the selected hosts:
for host_id in genja.host_ids() {
println!("{host_id}");
}
for host_id in genja.host_ids():
print(host_id)
Plugin Load And Resolution Failures
Settings and task metadata select plugins by name. The plugin must be registered with the correct plugin type before the runtime needs it.
Common plugin failures:
plugin 'name' not found: the plugin was not registered or loadedplugin 'name' is not an inventory plugin: the name exists with the wrong typeplugin 'name' is not a runner plugin:runner.pluginpoints to the wrong typetransform plugin 'name' not found: inventory transform plugin is missing- dynamic load failure: the shared library is missing, invalid, or lacks
create_plugins - Python
pyproject.tomlmismatch: manifest key does not match pluginname
Inspect registered plugins before building or running:
for (name, group) in plugins.get_all_plugin_names_and_groups() {
println!("{name}: {group}");
}
for name, group in plugins.plugin_names_and_groups():
print(f"{name}: {group}")
Runner Errors
Use run_task_async(...) or run_tasks_async(...) when already inside an async
runtime. Rust sync wrappers return an error if called from an active Tokio
runtime.
Task entrypoint failures are recorded as failed host results. They do not
normally make run_task(...) return an outer error.
Outer runner errors usually mean one of these happened:
- configured runner plugin was missing or the wrong type
- processor name resolution failed
- processor hook returned an error
- custom runner returned an error
- threaded worker task failed before producing host results
Check both the outer Result and the returned task results. A successful outer
Ok(TaskResults) can still contain failed hosts.
Why Did Nothing Run?
Check these in order:
host_ids()is empty because inventory loaded no hosts.- A filter returned a runtime with no selected hosts.
max_depthis0, so sub-tasks did not run.- The task was skipped by task logic.
- The task selected a connection plugin that failed to resolve or open.
- You called a sync Rust API inside Tokio and got an outer error before task execution started.
Empty host selections are reportable, not fatal. Runners can return a task result object with no host results.
Result Checks
After a run, inspect host summaries before assuming the workflow succeeded:
let summary = results.task_summary();
let hosts = summary.hosts();
println!(
"passed={} failed={} skipped={}",
hosts.passed(),
hosts.failed(),
hosts.skipped(),
);
print(results.host_summary())
print(results.to_json(pretty=True))
Use Settings for the field reference, Inventory for inventory structure, Plugins for registration and loading, and Runners for execution behavior.