Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the Defold logging module by extracting functionality into separate internal modules (config, formatter, file_writter), adds file logging capabilities with callback support, and reorganizes documentation into dedicated files.
Key Changes:
- Refactored monolithic log.lua into modular structure with internal/config.lua, internal/formatter.lua, and internal/file_writter.lua
- Added file logging functionality with write_nearby_this_file() and callback system for custom log handlers
- Moved configuration and use case documentation from README.md to separate CONFIGURATION.md and USE_CASES.md files
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
| log/log.lua | Major refactoring to extract formatting and configuration logic; added file logging support and callback system |
| log/internal/config.lua | New module containing all configuration constants and system detection logic |
| log/internal/formatter.lua | New module containing log formatting and table serialization logic |
| log/internal/file_writter.lua | New module providing file writing functionality with state management |
| docs/USE_CASES.md | New documentation file with practical examples of log module usage |
| docs/CONFIGURATION.md | New documentation file detailing configuration options (moved from README) |
| README.md | Simplified by moving detailed configuration docs to separate files; updated API documentation |
| example/example.gui_script | Updated example to demonstrate file logging and callback features |
| game.project | Enabled write_log_file and updated log format configuration |
| .gitignore | Added log.txt to ignored files |
Comments suppressed due to low confidence (1)
log/log.lua:254
- The metatable no longer has an '__index' metamethod pointing to a default logger instance. This means that when the log module is used directly (e.g., 'log:trace()'), it will try to call methods on the M table which only has the name and level fields set. The _last_gc_memory and _last_message_time fields are not initialized for the default logger, which could cause issues with tracking features. Consider either restoring the __index to a properly initialized default logger, or initializing all necessary fields on M directly.
return setmetatable(M, {
__call = function(self, name, force_logger_level_in_debug)
return M.get_logger(name, force_logger_level_in_debug)
end
})
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,114 @@ | |||
| local config = require("log.internal.config") | |||
| local formatter = require("log.internal.formatter") | |||
|
|
|||
There was a problem hiding this comment.
The filename 'file_writter.lua' contains a spelling error. It should be 'file_writer.lua' (single 't'). This affects the module name and all references to it.
| ---@param data any | ||
| ---@param message string Message to log |
There was a problem hiding this comment.
The function parameter order for 'message' and 'data' annotations is inconsistent. Line 140 shows 'message' as nullable (string?) while line 141 shows 'data', but line 149 has them in reverse order (data before message). This inconsistency could confuse API consumers. The parameter order in the actual function signature should match the documentation order.
| ---@param data any | |
| ---@param message string Message to log | |
| ---@param message string Message to log | |
| ---@param data any |
| end | ||
| ---Clear all callbacks | ||
| function M.clear_callbacks() | ||
| log_callbacks = {} |
There was a problem hiding this comment.
After clearing callbacks with 'log_callbacks = {}', the file_writter.log_callback (which was in the original array at line 21) is lost and won't be restored. This will break file logging functionality since the internal callback is removed. Consider using a different approach, such as only removing user-added callbacks while preserving internal ones, or documenting that this clears ALL callbacks including internal file writing.
| log_callbacks = {} | |
| -- Preserve internal callbacks such as the file writer while | |
| -- removing any user-added callbacks. | |
| log_callbacks = { file_writter.log_callback } |
|
|
||
|
|
||
| function init(self) | ||
| log.clear_log_files() |
There was a problem hiding this comment.
Calling 'log.clear_log_files()' before any logging occurs will try to delete files that may not exist yet, which could produce error messages. Additionally, calling it in the 'final' function (line 103) right after 'close_log_files()' (line 102) will delete all log files that were just created during the game session. Consider removing the call from 'init' and documenting that 'clear_log_files' is typically used for maintenance rather than normal operation.
| if config.IS_FORMAT_MESSAGE and message then | ||
| string_message_block = string_m.gsub(string_message_block, "%%message", message) |
There was a problem hiding this comment.
When message is nil and IS_FORMAT_MESSAGE is true, the %message placeholder in the format string won't be replaced (line 167 guards against this). This will leave '%message' literally in the output string, which looks like a formatting error. Consider either: (1) replacing %message with an empty string or a placeholder like "(no message)" when message is nil, or (2) ensuring that the check at line 71 of log.lua prevents calling format when message is nil.
| if config.IS_FORMAT_MESSAGE and message then | |
| string_message_block = string_m.gsub(string_message_block, "%%message", message) | |
| if config.IS_FORMAT_MESSAGE then | |
| local formatted_message = message ~= nil and tostring(message) or "" | |
| string_message_block = string_m.gsub(string_message_block, "%%message", formatted_message) |
| **log.set_callback** | ||
| --- | ||
| ```lua | ||
| log.set_callback(callback) | ||
| ``` | ||
| Set a custom handler for log messages. This allows you to intercept and process log messages for additional purposes such as file logging, remote logging, or custom formatting. The callback is called in addition to the default console output. | ||
|
|
||
| - **Parameters:** | ||
| - `callback`: A function that receives `(logger, level, message, context, log_message)` parameters, or `nil` to remove the callback. | ||
| - `logger`: The logger instance that generated the log | ||
| - `level`: The log level (TRACE, DEBUG, INFO, WARN, ERROR, FATAL) | ||
| - `message`: The original message string | ||
| - `context`: Any additional context data passed to the log function | ||
| - `log_message`: The formatted log message string (as it appears in console) | ||
|
|
||
| - **Usage Example:** | ||
|
|
||
| ```lua | ||
| -- Set up file logging | ||
| log.set_callback(function(logger, level, message, context, log_message) | ||
| local file = io.open("game.log", "a") | ||
| if file then | ||
| file:write(log_message .. "\n") | ||
| file:close() | ||
| end | ||
| end) | ||
|
|
||
| -- Remove callback | ||
| log.set_callback(nil) | ||
| ``` |
There was a problem hiding this comment.
The documentation describes 'log.set_callback(callback)' with a single callback parameter that can be nil to remove the callback. However, the actual implementation uses 'log.add_callback(callback)' which adds callbacks to an array, not replaces them. The documented behavior of passing nil to remove callbacks doesn't match the implementation. Update the documentation to reflect the actual API: add_callback, remove_callback, and clear_callbacks.
| function update(self, dt) | ||
| logger:info("Update", dt) | ||
| end |
There was a problem hiding this comment.
The 'update' function will log every frame (line 96), which could generate an enormous number of log entries very quickly (60+ logs per second). This will rapidly fill disk space and likely cause performance issues. This is probably intended as an example to demonstrate file logging, but it should include a warning comment or use a less aggressive logging pattern (e.g., log every N frames or every second).
| if logger.file then | ||
| if not FILE_HANDLERS[logger.file] then | ||
| FILE_HANDLERS[logger.file] = io.open(logger.file, "a") | ||
| end | ||
|
|
||
| local handler = FILE_HANDLERS[logger.file] | ||
| if handler then | ||
| handler:write(log_message .. "\n") | ||
| handler:flush() |
There was a problem hiding this comment.
If io.open() fails at line 24 (e.g., due to invalid path, permissions, or disk full), the FILE_HANDLERS entry is never set, but the code will retry opening the file on every subsequent log message. This creates repeated failed open attempts. Consider either: (1) caching the failure (set FILE_HANDLERS[logger.file] = false on failure), or (2) logging a warning when the file cannot be opened so the user knows file logging has failed.
| handler:close() | ||
| end | ||
|
|
||
| FILE_HANDLERS = {} |
There was a problem hiding this comment.
Assigning 'FILE_HANDLERS = {}' at line 111 creates a new empty table but doesn't update the module's reference to it. Any code that cached a reference to the old FILE_HANDLERS table will still have stale file handlers. While this may not be an issue in the current implementation, it's better practice to clear the existing table contents with a loop (for k in pairs(FILE_HANDLERS) do FILE_HANDLERS[k] = nil end) to maintain the same table reference.
| FILE_HANDLERS = {} | |
| for k in pairs(FILE_HANDLERS) do | |
| FILE_HANDLERS[k] = nil | |
| end |
| |----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||
| | **%logger** | The name of the logger instance producing the log message. Helps in identifying the source of the log message. | | ||
| | **%levelname** | The name of the log level (e.g., DEBUG, INFO, WARN, etc.). Provides clarity on the severity or nature of the log message. Should be placed at the beginning of the log message for color highlighting in the Defold Console. | | ||
| | **%levelshort** | The short name of the log level (e.g., D, I, W, E). Provides a compact representation of the log level. But Defold Console will be not able to highlight it. | |
There was a problem hiding this comment.
Grammar issue: "But Defold Console will be not able" should be "But Defold Console will not be able" (word order - 'not' should come before 'be').
| | **%levelshort** | The short name of the log level (e.g., D, I, W, E). Provides a compact representation of the log level. But Defold Console will be not able to highlight it. | | |
| | **%levelshort** | The short name of the log level (e.g., D, I, W, E). Provides a compact representation of the log level. But Defold Console will not be able to highlight it. | |
No description provided.