UwULogger v1.1.0 - intensity levels, table printing, and a logger that refuses to be silenced

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 :cherry_blossom:

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 :+1:. If it didn’t help you, you are using it wrong.

happ logging !! uwu :cherry_blossom:

14 Likes

POV: you drank too much and decided to go on the devforum

12 Likes

Right so what about printing the beloved CFrames?

1 Like

POV: you watch too much romantic movies and posted this on the devforum

1 Like

this is so cancer.

bookmarked it.

1 Like

My friend Epstein Died from ts :sob:

This is so sigma, I’ll definitely use this for all my games

1 Like

what is this abomination i see


Just by looking at it, everything seems to be working.

1 Like

Interesting idea. I’ll note that down for when I work on this again in the next few months

Send game link so I can bot it

This also works as anti-cheat since no (normal) hacker will want to deal with this

4 Likes

UwULogger v1.1.0 out now!! Sooner than expected (3 years)!!

1 Like

Dissappointing that it doesn’t support it by default. I’ll make sure to leave a negative review!

2 Likes