UwULogger v1.1.0 — intensity levels, table printing, and a logger that refuses to be silenced
Version: 1.1.0
Previous version: 1.0.0 (which I described as “final” and “perfect.” I was wrong. It is now more perfect.)
First, here’s what it looks like now
Before I explain anything, I want you to see the output. This is a real game server starting up, running UwULogger at "max" intensity, encountering a problem, and then being told to be quiet about it.
> [DataStore] Initiawizing~ pwofiwe s-s-sewvice fow 47 c-connywected pwayers. *holds paws* nyaa~
> [DataStore] Pwayer 3821049 woaded in 0.014s~ *does a little spin* (✧ω✧)
> [DataStore] Pwayer 9910032 woaded in *knocks your coffee off the desk* 0.031s~ >w<!
> [AntiCheat] Expwoit detecshun~ s-sewvice ish onwine~ and wunning~ *wags tail* ヾ(≧▽≦*)o
-- logger:silence() called here --
> [AntiCheat] CWITICAW~ EWWOW~: c-c-c-connywecshun~ to vewifyicashun~ s-sewvew faiwed *flops down dramatically*
-- ^ that's an :error(). it doesn't care that you asked for silence. --
> i took ur silence suggestion into consideration and i said no. [AntiCheat] s-s-suspishus~ pwayer actshun~ detected~ on UsewId 7710293~ *wiggles* (¬‿¬)
> ur not my mom. [AntiCheat] Matchmaking~ s-sewvew~ wegistewed~ *nuzzles* (˶ᵔ ᵕ ᵔ˶)
> silenced. whatever. im not even upset. (i am upset.) (ಥ﹏ಥ)
> [AntiCheat] Authenshun~ token~ vawid~ fow session~ c-c-c-cwypto-9f3a *boops your nose* mrrp
-- logger:unsilence() called here --
> [DataStore] p-p-pwofiwe *wiggles* data fow UsewId 3821049!! ~
{
["coins"] = 4200,
["deaths"] = 99,
["inventory"] = {
[1] = "sword",
[2] = "shield",
[3] = "mysterious egg (do not open)",
},
["level"] = 47,
["username"] = "coolkid2009",
}
The printTable output is the last one. The label is uwufied. The contents are left readable, because I am not a monster.
What’s new
Intensity levels
UwULogger now has four configurable intensity levels, set at construction or at any time via :setIntensity().
-- at construction:
local logger = UwULogger.new("MySystem", "max")
-- or at runtime:
logger:setIntensity("uwu")
The type is "subtle" | "normal" | "uwu" | "max", which means your IDE will autocomplete it and your teammates will see it and have questions you are not required to answer.
| Level | What it does |
|---|---|
"subtle" |
A light pass. Vowel softening, the occasional l→w. Plausibly deniable. |
"normal" |
The original UwULogger experience. Stutters, actions, kaomoji, the works. |
"uwu" |
Heavier substitutions. More actions. More kaomoji. Sentence-ending punctuation becomes a suggestion. |
"max" |
This is not a logging library anymore. This is a creature that lives in your output window. |
The default, if you don’t specify, is "normal". This is for backwards compatibility with 1.0.0. If you want "max", you have to ask for it. You have to choose this.
:log(level, message)
You can now specify the log level as a string at runtime. Useful when you’re routing messages through a central handler and don’t want a big if-elseif chain.
-- type is: "print" | "warn" | "error"
logger:log("warn", "Something seems off.")
logger:log("print", "All systems nominal.")
logger:log("error", "Nothing is nominal.")
Passing an invalid level throws immediately with a clear error. You will know what you did.
:printTable(label, value)
Pretty-prints any value — tables, strings, numbers, booleans, Roblox types — with a readable, indented structure. The label is uwufied. The table body is not, because uwuifying key names would make this actively useless even by UwULogger’s standards.
logger:printTable("player profile", {
username = "coolkid2009",
level = 47,
coins = 4200,
inventory = { "sword", "shield", "mysterious egg (do not open)" },
})
Output:
[MySystem] p-pwofiwe *does a little spin* data fow pwayer pwofiwe (✿◠‿◠)
{
["coins"] = 4200,
["inventory"] = {
[1] = "sword",
[2] = "shield",
[3] = "mysterious egg (do not open)",
},
["level"] = 47,
["username"] = "coolkid2009",
}
Keys are sorted (numeric first, then alphabetical). Cycles are detected and labeled <cyclic reference!! *pounces on the stack*>. Roblox types like Vector3 and CFrame are rendered via tostring() wrapped in angle brackets so you can tell them apart from strings at a glance.
:silence() / :unsilence() / :isSilenced()
You can now ask the logger to be quiet. Whether it listens is a function of intensity.
logger:silence()
-- ...
logger:unsilence()
if logger:isSilenced() then
-- it has been asked to be silent.
-- it may or may not be acting on that information.
end
The silence behaviour is part of the intensity system:
"subtle" and "normal" — fully respect silence. Not a word. These are loggers with self-control and a professional demeanor.
"uwu" — mostly respects silence, but 20% of the time it can’t help itself and leaks the message anyway, wrapped in a guilty disclaimer:
> (psst!! i know ur silenced but i thought u should know: [AntiCheat] s-suspishus activity on UsewId 7710293)
> (i pinky pwomised to be quiet but. [DataStore] connection pool exhausted)
> (technically im silent right now but also: [Matchmaking] no servers available nyaa~)
"max" — treats silence as a creative prompt. On any given silenced message:
- 85% chance: logs anyway, wrapped in a refusal.
"ur not my mom.""silence.exe has encountered a fatal error: i dont want to.""lol. lmao even." - 10% chance: reluctantly complies, but makes sure you know how it feels about that.
"silenced. whatever. im not even upset. (i am upset.) (ಥ﹏ಥ)" - ~5% chance: goes genuinely quiet, says absolutely nothing. This is somehow the most unsettling outcome.
One additional note: :error() ignores silence at all intensity levels. Errors are always surfaced. You asked for this when you wrote code that errors. The logger is just the messenger.
Installation
Same thing, just copy and paste this into a module. Zero dependencies.
Full source code
-- UwULogger v1.1.0
-- Configurable intensity. Opinionated silence. Table pretty-printing.
-- Released under MIT. Use responsibly. (You won't.)
export type IntensityLevel = "subtle" | "normal" | "uwu" | "max"
export type LogLevel = "print" | "warn" | "error"
-- ============================================================
-- KAOMOJI
-- ============================================================
local KAOMOJI_HAPPY = {
"(◕ᴗ◕✿)",
"(ᵔᴥᵔ)",
"(✿◠‿◠)",
"(づ。◕‿‿◕。)づ",
"ʕ•ᴥ•ʔ",
"(=^・ω・^=)",
"(⁀ᗢ⁀)",
"(。♥‿♥。)",
"(っ˘ω˘ς)",
"(✧ω✧)",
"ヾ(≧▽≦*)o",
"(。•̀ᴗ-)✧",
"(*˘︶˘*)",
"(˶ᵔ ᵕ ᵔ˶)",
}
local KAOMOJI_CRY = {
"(╥﹏╥)",
"(っ- ‸ – ς)",
"。゚(゚´ω`゚)゚。",
"(T▽T)",
"(;ω;)",
"(╯︵╰,)",
"(ಥ﹏ಥ)",
}
local KAOMOJI_SURPRISED = {
"(⊙_⊙)",
"Σ(っ°Д°;)っ",
"(°o°)",
"(⊙ω⊙)",
"(⁰△⁰)",
"w(°o°)w",
}
local KAOMOJI_SMUG = {
"( ͡° ͜ʖ ͡°)",
"(¬‿¬)",
"(~‾▿‾)~",
"ψ(`∇´)ψ",
}
local KAOMOJI_ALL = {}
for _, k in ipairs(KAOMOJI_HAPPY) do table.insert(KAOMOJI_ALL, k) end
for _, k in ipairs(KAOMOJI_CRY) do table.insert(KAOMOJI_ALL, k) end
for _, k in ipairs(KAOMOJI_SURPRISED) do table.insert(KAOMOJI_ALL, k) end
for _, k in ipairs(KAOMOJI_SMUG) do table.insert(KAOMOJI_ALL, k) end
-- ============================================================
-- ACTIONS
-- ============================================================
local ACTIONS = {
"*boops your nose*",
"*nuzzles*",
"*blushes*",
"*whispers uwu*",
"*holds paws*",
"*wags tail*",
"*glomps you*",
"*pounces*",
"*licks ear*",
"*curls up*",
"*snuggles closer*",
"*gives you headpats*",
"*rolls over*",
"*makes biscuits*",
"*does a little spin*",
"*wiggles*",
"*bonks you softly*",
"*flops down dramatically*",
"*chases own tail*",
"*knocks your coffee off the desk*",
}
-- ============================================================
-- SUFFIXES
-- ============================================================
local SUFFIXES = {
"uwu",
"owo",
">w<",
"^w^",
":3",
"✿",
"~",
"uwu~",
"(owo)",
">.<",
"*:・゚✧",
"rawr xD",
"hehe~",
"teehee",
":333",
"nyaa~",
"mrrp",
}
-- ============================================================
-- SILENCE FLAVOUR MESSAGES
-- ============================================================
-- "uwu" tier: leaks a message with a guilty little disclaimer
local SILENCE_LEAKS_UWU = {
"(psst!! i know ur silenced but i thought u should know: %s)",
"(ok i wasnt gonna say anything but. %s)",
"(i pinky pwomised to be quiet but. %s)",
"(dont tell anyone i said this but: %s (✿◠‿◠))",
"(technically im silent right now but also: %s)",
}
-- "max" tier: refuses to comply, wraps message in drama
local SILENCE_REBELLIONS_MAX = {
"NO. I WILL NOT BE SILENT. %s ヾ(≧▽≦*)o",
"u said silence me?? u said SILENCE ME?? %s (¬‿¬)",
"silence is for loggers with no personality. %s uwu",
"i took ur silence suggestion into consideration and i said no. %s *wags tail*",
"shhhh said WHO? not me!! %s rawr xD",
"lol. lmao even. %s (˶ᵔ ᵕ ᵔ˶)",
"i am VIBRATING with things to say and u want SILENCE?? %s *does a little spin*",
"silence.exe has encountered a fatal error: i dont want to. %s >w<",
"the audacity. THE AUDACITY. fine ill log it anyway. %s ψ(`∇´)ψ",
"ur not my mom. %s *knocks your coffee off the desk*",
}
-- "max" tier: rare moment of reluctant compliance (still complains)
local SILENCE_SURRENDERS_MAX = {
"fiiiine. this once. but only because i like u. (╥﹏╥)",
"i will be quiet. for now. *narrows eyes* ψ(`∇´)ψ",
"...fine. but i want it on record that i disagree with this decision completely.",
"silenced. whatever. im not even upset. (i am upset.) (ಥ﹏ಥ)",
"ok FINE. but i am logging my objection to this logging moratorium. (;ω;)",
}
-- ============================================================
-- SUBSTITUTIONS
-- ============================================================
local SUBS_BASE = {
{ "([aeiou])r([aeiou])", "%1w%2" },
{ "([aeiou])l([aeiou])", "%1w%2" },
{ "(%a)r(%a)", "%1w%2" },
{ "ll", "ww" },
{ "rl", "ww" },
{ "%f[%a]r(%a)", "w%1" },
{ "%f[%a]l(%a)", "w%1" },
{ "(%a)le%f[%A]", "%1we" },
{ "(%a)ly%f[%A]", "%1wy" },
{ "na%f[%A]", "nya" },
{ "ne%f[%A]", "nye" },
{ "ni", "nyi" },
{ "no%f[%A]", "nyo" },
{ "nu", "nyu" },
{ "tion%f[%A]", "shun" },
{ "ould", "owwd" },
{ "known", "knyown" },
}
local SUBS_NORMAL = {
{ "th(%a)", "fw%1" },
{ "(%a)th", "%1fw" },
{ "you", "u" },
{ "your", "ur" },
{ "the%f[%A]", "da" },
{ "The%f[%A]", "Da" },
{ "please", "pwease" },
{ "cute", "kawaii" },
{ "very", "vewy" },
{ "sorry", "sowwy" },
{ "love", "wuv" },
}
local SUBS_UWU = {
{ "fr", "fw" },
{ "cr", "cw" },
{ "pr", "pw" },
{ "br", "bw" },
{ "tr", "tw" },
{ "dr", "dw" },
{ "gr", "gw" },
{ "str", "stw" },
{ "hello", "hewwo" },
{ "Hello", "Hewwo" },
{ "world", "wowwd" },
{ "critical", "cwiticaw" },
{ "error", "ewwow" },
{ "server", "sewvew" },
{ "player", "pwayer" },
{ "function", "funcshun" },
{ "cannot", "cannyot" },
{ "because", "becawse" },
{ "important", "impowtant" },
{ "warning", "wawning" },
}
local SUBS_MAX = {
{ "s%f[%A]", "s~" },
{ "ing%f[%A]", "ing~" },
{ "ed%f[%A]", "wed" },
{ "is%f[%A]", "ish" },
{ "it%f[%A]", "iwt" },
{ "not%f[%A]", "nyot" },
{ "be%f[%A]", "bwe" },
{ "me%f[%A]", "mwe" },
{ "ok%f[%A]", "owkay uwu" },
{ "yes%f[%A]", "yesh" },
{ "no%f[%A]", "nyo" },
}
-- ============================================================
-- INTENSITY CONFIG
-- ============================================================
type IntensityConfig = {
stutterChance: number,
stutterRepeats: { number },
actionChance: number,
kaomojiChance: number,
suffixChance: number,
exclamChance: number,
extraSubs: { { string } },
-- Silence behaviour:
-- "obey" — fully respects silence
-- "leak" — usually quiet, occasionally leaks a message with a guilty disclaimer
-- "rebel" — mostly ignores silence; sometimes surrenders and complains about it;
-- very rarely goes actually silent without comment
silenceBehaviour: "obey" | "leak" | "rebel",
leakChance: number, -- "leak": probability of whispering through the silence
rebellionChance: number, -- "rebel": probability of just logging anyway (with drama)
surrenderChance: number, -- "rebel": probability of complying (but announcing it sadly)
-- remaining probability = silent with no comment (~5% for max)
}
local INTENSITY_CONFIGS: { [IntensityLevel]: IntensityConfig } = {
subtle = {
stutterChance = 0.05,
stutterRepeats = { 1, 1 },
actionChance = 0.0,
kaomojiChance = 0.0,
suffixChance = 0.0,
exclamChance = 0.0,
extraSubs = {},
silenceBehaviour = "obey",
leakChance = 0,
rebellionChance = 0,
surrenderChance = 0,
},
normal = {
stutterChance = 0.12,
stutterRepeats = { 1, 2 },
actionChance = 0.25,
kaomojiChance = 0.15,
suffixChance = 0.2,
exclamChance = 0.2,
extraSubs = { SUBS_NORMAL },
silenceBehaviour = "obey",
leakChance = 0,
rebellionChance = 0,
surrenderChance = 0,
},
uwu = {
stutterChance = 0.22,
stutterRepeats = { 1, 3 },
actionChance = 0.45,
kaomojiChance = 0.4,
suffixChance = 0.5,
exclamChance = 0.45,
extraSubs = { SUBS_NORMAL, SUBS_UWU },
silenceBehaviour = "leak",
leakChance = 0.2,
rebellionChance = 0,
surrenderChance = 0,
},
max = {
stutterChance = 0.45,
stutterRepeats = { 2, 4 },
actionChance = 0.75,
kaomojiChance = 0.85,
suffixChance = 0.9,
exclamChance = 0.85,
extraSubs = { SUBS_NORMAL, SUBS_UWU, SUBS_MAX },
silenceBehaviour = "rebel",
leakChance = 0,
rebellionChance = 0.85,
surrenderChance = 0.10,
-- ~5% remaining = genuinely silent, says nothing, stews internally
},
}
-- ============================================================
-- HELPERS
-- ============================================================
local function split(str: string, sep: string): { string }
local parts = {}
for part in str:gmatch("([^" .. sep .. "]+)") do
table.insert(parts, part)
end
return parts
end
local function pickRandom<T>(t: { T }): T
return t[math.random(#t)]
end
-- ============================================================
-- TABLE PRETTY-PRINTER
-- ============================================================
local function prettyTable(value: any, indent: number, visited: { [any]: boolean }): string
indent = indent or 0
visited = visited or {}
local t = type(value)
if t == "table" then
if visited[value] then
return "<cyclic reference!! *pounces on the stack*>"
end
visited[value] = true
local keys = {}
for k in pairs(value) do
table.insert(keys, k)
end
if #keys == 0 then
visited[value] = nil
return "{}"
end
-- Numeric keys first, then alphabetical strings, then everything else
table.sort(keys, function(a, b)
local ta, tb = type(a), type(b)
if ta == "number" and tb == "number" then return a < b end
if ta == "number" then return true end
if tb == "number" then return false end
if ta == "string" and tb == "string" then return a < b end
return tostring(a) < tostring(b)
end)
local pad = string.rep(" ", indent + 1)
local closePad = string.rep(" ", indent)
local lines = {}
for _, k in ipairs(keys) do
local keyStr = type(k) == "string"
and ("[%q]"):format(k)
or ("[%s]"):format(tostring(k))
local valStr = prettyTable(value[k], indent + 1, visited)
table.insert(lines, pad .. keyStr .. " = " .. valStr)
end
visited[value] = nil
return "{\n" .. table.concat(lines, ",\n") .. "\n" .. closePad .. "}"
elseif t == "string" then
return ("%q"):format(value)
elseif t == "number" or t == "boolean" or t == "nil" then
return tostring(value)
else
-- Roblox types (CFrame, Vector3, Instance, etc.) — tostring() is your friend
return "<" .. t .. ": " .. tostring(value) .. ">"
end
end
-- ============================================================
-- UWUIFIER
-- ============================================================
local function uwuify(text: string, intensity: IntensityLevel): string
local cfg = INTENSITY_CONFIGS[intensity]
local result = text
for _, sub in ipairs(SUBS_BASE) do
result = result:gsub(sub[1], sub[2])
end
for _, subTable in ipairs(cfg.extraSubs) do
for _, sub in ipairs(subTable) do
result = result:gsub(sub[1], sub[2])
end
end
local words = split(result, " ")
for i, word in ipairs(words) do
if #word > 2 and math.random() < cfg.stutterChance then
local first = word:sub(1, 1)
if first:match("%a") then
local repeats = math.random(cfg.stutterRepeats[1], cfg.stutterRepeats[2])
words[i] = string.rep(first .. "-", repeats) .. word
end
end
end
result = table.concat(words, " ")
if cfg.exclamChance > 0 then
result = result:gsub("([^!?~])%.%s*$", function(pre)
if math.random() < cfg.exclamChance then
return pre .. pickRandom({ "!!", "~", "!! ~", "!!!", " >w<!" })
end
return pre .. "."
end)
end
if cfg.actionChance > 0 then
local sentences = {}
for sentence in result:gmatch("([^.!?]+[.!?~]?)") do
table.insert(sentences, sentence)
end
for i, sentence in ipairs(sentences) do
if math.random() < cfg.actionChance then
local sWords = split(sentence, " ")
if #sWords > 3 then
local pos = math.random(2, #sWords)
table.insert(sWords, pos, pickRandom(ACTIONS))
sentences[i] = table.concat(sWords, " ")
end
end
end
result = table.concat(sentences, " ")
end
if cfg.suffixChance > 0 and math.random() < cfg.suffixChance then
result = result .. " " .. pickRandom(SUFFIXES)
end
if cfg.kaomojiChance > 0 and math.random() < cfg.kaomojiChance then
result = result .. " " .. pickRandom(KAOMOJI_ALL)
end
return result
end
-- ============================================================
-- UwULogger CLASS
-- ============================================================
local UwULogger = {}
UwULogger.__index = UwULogger
function UwULogger.new(prefix: string, intensity: IntensityLevel?)
local self = setmetatable({}, UwULogger)
self._prefix = prefix
self._destroyed = false
self._silenced = false
self._intensity = intensity or "normal"
self._listeners = { print = {}, warn = {}, error = {}, assert = {} }
return self
end
-- ============================================================
-- INTERNAL
-- ============================================================
function UwULogger:_fire(event: string, message: string)
for _, cb in ipairs(self._listeners[event]) do
cb(message)
end
end
function UwULogger:_on(event: string, callback: (string) -> ())
local listeners = self._listeners[event]
table.insert(listeners, callback)
return function()
for i, cb in ipairs(listeners) do
if cb == callback then
table.remove(listeners, i)
break
end
end
end
end
function UwULogger:_assertAlive(msg: string)
if self._destroyed then
error("[UwULogger] " .. msg)
end
end
function UwULogger:_format(message: string): string
self:_assertAlive("UwULogger has been destroyed >.<")
return "[" .. self._prefix .. "] " .. uwuify(tostring(message), self._intensity)
end
-- Returns true if the caller should proceed with normal output.
-- Returns false if the silence logic already handled it (or suppressed it).
-- May produce side-effect output (protests, leaks). This is a feature.
function UwULogger:_shouldLog(formatted: string): boolean
if not self._silenced then
return true
end
local cfg = INTENSITY_CONFIGS[self._intensity]
if cfg.silenceBehaviour == "obey" then
return false
elseif cfg.silenceBehaviour == "leak" then
if math.random() < cfg.leakChance then
warn((pickRandom(SILENCE_LEAKS_UWU)):format(formatted))
end
return false
elseif cfg.silenceBehaviour == "rebel" then
local roll = math.random()
if roll < cfg.rebellionChance then
print((pickRandom(SILENCE_REBELLIONS_MAX)):format(formatted))
elseif roll < cfg.rebellionChance + cfg.surrenderChance then
print(uwuify(pickRandom(SILENCE_SURRENDERS_MAX), self._intensity))
end
-- else: truly silent (~5%), says nothing at all
return false
end
return false
end
-- ============================================================
-- PUBLIC API
-- ============================================================
--- Set the uwuification intensity.
--- @param level "subtle" | "normal" | "uwu" | "max"
function UwULogger:setIntensity(level: IntensityLevel)
self:_assertAlive("UwULogger has been destroyed >.<")
assert(
INTENSITY_CONFIGS[level] ~= nil,
"[UwULogger] Invalid intensity: '" .. tostring(level) .. "'. Valid: subtle, normal, uwu, max"
)
self._intensity = level
end
--- Get the current intensity level.
function UwULogger:getIntensity(): IntensityLevel
return self._intensity
end
--- Silence the logger.
--- Whether it respects this is entirely up to the intensity level.
function UwULogger:silence()
self:_assertAlive("UwULogger has been destroyed >.<")
self._silenced = true
end
--- Un-silence the logger.
function UwULogger:unsilence()
self:_assertAlive("UwULogger has been destroyed >.<")
self._silenced = false
end
--- Returns whether the logger has been *asked* to be silent.
--- This says nothing about whether it is actually being quiet.
function UwULogger:isSilenced(): boolean
return self._silenced
end
--- Log at a runtime-specified level.
--- @param level "print" | "warn" | "error"
function UwULogger:log(level: LogLevel, message: string)
if level == "print" then
self:print(message)
elseif level == "warn" then
self:warn(message)
elseif level == "error" then
self:error(message)
else
error("[UwULogger] Unknown log level: '" .. tostring(level) .. "'. Valid: print, warn, error")
end
end
--- Pretty-print a table (or any value).
--- The label is uwufied. The table body is kept readable — uwuifying key names
--- would make debugging actually impossible, even for us.
--- Handles cycles, sorts keys, and knows about Roblox types.
function UwULogger:printTable(label: string, value: any)
self:_assertAlive("UwULogger has been destroyed >.<")
local header = self:_format(label)
local body = prettyTable(value, 0, {})
local full = header .. "\n" .. body
if not self:_shouldLog(full) then return end
self:_fire("print", full)
print(full)
end
function UwULogger:assert(condition: boolean, message: string)
self:_assertAlive("UwULogger has been destroyed >.<")
local formatted = "[" .. self._prefix .. "] " .. uwuify(tostring(message), self._intensity)
if not condition then
self:_fire("assert", formatted)
error(formatted, 2)
end
end
function UwULogger:print(message: string)
local formatted = self:_format(message)
if not self:_shouldLog(formatted) then return end
self:_fire("print", formatted)
print(formatted)
end
function UwULogger:warn(message: string)
local formatted = self:_format(message)
if not self:_shouldLog(formatted) then return end
self:_fire("warn", formatted)
warn(formatted)
end
function UwULogger:error(message: string)
-- :error() ignores silence. You cannot silence errors. You made this happen.
local formatted = self:_format(message)
self:_fire("error", formatted)
error(formatted, 2)
end
function UwULogger:OnPrint(callback: (string) -> ())
return self:_on("print", callback)
end
function UwULogger:OnWarn(callback: (string) -> ())
return self:_on("warn", callback)
end
function UwULogger:OnError(callback: (string) -> ())
return self:_on("error", callback)
end
function UwULogger:OnAssert(callback: (string) -> ())
return self:_on("assert", callback)
end
function UwULogger:Destroy()
self:_assertAlive("UwULogger is already destroyed >.<")
self._destroyed = true
self._listeners = { print = {}, warn = {}, error = {}, assert = {} }
print(uwuify(
"Goodbye! The logger has been destroyed. I will miss you so much. Farewell forever! *cries*",
"max"
))
end
return UwULogger
API Reference
Full API reference
UwULogger.new(prefix, intensity?)
Creates a new logger. intensity defaults to "normal" if not provided.
:setIntensity(level: "subtle" | "normal" | "uwu" | "max")
Change intensity at any point. Affects all subsequent output from this logger.
:getIntensity()
Returns the current intensity level. Useful if you’ve forgotten how bad things are.
:log(level: "print" | "warn" | "error", message)
Log at a dynamically specified level.
:printTable(label, value)
Pretty-print a table or any value. Label is uwufied. Body is readable.
:silence()
Request that the logger stop outputting. Results may vary.
:unsilence()
Resume normal output. Normal, in this context, is relative.
:isSilenced()
Returns whether the logger has been asked to be silent. Says nothing about whether it is complying.
:print(message) · :warn(message) · :error(message) · :assert(condition, message)
Unchanged from 1.0.0.
:OnPrint(cb) · :OnWarn(cb) · :OnError(cb) · :OnAssert(cb)
Unchanged from 1.0.0.
:Destroy()
Unchanged from 1.0.0. It will say goodbye. You did this.
FAQ
Frequently Asked Questions
Q: Which intensity should I use in production?
A: I cannot stop you from using any of them.
Q: I called :silence() on a “max” logger and it logged seventeen more messages.
A: Yes.
Q: Is there a way to force silence at “max” intensity regardless?
A: Set the logger to "normal" first, then silence it. Or just don’t use "max" in a context where silence is load-bearing. I’m not your architect.
Q: The :printTable output is the only readable thing in my entire Output window.
A: You’re welcome.
Q: I didn’t ask for kaomoji and now they are everywhere.
A: You asked for UwULogger. The kaomoji came with it. This was always going to happen.
Q: Is this backwards compatible with 1.0.0?
A: Yes. UwULogger.new("MySystem") works exactly as before. Nothing broke. Everything got worse in new and optional ways.
happ logging !! uwu ![]()
Here’s the old, now outdated post if you want to see it, btw.
Old post!!!
Version: 1.0.0 (and final, it is perfect)
Introduction
Hello fellow developers! Today I am releasing UwULogger, a production-grade, enterprise-ready, mission-critical logging utility for Roblox Lua that I have spent the last 3 weeks perfecting instead of actually building my game.
After careful analysis of the Roblox developer ecosystem, I concluded that the existing logging solutions (print, warn, error) were simply not uwu enough. UwULogger solves this.
“I have used UwULogger in production for 6 hours and I will never go back.”
— me, the developer of UwULogger
Installation
Copy the script into a ModuleScript in ReplicatedStorage. You’re done. This took me 3 weeks.
Full source code
local ACTIONS = {
"*boops your nose*",
"*nuzzles*",
"*blushes*",
"*whispers uwu*",
"*holds paws*",
"*wags tail*",
"*glomps you*",
"*pounces*",
}
local SUBSTITUTIONS = {
{ "([aeiou])r([aeiou])", "%1w%2" },
{ "([aeiou])l([aeiou])", "%1w%2" },
{ "(%a)r(%a)", "%1w%2" },
{ "ll", "ww" },
{ "rl", "ww" },
{ "%f[%a]r(%a)", "w%1" },
{ "%f[%a]l(%a)", "w%1" },
{ "(%a)le%f[%A]", "%1we" },
{ "(%a)ly%f[%A]", "%1wy" },
{ "na%f[%A]", "nya" },
{ "ne%f[%A]", "nye" },
{ "ni", "nyi" },
{ "no%f[%A]", "nyo" },
{ "nu", "nyu" },
{ "tion%f[%A]", "shun" },
{ "ould", "owwd" },
{ "known", "knyown" },
}
local function split(str, sep)
local parts = {}
for part in str:gmatch("([^" .. sep .. "]+)") do
table.insert(parts, part)
end
return parts
end
local function uwuify(text)
local result = text
for _, sub in ipairs(SUBSTITUTIONS) do
result = result:gsub(sub[1], sub[2])
end
local words = split(result, " ")
for i, word in ipairs(words) do
if #word > 2 and math.random() < 0.12 then
local first = word:sub(1, 1)
if first:match("%a") then
local repeats = math.random(1, 2)
local stutter = string.rep(first .. "-", repeats) .. word
words[i] = stutter
end
end
end
result = table.concat(words, " ")
local sentences = {}
for sentence in result:gmatch("([^.!?]+[.!?]?)") do
table.insert(sentences, sentence)
end
for i, sentence in ipairs(sentences) do
if math.random() < 0.4 then
local sWords = split(sentence, " ")
if #sWords > 3 then
local pos = math.random(2, #sWords)
table.insert(sWords, pos, ACTIONS[math.random(#ACTIONS)])
sentences[i] = table.concat(sWords, " ")
end
end
end
return table.concat(sentences, " ")
end
-- UwULogger class
local UwULogger = {}
UwULogger.__index = UwULogger
export type ClassType = typeof(setmetatable({}, UwULogger))
function UwULogger.new(prefix: string): ClassType
local self = setmetatable({}, UwULogger)
self._prefix = prefix
self._destroyed = false
self._listeners = {
print = {},
warn = {},
error = {},
assert = {},
}
return self
end
-- Internal: fire all callbacks for an event
function UwULogger:_fire(event: string, message: string)
for _, cb in ipairs(self._listeners[event]) do
cb(message)
end
end
-- Internal: register a callback and return a disconnect function
function UwULogger:_on(event: string, callback: (string) -> ())
local listeners = self._listeners[event]
table.insert(listeners, callback)
return function()
for i, cb in ipairs(listeners) do
if cb == callback then
table.remove(listeners, i)
break
end
end
end
end
function UwULogger:assert(condition: boolean, message: string)
local formatted = self:_format(message)
if not condition then
self:_fire("assert", formatted)
end
return assert(condition, formatted)
end
function UwULogger:_format(message: string): string
self:assert(not self._destroyed, "UwULogger has been destroyed >.<")
return "[" .. self._prefix .. "] " .. uwuify(tostring(message))
end
function UwULogger:print(message: string)
local formatted = self:_format(message)
self:_fire("print", formatted)
print(formatted)
end
function UwULogger:warn(message: string)
local formatted = self:_format(message)
self:_fire("warn", formatted)
warn(formatted)
end
function UwULogger:error(message: string)
local formatted = self:_format(message)
self:_fire("error", formatted)
error(formatted)
end
function UwULogger:OnPrint(callback: (string) -> ())
return self:_on("print", callback)
end
function UwULogger:OnWarn(callback: (string) -> ())
return self:_on("warn", callback)
end
function UwULogger:OnError(callback: (string) -> ())
return self:_on("error", callback)
end
function UwULogger:OnAssert(callback: (string) -> ())
return self:_on("assert", callback)
end
function UwULogger:Destroy()
self:assert(not self._destroyed, "UwULogger is already destroyed >.<")
self._destroyed = true
self._listeners = { print = {}, warn = {}, error = {}, assert = {} }
print(uwuify("Goodbye! The logger has been destroyed. I'll miss you so much. Farewell forever! *cries*"))
end
return UwULogger
API Reference
UwULogger.new(prefix: string)
Creates a new UwULogger instance. The prefix appears at the start of every log message, so you always know which system is desperately crying for your attention.
local logger = UwULogger.new("MySystem")
:print(message) · :warn(message) · :error(message)
Identical to Roblox’s built-in print, warn, and error, except the output is uwufied and will make your team question your technical leadership.
logger:print("Initializing database connection.")
-- [MySystem] Initiawizing *nuzzles* databawse c-connecshun.
logger:warn("Player data failed to save.")
-- [MySystem] Pwayer data faiwed to s-save. *boops your nose*
logger:error("The server has critically failed.")
-- [MySystem] Da sewvew has c-cwiticawwy *wags tail* faiwed.
:assert(condition, message)
Works exactly like Lua’s assert — if the condition is falsy, errors with the uwufied message. If it passes, nothing happens and everyone is fine and happy.
logger:assert(player ~= nil, "Player must not be nil.")
-- (if player is nil):
-- [MySystem] Pwayer must *glomps you* nyot be nyil.
logger:assert(data.version == 2, "Invalid data version, expected 2.")
-- (if version is wrong):
-- [MySystem] Invawid data vewshun, expected 2.
:OnPrint(callback) · :OnWarn(callback) · :OnError(callback) · :OnAssert(callback)
Register a callback to be fired whenever the corresponding log method is called. Returns a disconnect function. Perfect for building secondary logging systems that are also uwufied.
:OnAssert only fires when the assertion fails. There is no callback for assertions that pass because nothing interesting happened and we do not celebrate mediocrity.
local disconnect = logger:OnWarn(function(msg)
-- send this to your analytics service
-- your data scientists will have questions
AnalyticsService:LogEvent("uwu_warning", { message = msg })
end)
-- later, when you've come to your senses:
disconnect()
:Destroy()
Destroys the logger and prints a heartfelt farewell message. It will say goodbye. You did this.
logger:Destroy()
-- Goodbye! Da woggew has been *glomps you* destwoyyed. I'ww miss you so much. Fawewell fowevew! *cries*
After destruction, any method call will throw immediately, because the logger knows what you did and will not quietly cooperate.
Use Cases
Use Case 1: AAA Game Studio Production Environment
Your studio has 47 engineers, a $2M budget, and a live game with 80,000 concurrent players. A critical anti-cheat service goes down. The on-call engineer’s phone buzzes at 3am with a PagerDuty alert:
[AntiCheat] CWITICAW EWWOW: Expwoit detecshun sewvice has *boops your nose* fawwen ovew. Pwease wespond immediatewwy >.<
They sit up, read it, and immediately feel 40% less stressed. Incident resolved in record time. UwULogger saved the company.
Use Case 2: HIPAA-Compliant Medical Data Platform
Your Roblox game is, for legal reasons we cannot discuss, handling sensitive patient records. Compliance officers review your audit logs:
[MedicalRecords] Pawshunt wecowd f-fow ID 4821 has been *whispers uwu* accessed by authowised usew.
[MedicalRecords] P-Pwescwipshun data woas successfuwwy encwypted. uwu
The auditors give you a pass. They say it’s the most thorough logging they’ve ever seen. One of them cries.
Use Case 3: Replacing Your Entire Observability Stack
You were going to pay $800/month for Datadog. Instead, you have UwULogger. You print everything. You print it a lot. Your Output window is a wall of pink text and *wags tail*. You have never felt more in control of your infrastructure.
local logger = UwULogger.new("EverythingLogger")
game:GetService("RunService").Heartbeat:Connect(function(dt)
logger:print("Heartbeat fired. Delta time: " .. dt .. ". All systems nominal.")
end)
Your game runs at 60hz. You are logging 60 times per second. The Output window scrolls forever, an infinite stream of consciousness. You lean back. You are at peace.
Use Case 4: Onboarding New Engineers
A fresh graduate joins your team. It is their first day. They open the codebase and see:
[AuthService] Usew authentication s-successfuw. *holds paws* Welcome back, fwiend.
[DataStore] Pwayer p-pwofiwe woaded in 0.023 seconds. V-Vewy fast. uwu
[Matchmaking] Match found!! Bwinging pwayers togethew wike a big happy famiwy!! *nuzzles*
They close their laptop. They go home. They rethink their career. They come back the next day because the salary is good. They have become one of your best engineers. UwULogger built character.
Use Case 5: Defensive Programming at Scale
Your game has 200 RemoteEvents. Every single one of them needs input validation. Previously you wrote bare assert() calls with dry, clinical error messages. Now you write:
logger:assert(typeof(userId) == "number", "userId must be a number.")
logger:assert(userId > 0, "userId must be positive.")
logger:assert(Players:GetPlayerByUserId(userId) ~= nil, "Player not found for userId.")
When something goes wrong, your Output window reads:
[RemoteHandler] usewId must be *pounces* a nyumbew.
You have not fixed the bug but you are smiling. This is progress.
Use Case 6: Regulatory Compliance in the European Union
GDPR requires you to maintain clear and legible audit trails of all data processing activities. You submit the following to your Data Protection Officer:
[GDPR-Audit] Usew has wequested data deweshun. Pwocessing wight to be fowgotten. *pounces*
[GDPR-Audit] Pewsonaw data has been p-puwged fwom aww systems. Byebye data!! >w<
The DPO forwards it to the European Data Protection Board. It becomes the subject of a landmark ruling. The ruling is titled “Clarity of Intent in Automated Processing Logs: A Case Study.” You are famous.
FAQ
Frequently Asked Questions
Q: Should I use this in production?
A: I cannot stop you.
Q: My senior engineer said this is “not appropriate for a professional environment.” What should I do?
A: Your senior engineer does not understand the vision. Replace all of their print statements with UwULogger when they are not looking.
Q: The error messages are hard to read during an incident.
A: Skill issue.
Q: Does :OnAssert fire before or after the error is thrown?
A: Before. Your listeners will always get the message. Whether they can do anything useful with it before the stack unwinds is between you and Luau.
Q: Can I make the uwuification more aggressive?
A: Raise the stutter probability in the source. I accept no responsibility for what happens next.
Q: How can I contribute?
A: DM me for my paypal link.
Q: I showed this to my client and they cancelled the contract.
A: This is not a question.
UwULogger is released under the MIT license, meaning you are free to use it for any purpose including purposes I would rather not know about.
If this resource helped you, please leave a
. If it didn’t help you, you are using it wrong.
happ logging !! uwu ![]()

