Skip to main content

Getting Started

Call Logging:basicConfig with a config table! This will set the log level and a default handler on the root logger that sends logs to print and warn (depending on level/severity).

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Logging = require(ReplicatedStorage:WaitForChild("Logging"))
-- level: log events less severe than this are ignored
level=Logging.Level.Debug; -- hint: "Debug" also works
-- format: how do you want your log messages to look?
format="%(asctime)s - %(name)s - %(level)s - %(message)s";

You're all set to go! When stuff happens, log it! You can now create logs on the root logger using the following logging functions, which work like string.format:

-- Debug: For details useful during debugging:
Logging:debug("Score updated to %d", score) -- aka logger:print
-- Info: For confirmation that things are working:
Logging:info("Round started with %d participants", #players)
-- Warning: For potential/future issues:
Logging:warning("%s messed up", "The thing") -- aka logger:warn
-- Error: For suppressed errors:
Logging:error("Matchmaker broke with %d players in queue: %s", #players, error_message)
-- Critical: For showstopping events:
Logging:critical("Server is on fire! Abort, abort, abort!")
-- Or, specify a particular log level:
Logging:log(Logging.Level.Info, "Cave generated (difficulty = %s)", difficulty)

Calling these functions at the module level (Logging) calls the respective method on the root logger object. It is better to use logger objects directly - read on!

Logger Objects

Use Logging:getLogger to get a Logger with the name of whatever cool thing you happen to be making. Loggers contain the same previously mentioned functions (debug, info, warning, error, critical and log).

local logger = Logging:getLogger("MyFightingGame")
logger:info("Match started: %s vs %s", player1.Name, player2.Name)

Loggers exist in a hierarchy, much like Roblox objects. Every logger has one parent, except the root logger (named "Logging"). If a system uses a logger, then its sub-systems should use child loggers. You can use Logger:getChild or Logging:getLogger with period-separated names to get child loggers:

logger:getLogger("MyFightingGame.RoundSystem") -- OR


When a logger is used to create a log, also known as a record, its effective level is determined: by default, loggers have a level of NotSet, which means they defer to their parent's effective level. If the record's level is greater than or equal to the logger's effective level, and the record satisfies the logger's filters, the record is finally emit and passed to the logger's handlers!

logger:debug("Calculating dingbat...") -- Ignore, because Debug < Info
logger:info("Rocket launched") -- Emit, because Info == Info
logger:error("Sandwich storage full") -- Emit, because Error > Info

Setting the level of the root logger is done with Logging:basicConfig, whose default level is Warning. A record should be handled by at least one handler, otherwise you'll get an "unhandled record" warning.


Beyond the normal level filtering that is built in to loggers, you can also attach functions that do your own filtering logic. Each filter is called with both the logger itself and the record to filter. If any filter returns false, the record is ignored.

logger:addFilter(function (logger, record)
-- Handle only messages shorter than 10 bytes:
return record:getMessage():len() < 10


When a logger emits a log it propagates the record to its ancestor's handlers (unless logger.propagates = false). Note that the level and/or filters of the ancestors are not considered when a record is propagated.

Handler Objects

A Handler does something with log records. They can be added/removed to loggers:


Handlers are abstract. See the following concrete implementations:


When you call Logging:basicConfig, the root logger gets a OutputHandler which passes record messages to the built-in print/warn functions, depending on the record level. This is your bread-and-butter handler for use in Roblox Studio's Output window and in the Developer Console.


A MemoryHandler stores a number of records in a buffer until it fills, at which point it will flush all the records to a target handler (if set), then empty. If it handles a record of level Error or higher, it flushes early.


A NullHandler that doesn't do anything with records! How quaint. It is useful for loggers that do not propagate logs to their parent, but need at least one handler to avoid unhandled records.


A FuncHandler calls a function with a record immediately. If you pass a function to Logger:addHandler, it will be automatically wrapped in a FuncHandler.

Sugary Goodness

Logging is meant to be easy. To that end, there's some convenience Logger methods, which are also available on the Logging module itself.

Wrapping print and warn

If you like using the built-in print and warn functions, use Logger:wrap, which returns two functions that replace them. Calling these will emit Debug/Warning records accordingly. Like their original counterparts, they don't return anything.

-- Works great for using Logging in existing code:
local print, warn = logger:wrap()
print("Meow") -- works like logger:debug("Meow")
warn("Woof") -- works like logger:warning("Woof")

Pass true to wrap and it will also call the original functions. This isn't recommended, because it's preferable to attach an OutputHandler ideally using Logging:basicConfig.

pcall and xpcall

Replace pcall โ†’ logger:pcall and xpcall โ†’ logger:xpcall and an Error is logged automatically if the function raises one.

logger:pcall(error, "Oh noes") --> logger:error("Oh noes")
logger:xpcall(error, print, "Whoops") --> logger:error("Whoops"), then print("Whoops")