diff --git a/.gitmodules b/.gitmodules
index a08c951d775c733d157e782cebe3568af47a0e8e..edbd4839edd6b6ab71197dea705862cf485d7f73 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -10,3 +10,6 @@
 [submodule "module/lily"]
 	path = module/lily
 	url = https://github.com/gphg/lily.git
+[submodule "module/log"]
+	path = module/log
+	url = https://github.com/gphg/log.lua.git
diff --git a/game/init.lua b/game/init.lua
index 22909873dae81cab5f8965673f8df779bc8d165a..ac54346b7a302ba76647498d8ed3695342143b49 100644
--- a/game/init.lua
+++ b/game/init.lua
@@ -22,6 +22,7 @@ Game.util     = require 'game.util' -- must be loaded first!
 Game.base     = require 'game.base'
 Game.display  = require 'game.display'
 Game.handler  = require 'game.handler'
+Game.logger   = require 'game.logger'
 Game.manager  = require 'game.manager'
 Game.overlay  = require 'game.overlay'
 Game.pool     = require 'game.pool'
diff --git a/game/logger.lua b/game/logger.lua
new file mode 100644
index 0000000000000000000000000000000000000000..66b74dd725f39a49342cb421a81ea2ec02691a63
--- /dev/null
+++ b/game/logger.lua
@@ -0,0 +1,100 @@
+---@class Logger: Super
+---@overload fun(label?: string, logfile?: string, basepath?: string): self
+local Logger = require 'game.base':extend({}, ...)
+
+Logger.basepath = '' -- either empty or string with trailling slash
+Logger.outfile = 'latest.log'
+Logger._instances = {}
+Logger.debugthread = 3
+Logger.level = 'trace'
+
+---@alias LoggerMode
+---| 'trace'
+---| 'debug'
+---| 'info'
+---| 'warn'
+---| 'error'
+---| 'fatal'
+
+local love, table, Util = love, table, require 'util'
+local filesystem, loggerModule = love.filesystem, Util.log
+local rawget, rawset, ipairs = rawget, rawset, ipairs
+
+---@param label? string
+---@param outfile? string
+---@param basepath? string
+function Logger:constructor(label, outfile, basepath)
+	self.label = label
+	self.outfile = outfile
+	self.basepath = basepath
+
+	local dict = Logger._instances
+	rawset(dict, #dict + 1, self)
+end
+
+---@param lmode LoggerMode|string
+---@return self
+---@return string? formattedString
+function Logger:log(lmode, ...)
+	local mode = loggerModule[lmode]
+
+	if not Util.isCallable(mode) then
+		return self
+	end
+
+	loggerModule.level = self.level
+	loggerModule.debugthread = self.debugthread
+	local str = mode(...)
+	loggerModule.debugthread = 2
+
+	if not str or str == '' then
+		return self
+	end
+
+	rawset(self, #self + 1, str)
+	return self, str
+end
+
+---@param outfile string?
+function Logger:flush(outfile)
+	outfile = outfile or self.outfile
+	local logpath = self.basepath .. outfile
+
+	if self == Logger then
+		self:log('debug', 'Detected as Logger class instead an instance.')
+			:log('debug', 'Flushing all registered instances.')
+
+		local dict = Logger._instances
+		for _, obj in ipairs(dict) do
+			---@cast obj Logger
+			local n, s = #obj, obj.label or tostring(obj)
+			Logger.flush(obj, obj.outfile or outfile)
+			if n > 0 then
+				self:log('debug', 'Flushed', s)
+			end
+		end
+		-- Flushed the external module too
+		loggerModule.flush()
+	end
+
+	for i = 1, #self do
+		filesystem.append(logpath, rawget(self, i))
+		rawset(self, i, nil)
+	end
+
+	self:log('debug', 'Logs saved on', logpath)
+end
+
+---Release self and unregestered self from Logger instance table.
+---Does not deference self!
+function Logger:release()
+	self:flush()
+	local dict = Logger._instances
+	for i = #dict, 1, -1 do
+		table.remove(dict, i)
+	end
+end
+
+loggerModule.outfile = loggerModule.outfile or Logger.outfile
+
+return Logger
diff --git a/game/scene/boot.lua b/game/scene/boot.lua
index 7241bb1416a4f4aa1723adbd469b1fccf70e9880..a51fdecda615aaceeda508b36c5d6a205fb0cbba 100644
--- a/game/scene/boot.lua
+++ b/game/scene/boot.lua
@@ -1,13 +1,24 @@
 ---Boot is supposed to be called on love.load
+---@class BootScene: Scene
 local boot = require 'game.scene':extend({}, ...)
 
-local Timer, meta = require 'game.ticker', require 'meta'
-
-boot.handler = require 'game.handler' ()
+local Game, meta = require 'game', require 'meta'
+local Timer, Handler, Logger = Game.ticker, Game.handler, Game.logger
 
 ---@param _ Scene?
 ---@param tobeloaded string
 function boot:load(_, tobeloaded, ...)
+	--- Expectation: trace to debug is not available on release
+	local loggerLevel = os.getenv('RUNTIME_LOG_LEVEL')
+	Logger.level = loggerLevel
+		or not package.loaded.lldebugger and 'info'
+		or Logger.level
+
+	Logger:log('info', '|', os.date(), '| Booting up...')
+
+	boot.handler = boot.handler or Handler()
+
+
 	local timeout = meta.timeout or 0
 	if timeout > 0 then
 		Timer:after(timeout, function() love.event.quit(0) end)
diff --git a/game/scene/title.lua b/game/scene/title.lua
index d54b8fe273f7e58741082ed39685d0344c59c68b..36b5f075055839dbb7f322482e980bbb718659d7 100644
--- a/game/scene/title.lua
+++ b/game/scene/title.lua
@@ -25,11 +25,8 @@ function title:load(...)
 
 	local fontHeight = self.Font:getHeight()
 
-	-- Please forgive me.
-	rawset(self, #self + 1, (function(parent)
-		local demo = love.filesystem.getInfo('demo')
-		if not demo or demo.type ~= 'directory' then return end
-
+	local demo = love.filesystem.getInfo('demo')
+	if demo and demo.type == 'directory' then
 		-- Insert simple sample entity (object). It simply loop over.
 		local demoT = require 'game.base' ()
 		function demoT:draw(i)
@@ -39,12 +36,12 @@ function title:load(...)
 
 		function demoT.keypressed(_, key)
 			if key == 'd' then
-				parent._manager:push('demo')
+				self._manager:push('demo')
 			end
 		end
 
-		return demoT
-	end)(self))
+		rawset(self, #self + 1, demoT)
+	end
 
 	---Push the loading screen
 	---@module "scene.loading"
diff --git a/game/util.lua b/game/util.lua
index 6fed7a5dc8ef405774dceb08dfb1bb0e2c227553..c19b7ee298401493403f548c20152e841a27bd10 100644
--- a/game/util.lua
+++ b/game/util.lua
@@ -24,7 +24,8 @@ Util.reload('util', Util)
 local table, math = table, math
 local print, type, ipairs, select, getmetatable = print, type, ipairs, select, getmetatable
 
-Util.print = print
+Util.log = require 'module.log.log'
+Util.print = Util.log and Util.log.info or print
 
 Util.memo = require 'module.knife.memoize'
 Util.bind = require 'module.knife.bind'
diff --git a/main.lua b/main.lua
index 70a1c78a8bbd33724572d1655e963dad180d2e14..e73d081533067b23d5839dca3c4ed329a2dafce1 100644
--- a/main.lua
+++ b/main.lua
@@ -18,8 +18,7 @@ setmetatable(_G, {
 ---
 
 local Game = require 'game'
-local Util, Ticker, Handler, Overlay = Game.util, Game.ticker, Game.handler, Game.overlay
-local print = Util.print or print
+local Ticker, Handler, Overlay, Logger = Game.ticker, Game.handler, Game.overlay, Game.logger
 
 Overlay.init()
 
@@ -27,14 +26,17 @@ Overlay.init()
 ---Called when shutdown
 ---
 ---@param message string?
+---@param level LoggerMode|string?
 ---@return boolean
-function love.shutdown(message)
-	if message then print('Shutting down: ' .. message) end
+function love.shutdown(message, level)
+	Logger:log('info', '|', os.date(), '| Shutting down...')
+	if message then Logger:log(level or 'info', 'Reason: ' .. message) end
 	if love.audio then love.audio.stop() end
 	for _, archive in pairs(meta.mounted) do
 		love.filesystem.unmount(archive[1])
 		archive[#archive]:release()
 	end
+	Logger:flush()
 	return true
 end
 
@@ -48,7 +50,8 @@ end
 
 local old_errhand = love.errhand --[[@as function]] or love.errorhandler
 function love.errorhandler(msg)
-	love.shutdown('errorhandler: ' .. msg)
+	-- Known Issue: doesn't log trackback.
+	love.shutdown(msg, 'error')
 	return old_errhand(msg)
 end
 
diff --git a/module/log b/module/log
new file mode 160000
index 0000000000000000000000000000000000000000..b4c72ff40f9ac640b500a67e85cd29bb3645601b
--- /dev/null
+++ b/module/log
@@ -0,0 +1 @@
+Subproject commit b4c72ff40f9ac640b500a67e85cd29bb3645601b