A unified logging framework for Project Zomboid mods with multi-level logging support, child loggers, and configurable sandbox options.
- Quick Start Guide — Get started in 5 minutes
- Usage Examples — Practical code snippets and patterns
- Architectural Decisions — Design philosophy and lifecycle details
- Sandbox Testing Guide — How to verify settings in-game
- Integration Test Protocol — Step-by-step verification flow
- Benchmark Results — Performance benchmarks and stress test results
- Implementation Summary — Technical overview of the framework
- Multi-Level Logging: TRACE, DEBUG, INFO, WARN, ERROR, FATAL (via
writeLog) - Direct Console Logging:
.log()method for immediateprint()output - Sandbox Options: Configure global log levels and mod filtering via in-game settings
- Server/Client Identification: Automatic side detection in logs ([SERVER], [CLIENT], [SINGLE_PLAYER])
- Structured Logging: Support for context objects and details
- Include/Exclude Lists: Fine-grained control over which mods use ZUL settings
- Pino/Winston-inspired API: Familiar logging patterns for JavaScript developers
- Subscribe to the mod on Steam Workshop (or download manually)
- Enable the mod in your Project Zomboid mod list
- Configure sandbox options (optional) before starting a new game
local ZUL = require("zul")
local logger = ZUL.new("MyMod")
-- Simple logging
logger:info("Player entered RV")
logger:warn("Low health detected")
logger:error("Failed to load config")
-- Logging with context
logger:debug("Processing player data", { playerId = 123, health = 50 })
-- Structured logging with action:phase pattern
logger:trace("Database", "Query", { table = "players", rows = 10 })ZUL supports 6 log levels (from most to least verbose):
- TRACE (10): Very detailed debugging information
- DEBUG (20): Debugging information
- INFO (30): Informational messages (default)
- WARN (40): Warning messages
- ERROR (50): Error messages
- FATAL (60): Fatal error messages
local ZUL = require("zul")
-- Set log level for a specific mod
ZUL.setLevel("MyMod", "DEBUG")
ZUL.setLevel("AnotherMod", ZUL.Level.TRACE)
-- Using child logger
local logger = ZUL.new("MyMod")
logger:setLevel("WARN") -- Only show WARN and above
-- Disable logging entirely for a mod
ZUL.setLevel("NoisyMod", ZUL.Level.NONE)ZUL provides three sandbox options for server administrators:
Sets the default log level for all mods using ZUL. This applies to:
- All mods if the Include list is empty
- Only mods in the Include list (if specified)
- Does NOT apply to mods in the Exclude list
Options: TRACE, DEBUG, INFO (default), WARN, ERROR, FATAL
Comma-separated list of mod names to apply ZUL sandbox settings to.
Example: MyMod,AnotherMod,ThirdMod
Behavior:
- If empty: Settings apply to ALL mods (except excluded ones)
- If specified: Settings apply ONLY to listed mods
Comma-separated list of mod names to exclude from ZUL sandbox settings.
Example: NoisyMod,DebugMod
Behavior:
- These mods will use their own programmatically set log levels
- Exclude list takes precedence over Include list
You can override how ZUL handles level-specific logging (TRACE, DEBUG, INFO, WARN, ERROR, FATAL). Note that .log() bypasses this and always uses print().
local ZUL = require("zul")
-- Custom log handler (e.g., send to external service)
function ZUL.logImpl(modName, fullMessage)
-- Default behavior
writeLog(modName, fullMessage)
-- Custom behavior
sendToExternalLogger(modName, fullMessage)
endlocal logger = ZUL.new("MyMod")
local currentLevel = logger:getEffectiveLevel()
print("Current log level: " .. currentLevel)
-- Check if a specific level is enabled
if ZUL.isLoggingEnabled("MyMod", ZUL.Level.DEBUG) then
-- Perform expensive debug operation
local debugData = collectDebugInfo()
logger:debug("Debug info", debugData)
endlocal logger = ZUL.new("MyMod")
-- Log with context object
logger:info("Player action", {
action = "craft",
item = "axe",
duration = 5.2
})
-- Log with action:phase:details pattern
logger:debug("RV", "Entry", {
vehicleId = "rv_001",
playerId = 123,
roomId = 5
})If you were using the previous SharedLogger module:
-
Update your require statement:
-- Old local SharedLogger = require "utils/SharedLogger" -- New local ZUL = require("zul")
-
Update all references from
SharedLoggertoZUL:-- Old local logger = SharedLogger.new("MyMod") -- New local logger = ZUL.new("MyMod")
-
The API is fully backward compatible - no other changes needed!
ZUL.Level- Table containing numeric log levels:TRACE(10)DEBUG(20)INFO(30)WARN(40)ERROR(50)FATAL(60)NONE(100) - Disables logging for the mod
ZUL.new(modName)- Create a child logger for a modZUL.setLevel(modName, level)- Set log level for a modZUL.getEffectiveLevel(modName)- Get current log level for a modZUL.isLoggingEnabled(modName, level)- Check if logging is enabled for a modZUL.shouldApplySandboxSettings(modName)- Check if sandbox logic applies to a modZUL.resolveLevel(level)- Convert level name to numeric valueZUL.loadSandboxOptions(force)- Load/refresh sandbox options (called automatically)ZUL.serialize(val)- Convert tables/values to strings for logging (internal helper)
logger:trace(message, context, details)- Log at TRACE levellogger:debug(message, context, details)- Log at DEBUG levellogger:info(message, context, details)- Log at INFO levellogger:warn(message, context, details)- Log at WARN levellogger:error(message, context, details)- Log at ERROR levellogger:fatal(message, context, details)- Log at FATAL levellogger:log(message, context, details)- Log directly to console viaprint()(bypassesZUL.logImpl)logger:setLevel(level)- Set log level for this logger's modlogger:getEffectiveLevel()- Get effective log levellogger:isLoggingEnabled(level)- Check if a specific level is enabled for this logger
local ZUL = require("zul")
local logger = ZUL.new("MyAwesomeMod")
local function onPlayerDeath(player)
logger:warn("Player died", {
username = player:getUsername(),
location = player:getX() .. "," .. player:getY()
})
end
Events.OnPlayerDeath.Add(onPlayerDeath)local ZUL = require("zul")
local logger = ZUL.new("PerformanceMod")
local function expensiveOperation()
if ZUL.isLoggingEnabled("PerformanceMod", ZUL.Level.DEBUG) then
local startTime = os.clock()
-- ... do work ...
local elapsed = os.clock() - startTime
logger:debug("Operation completed", { duration = elapsed })
end
end-- ModuleA.lua
local ZUL = require("zul")
local logger = ZUL.new("MyMod")
function ModuleA.init()
logger:info("Module A initialized")
end
-- ModuleB.lua
local ZUL = require("zul")
local logger = ZUL.new("MyMod")
function ModuleB.init()
logger:info("Module B initialized")
end
-- Both modules share the same log level settingsProject Zomboid's Lua environment initializes in stages. ZUL uses a multi-stage approach to ensure settings are captured as early as possible while remaining robust in multiplayer environments where sandbox settings are synchronized after boot.
During OnGameBoot, ZUL makes its first attempt to load settings. This catches mods that initialize very early. In Multiplayer, this will likely use local/default settings.
ZUL hooks into OnInitGlobalModData, OnInitWorld, OnGameStart, and OnServerStarted with a force refresh flag.
- As soon as the server sends the synchronized
SandboxVarspacket, ZUL will update its internal configuration. - You will see
ZUL sandbox settings refreshed (Server Sync)in the TRACE logs when this occurs.
Mods that log during the early boot sequence (before Stage 2) may use the framework's default levels (INFO) or local settings until the server sync completes. Once synchronized, all subsequent logs will respect the server's sandbox configuration.
- Ko-fi: https://ko-fi.com/escapepz
- GitHub: https://github.com/escapepz
Created by eScape (@escapepz)
- Update folder structure to match new game version (42.14).
- Rename files to lowercase for cross-platform compatibility. Now use require("zul") instead, NOT "ZUL"
- Multiplayer Sync Fix: Implemented staged initialization with forced re-syncs to handle server Sandbox synchronization lag.
- Improved Logging: Added state-aware initialization logs (First-time vs Refresh).
- Hardened Events: Added
OnGameStartandOnServerStartedas final initialization safety nets. - Enhanced Guarding: Improved state management during the complex PZ startup sequence.
- Initial release
- Multi-level logging support
- Child logger API
- Sandbox options integration
- Include/exclude mod filtering