|
| 1 | +# Logging |
| 2 | + |
| 3 | +Logging in Godot can be accessed by using the `godot_print!`, `godot_warn!`, and `godot_error!` macros. |
| 4 | + |
| 5 | +These macros only work when the library is loaded by Godot. They will panic instead when invoked outside that context, for example, when the crate is being tested with cargo test |
| 6 | + |
| 7 | +## Simple wrapper macros for test configurations |
| 8 | + |
| 9 | +The first option that you have is wrap the `godot_print!` macros in the following macro that will use `godot_print!` when running with Godot, and stdout when run during tests. |
| 10 | + |
| 11 | +```rust |
| 12 | +/// prints to Godot's console, except in tests, there it prints to stdout |
| 13 | +macro_rules! console_print { |
| 14 | + ($($args:tt)*) => ({ |
| 15 | + if cfg!(test) { |
| 16 | + println!($($args)*); |
| 17 | + } else { |
| 18 | + gdnative::godot_print!($($args)*); |
| 19 | + } |
| 20 | + }); |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +## Using a logging Crate |
| 25 | + |
| 26 | +A more robust solution is to integrate an existing logging library. |
| 27 | + |
| 28 | +This recipe demonstrates using the log crate with flexi-logger. While most of this guide will work with other backends, the initialization and `LogWriter` implementation may differ. |
| 29 | + |
| 30 | +First add the following crates to your `Cargo.toml` file. |
| 31 | + |
| 32 | +```toml |
| 33 | +log = "0.4.14" |
| 34 | +flexi_logger = "0.17.1" |
| 35 | +``` |
| 36 | + |
| 37 | +As the logger will need to use Godot's logging methods, add a Writer implementation: |
| 38 | + |
| 39 | +When using flexi_logger it requires a LogWriter implementation that can point to the godot_print! macro. Note: this will vary by logging framework. |
| 40 | + |
| 41 | +```rust |
| 42 | +use gdnative::prelude::*; |
| 43 | +use flexi_logger::writers::LogWriter; |
| 44 | +use flexi_logger::{DeferredNow, Record}; |
| 45 | +use log::{Level, LevelFilter}; |
| 46 | + |
| 47 | +pub struct GodotLogWriter {} |
| 48 | + |
| 49 | +impl LogWriter for GodotLogWriter { |
| 50 | + fn write(&self, _now: &mut DeferredNow, record: &Record) -> std::io::Result<()> { |
| 51 | + match record.level() { |
| 52 | + // Optionally push the Warnings to the godot_error! macro to display as an error in the Godot editor. |
| 53 | + flexi_logger::Level::Error => godot_error!("{}:{} -- {}", record.level(), record.target(), record.args()), |
| 54 | + // Optionally push the Warnings to the godot_warn! macro to display as a warning in the Godot editor. |
| 55 | + flexi_logger::Level::Warn => godot_warn!("{}:{} -- {}",record.level(), record.target(), record.args()), |
| 56 | + _ => godot_print!("{}:{} -- {}", record.level(), record.target(), record.args()) |
| 57 | + } ; |
| 58 | + Ok(()) |
| 59 | + } |
| 60 | + |
| 61 | + fn flush(&self) -> std::io::Result<()> { |
| 62 | + Ok(()) |
| 63 | + } |
| 64 | + |
| 65 | + fn max_log_level(&self) -> LevelFilter { |
| 66 | + LevelFilter::Trace |
| 67 | + } |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +For the logger setup, place the code logger configuration code in your `fn init(handle: InitHandle)` as follows. |
| 72 | + |
| 73 | +To add the logging configuration, you need to add the initial configuration and start the logger inside the init function. |
| 74 | +```rust |
| 75 | +fn init(handle: InitHandle) { |
| 76 | + flexi_logger::Logger::with_str("trace") |
| 77 | + .log_target(flexi_logger::LogTarget::Writer(Box::new(crate::util::GodotLogWriter {}))) |
| 78 | + .start() |
| 79 | + .expect("the logger should start"); |
| 80 | + /* Otherclass initialization goes here */ |
| 81 | +} |
| 82 | +godot_init!(init); |
| 83 | +``` |
| 84 | + |
| 85 | +### Setting up a log target for tests |
| 86 | +When running in a test configuration, if you would like logging functionality, you will need to initialize a log target. |
| 87 | + |
| 88 | +As tests are run in parallel, it will be necessary to use something like the following code to initialize the logger only once. The `#[cfg(test)]` attributes are used to ensure that this code is not accessible outside of test builds. |
| 89 | + |
| 90 | +Place this in your crate root (usually lib.rs) |
| 91 | + |
| 92 | +```rust |
| 93 | +#[cfg(test)] |
| 94 | +use std::sync::Once; |
| 95 | +#[cfg(test)] |
| 96 | +static TEST_LOGGER_INIT: Once = Once::new(); |
| 97 | +#[cfg(test)] |
| 98 | +fn test_setup_logger() { |
| 99 | + TEST_LOGGER_INIT.call_once(||{ |
| 100 | + flexi_logger::Logger::with_str("debug") |
| 101 | + .log_target(flexi_logger::LogTarget::StdOut) |
| 102 | + .start() |
| 103 | + .expect("the logger should start"); |
| 104 | + }); |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +You can call the above code in your units tests with `crate::test_setup_logger()`. Please note: currently there does not appear to be a tes case that will be called before tests are configured, so the `test_setup_logger` will need to be called in every test where you require log output. |
| 109 | + |
| 110 | +Now that the logging is configured, you can use use it in your code such as in the following sample |
| 111 | +```rust |
| 112 | +log::trace!("trace message: {}", "message string"); |
| 113 | +log::debug!("debug message: {}", "message string"); |
| 114 | +log::info!("info message: {}", "message string"); |
| 115 | +log::warn!("warning message: {}", "message string"); |
| 116 | +log::error!("error message: {}", "message string"); |
| 117 | +``` |
| 118 | + |
| 119 | +At this point, we have a logging solution implemented for our Rust based code that will pipe the log messages to Godot. |
| 120 | + |
| 121 | +But what about GDScript? It would be nice to have consistent log messages in both GDScript and GDNative. One way to ensure that is to expose the logging functionality to Godot with a `NativeClass`. |
| 122 | + |
| 123 | +### Exposing to GDScript |
| 124 | + |
| 125 | +> ### Note |
| 126 | +> As the rust macros cannot get the GDScript name or resource_path, it is necessary to pass the log target from GDScript. |
| 127 | +
|
| 128 | +```rust |
| 129 | +#[derive(NativeClass, Copy, Clone, Default)] |
| 130 | +#[user_data(Aether<DebugLogger>)] |
| 131 | +#[inherit(Node)] |
| 132 | +pub struct DebugLogger; |
| 133 | + |
| 134 | +#[methods] |
| 135 | +impl DebugLogger { |
| 136 | + fn new(_: &Node) -> Self { |
| 137 | + Self {} |
| 138 | + } |
| 139 | + #[export] |
| 140 | + fn error(&self, _owner: &Node, target: String, message: String) { |
| 141 | + log::error!(target: &target, "{}", message); |
| 142 | + } |
| 143 | + #[export] |
| 144 | + fn warn(&self, _: &Node, target: String, message: String) { |
| 145 | + log::warn!(target: &target, "{}", message); |
| 146 | + } |
| 147 | + #[export] |
| 148 | + fn info(&self, _: &Node, target: String, message: String) { |
| 149 | + log::info!(target: &target, "{}", message); |
| 150 | + } |
| 151 | + #[export] |
| 152 | + fn debug(&self, _: &Node, target: String, message: String) { |
| 153 | + log::debug!(target: &target, "{}", message); |
| 154 | + } |
| 155 | + #[export] |
| 156 | + fn trace(&self, _: &Node, target: String, message: String) { |
| 157 | + log::trace!(target: &target, "{}", message); |
| 158 | + } |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +After adding the above class with the `handle.add_class::<DebugLogger>()` in the `init(handle: InitHandle)` function, add it as an Autoload Singleton in your project and you can access the logging functionality with the following function. |
| 163 | + |
| 164 | +In the example below, I chose the name "game_logger" for the Autoload singleton. |
| 165 | + |
| 166 | +```gdscript |
| 167 | +game_logger.trace("name_of_script.gd", "this is a trace message") |
| 168 | +game_logger.debug("name_of_script.gd", "this is a debug message") |
| 169 | +game_logger.info("name_of_script.gd", "this is an info message") |
| 170 | +game_logger.warn("name_of_script.gd", "this is a warning message") |
| 171 | +game_logger.error("name_of_script.gd", "this is an error message") |
| 172 | +``` |
| 173 | + |
| 174 | +As this is not very ergonomic, it is possible to make a more convenient access point that you can use in your scripts. To make the interface closer to the Rust one, we can create a `Logger` class in GDScript that will call the global methods with the name of our script class. |
| 175 | + |
| 176 | +```gdscript |
| 177 | +extends Reference |
| 178 | +
|
| 179 | +class_name Logger |
| 180 | +
|
| 181 | +var _script_name |
| 182 | +
|
| 183 | +func _init(script_name: String) -> void: |
| 184 | + self._script_name = script_name |
| 185 | +
|
| 186 | +func trace(msg: String) -> void: |
| 187 | + D.trace(self._script_name, msg) |
| 188 | + |
| 189 | +func debug(msg: String) -> void: |
| 190 | + D.debug(self._script_name, msg) |
| 191 | +
|
| 192 | +func info(msg: String) -> void: |
| 193 | + D.info(self._script_name, msg) |
| 194 | +
|
| 195 | +func warn(msg: String) -> void: |
| 196 | + D.warn(self._script_name, msg) |
| 197 | +
|
| 198 | +func error(msg: String) -> void: |
| 199 | + D.error(self._script_name, msg) |
| 200 | +``` |
| 201 | + |
| 202 | +To use the above class, create an instance of `Logger` in a local variable with the desired `script_name` and use it as in the script example below: |
| 203 | + |
| 204 | +```gdscript |
| 205 | +extends Node |
| 206 | +
|
| 207 | +var logger = Logger.new("script_name.gd") |
| 208 | +
|
| 209 | +func _ready() -> void: |
| 210 | + logger.info("_ready") |
| 211 | +``` |
| 212 | + |
| 213 | +And now you have a logging solution fully implemented in Rust and usable in GDScript. |
0 commit comments