This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_cryo"]["cryo"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,6 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "roblox/lumberyak"
version = "0.1.0"
commit = "47552268e68c899226295e82936d3b68dfd53565"
source = "git+https://github.rbx.com/roblox/lumberyak#master"
dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"]
@@ -0,0 +1,247 @@
local Root = script.Parent.Parent
local Cryo = require(Root.Cryo)
local Logger = {}
Logger.__index = Logger
Logger.Levels = {
Error = "Error",
Warning = "Warning",
Info = "Info",
Debug = "Debug",
Trace = "Trace",
}
local levelOrder = {
Logger.Levels.Error,
Logger.Levels.Warning,
Logger.Levels.Info,
Logger.Levels.Debug,
Logger.Levels.Trace,
}
local levelRank = {}
for k, v in pairs(levelOrder) do
levelRank[v] = k
end
function Logger.Levels.fromString(str)
if type(str) ~= "string" then
return nil
end
for _, k in pairs(levelOrder) do
if string.lower(k) == string.lower(str) then
return k
end
end
return nil
end
function Logger.new(parent, name)
local logger = {
name = name,
sinks = {},
children = {},
parent = parent,
context = {},
dirty = true,
active = {},
cache = {
sinks = {},
context = {},
}
}
for k, _ in pairs(Logger.Levels) do
if parent then
logger.active[k] = parent.active[k]
else
logger.active[k] = false
end
end
if parent then
parent.children[logger] = true
end
setmetatable(logger, Logger)
return logger
end
-- Activate `level` and above logging levels.
local function setActive(level, node)
local maxLevel = levelRank[level]
if maxLevel then
for n = 1,maxLevel do
node.active[levelOrder[n]] = true
end
for k, _ in pairs(node.children) do
setActive(level, k)
end
end
end
-- Set the dirty flag for `node` and all its children.
local function setDirty(node)
node.dirty = true
for k, _ in pairs(node.children) do
setDirty(k)
end
end
-- Update the context and sinks cache for `node` and its ancestors.
local function updateCache(node)
if not node.dirty then
return
end
if not node.parent then
node.cache.context = node.context
node.cache.sinks = node.sinks
node.dirty = false
return
end
updateCache(node.parent)
-- Dictionary join the context. List join the sinks. Concatenate the prefixes.
node.cache.context = Cryo.Dictionary.join(node.parent.cache.context, node.context)
if node.parent.cache.context.prefix and node.context.prefix then
node.cache.context.prefix = node.parent.cache.context.prefix .. node.context.prefix
end
node.cache.sinks = Cryo.List.join(node.parent.cache.sinks, node.sinks)
node.dirty = false
end
-- Set the parent of this Logger and update its cache, active bits and dirty bit.
function Logger:setParent(parent)
if self.parent then
self.parent.children[self] = nil
end
updateCache(parent)
self.parent = parent
self.parent.children[self] = true
local maxLevel = -1
for _, sink in pairs(parent.cache.sinks) do
local sinkLevel = levelRank[sink.maxLevel]
if sinkLevel then
maxLevel = math.max(maxLevel, levelRank[sink.maxLevel])
end
end
if maxLevel > -1 then
setActive(levelOrder[maxLevel], self)
end
setDirty(self)
end
function Logger:addSink(sink)
setActive(sink.maxLevel, self)
table.insert(self.sinks, sink)
setDirty(self)
end
function Logger:setContext(context)
self.context = context
setDirty(self)
end
local function log(level, node, args)
if node.dirty then
updateCache(node)
end
-- Collect per-log context.
local fullContext = {
level = level,
rawMessage = args,
loggerName = node.name,
}
-- Call any functions in the context.
for k, v in pairs(node.cache.context) do
if type(v) == "function" then
fullContext[k] = v()
else
fullContext[k] = v
end
end
-- Interpolate the log message.
local interpMsg
if args.n == 0 then
interpMsg = "LUMBERYAK INTERNAL: No log message given"
else
interpMsg = args[1]
end
if fullContext.prefix then
interpMsg = fullContext.prefix .. interpMsg
end
if interpMsg:find("{") then
local i = 1
interpMsg = (interpMsg:gsub("{(.-)}", function(w)
-- Treat {} as a positional arg.
if w == "" then
i = i + 1
return args[i]
end
return fullContext[w] or w
end))
if i < args.n then
interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too many arguments given for format string"
elseif i > args.n then
interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too few arguments given for format string"
end
elseif args.n > 1 then
interpMsg = interpMsg .. "\nLUMBERYAK INTERNAL: Too many arguments given for format string"
end
-- Send the message to any sinks that are listening to the right level.
local rank = levelRank[level]
for _, k in pairs(node.cache.sinks) do
if levelRank[k.maxLevel] and levelRank[k.maxLevel] >= rank then
k:log(interpMsg, fullContext)
end
end
end
function Logger:error(...)
if not self.active[Logger.Levels.Error] then
return
end
log(Logger.Levels.Error, self, table.pack(...))
end
function Logger:warning(...)
if not self.active[Logger.Levels.Warning] then
return
end
log(Logger.Levels.Warning, self, table.pack(...))
end
function Logger:info(...)
if not self.active[Logger.Levels.Info] then
return
end
log(Logger.Levels.Info, self, table.pack(...))
end
function Logger:debug(...)
if not self.active[Logger.Levels.Debug] then
return
end
log(Logger.Levels.Debug, self, table.pack(...))
end
function Logger:trace(...)
if not self.active[Logger.Levels.Trace] then
return
end
log(Logger.Levels.Trace, self, table.pack(...))
end
return Logger
@@ -0,0 +1,659 @@
return function()
local Logger = require(script.Parent.Logger)
local function newSink(level)
return {
maxLevel = level,
seen = {},
log = function(self, message, context)
table.insert(self.seen, {message=message, context=context})
end,
}
end
describe("A new Logger", function()
it("should be creatable without a parent", function()
expect(function()
local _ = Logger.new()
end).to.never.throw()
end)
it("should be creatable with a parent", function()
expect(function()
local log1 = Logger.new()
local _ = Logger.new(log1)
end).to.never.throw()
end)
it("should be creatable with a parent, alternate syntax", function()
expect(function()
local log1 = Logger.new()
local _ = log1:new()
end).to.never.throw()
end)
it("should add sinks", function()
expect(function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
end).to.never.throw()
end)
it("should add context", function()
expect(function()
local log = Logger.new()
log:setContext({foo = "bar"})
end).to.never.throw()
end)
end)
describe("Basic logging", function()
it("to the root logger", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:info("foo")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo")
end)
it("to a child logger", function()
local log1 = Logger.new()
local log2 = log1:new()
local sink = newSink(Logger.Levels.Info)
log1:addSink(sink)
log2:info("foo")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo")
end)
it("to a sibling logger", function()
local log1 = Logger.new()
local log2 = log1:new()
local log3 = log1:new()
local sink = newSink(Logger.Levels.Info)
log2:addSink(sink)
log3:info("foo")
expect(#sink.seen).to.equal(0)
end)
it("to a parent logger", function()
local log1 = Logger.new()
local log2 = log1:new()
local sink = newSink(Logger.Levels.Info)
log2:addSink(sink)
log1:info("foo")
expect(#sink.seen).to.equal(0)
end)
it("to a child logger, sink added first", function()
local log1 = Logger.new()
local sink = newSink(Logger.Levels.Info)
log1:addSink(sink)
local log2 = log1:new()
log2:info("foo")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo")
end)
it("to a sibling logger, sink added first", function()
local log1 = Logger.new()
local log2 = log1:new()
local sink = newSink(Logger.Levels.Info)
log2:addSink(sink)
local log3 = log1:new()
log3:info("foo")
expect(#sink.seen).to.equal(0)
end)
end)
describe("When logging different levels", function()
local cases = {
[Logger.Levels.Error] = 1,
[Logger.Levels.Warning] = 2,
[Logger.Levels.Info] = 3,
[Logger.Levels.Debug] = 4,
[Logger.Levels.Trace] = 5,
}
for level, count in pairs(cases) do
it("logging to the root should respect " .. level, function()
local log = Logger.new()
local sink = newSink(level)
log:addSink(sink)
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(#sink.seen).to.equal(count)
end)
it("logging to the child should respect " .. level, function()
local log1 = Logger.new()
local log2 = log1:new()
local sink = newSink(level)
log1:addSink(sink)
log2:error("error")
log2:warning("warning")
log2:info("info")
log2:debug("debug")
log2:trace("trace")
expect(#sink.seen).to.equal(count)
end)
end
describe("should treat invalid log levels as disabled", function()
it("should log to no levels when given a bad maxLevel", function()
local log = Logger.new()
local sink = newSink("not-a-level")
log:addSink(sink)
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(#sink.seen).to.equal(0)
end)
it("should log to no levels when given nil", function()
local log = Logger.new()
local sink = newSink(nil)
log:addSink(sink)
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(#sink.seen).to.equal(0)
end)
it("should handle multiple sinks when some are disabled", function()
local log = Logger.new()
local sink1 = newSink(nil)
local sink2 = newSink(Logger.Levels.Trace)
log:addSink(sink1)
log:addSink(sink2)
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(#sink1.seen).to.equal(0)
expect(#sink2.seen).to.equal(5)
end)
end)
end)
describe("When logging different levels using fromString", function()
local cases = {
["error"] = 1,
["Warning"] = 2,
["INFO"] = 3,
["dEBUG"] = 4,
["TrAcE"] = 5,
["invalid"] = 0,
}
for level, count in pairs(cases) do
it("fromString should handle " .. level, function()
local log = Logger.new()
local sink = newSink(Logger.Levels.fromString(level))
log:addSink(sink)
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(#sink.seen).to.equal(count)
end)
end
end)
describe("sinks", function()
it("should be disabled without error when maxLevel isn't set", function()
local log = Logger.new()
local seen = 0
log:addSink({
log = function()
seen = seen + 1
end,
})
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(seen).to.equal(0)
end)
it("should be disabled without error when maxLevel is set incorrectly", function()
local log = Logger.new()
local seen = 0
log:addSink({
maxLevel = "foo",
log = function()
seen = seen + 1
end,
})
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(seen).to.equal(0)
end)
it("should not cause problems with parenting when not set", function()
local log1 = Logger.new()
local seen = 0
log1:addSink({
log = function()
seen = seen + 1
end,
})
local log2 = log1:new()
log2:error("error")
log2:warning("warning")
log2:info("info")
log2:debug("debug")
log2:trace("trace")
expect(seen).to.equal(0)
end)
end)
describe("should treat invalid log levels as disabled", function()
it("should log to no levels when given a bad maxLevel", function()
local log = Logger.new()
local sink = newSink("not-a-level")
log:addSink(sink)
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(#sink.seen).to.equal(0)
end)
it("should log to no levels when given nil", function()
local log = Logger.new()
local sink = newSink(nil)
log:addSink(sink)
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(#sink.seen).to.equal(0)
end)
it("should handle multiple sinks when some are disabled", function()
local log = Logger.new()
local sink1 = newSink(nil)
local sink2 = newSink(Logger.Levels.Trace)
log:addSink(sink1)
log:addSink(sink2)
log:error("error")
log:warning("warning")
log:info("info")
log:debug("debug")
log:trace("trace")
expect(#sink1.seen).to.equal(0)
expect(#sink2.seen).to.equal(5)
end)
end)
describe("with positional arguments", function()
it("should work with one arg", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:info("foo {}", "bar")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo bar")
end)
it("should work with two args", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:info("foo {} {}", "bar", "baz")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo bar baz")
end)
it("should output a warning with too many arguments", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:info("foo {}", "bar", "baz")
assert(string.find(sink.seen[1].message, "LUMBERYAK INTERNAL"),
"Expected an internal warning, got [[\n" .. sink.seen[1].message .. "\n]]")
end)
it("should output a warning with too few arguments", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:info("foo {} {}", "bar")
assert(string.find(sink.seen[1].message, "LUMBERYAK INTERNAL"),
"Expected an internal warning, got [[\n" .. sink.seen[1].message .. "\n]]")
end)
end)
describe("When passing in context", function()
describe("with context from root", function()
it("should pass along static context", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:setContext({bar = 1})
log:info("foo")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].context.bar).to.equal(1)
end)
it("should call dynamic context", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:setContext({bar = function()
return 1
end})
log:info("foo")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].context.bar).to.equal(1)
end)
end)
describe("when combining context", function()
it("should merge non-overlapping context", function()
local log1 = Logger.new()
local log2 = log1:new()
local log3 = log2:new()
local sink = newSink(Logger.Levels.Info)
log1:addSink(sink)
log1:setContext({bar = 1})
log2:setContext({baz = 2})
log3:setContext({quz = 3})
log3:info("foo")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].context.bar).to.equal(1)
expect(sink.seen[1].context.baz).to.equal(2)
expect(sink.seen[1].context.quz).to.equal(3)
end)
it("should overwrite overlapping context", function()
local log1 = Logger.new()
local log2 = log1:new()
local log3 = log2:new()
local sink = newSink(Logger.Levels.Info)
log1:addSink(sink)
log1:setContext({bar = 1, baz = 1, quz = 1})
log2:setContext({bar = 2, baz = 2})
log3:setContext({baz = 3})
log3:info("foo")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].context.quz).to.equal(1)
expect(sink.seen[1].context.bar).to.equal(2)
expect(sink.seen[1].context.baz).to.equal(3)
end)
it("should call dynamic context", function()
local log1 = Logger.new()
local log2 = log1:new()
local sink = newSink(Logger.Levels.Info)
log1:addSink(sink)
log1:setContext({bar = 1})
log2:setContext({bar = function()
return 2
end})
log2:info("foo")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].context.bar).to.equal(2)
end)
end)
end)
describe("Message interpolation", function()
it("should leave plain messages alone", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:setContext({bar = "baz"})
log:info("foo")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo")
end)
it("should substitute info from static context", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:setContext({bar = "baz"})
log:info("foo {bar}")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo baz")
end)
it("should substitute info from dynamic context", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:setContext({bar = function()
return "baz"
end})
log:info("foo {bar}")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo baz")
end)
end)
describe("Message prefix", function()
it("should prepend the prefix", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:setContext({prefix = "foo: "})
log:info("bar")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo: bar")
end)
it("should interpolate the prefix", function()
local log = Logger.new()
local sink = newSink(Logger.Levels.Info)
log:addSink(sink)
log:setContext({prefix = "{foo}: ", foo = "baz"})
log:info("bar")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("baz: bar")
end)
it("should stack prefixes", function()
local log1 = Logger.new()
local log2 = log1:new()
local sink = newSink(Logger.Levels.Info)
log1:addSink(sink)
log1:setContext({prefix = "foo: "})
log2:setContext({prefix = "bar: "})
log2:info("baz")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("foo: bar: baz")
end)
end)
it("Should get the name of logger used", function()
local log1 = Logger.new(nil, "log1")
local log2 = log1:new("log2")
local sink = newSink(Logger.Levels.Info)
log1:addSink(sink)
log1:info("{loggerName}")
log2:info("{loggerName}")
expect(#sink.seen).to.equal(2)
expect(sink.seen[1].message).to.equal("log1")
expect(sink.seen[2].message).to.equal("log2")
end)
describe("setParent should work", function()
it("when calling A->B then B->C", function()
local a = Logger.new()
local b = Logger.new()
local c = Logger.new()
local sink = newSink(Logger.Levels.Info)
a:addSink(sink)
a:setContext({a = "A"})
b:setContext({b = "B"})
c:setContext({c = "C"})
b:setParent(a)
c:setParent(b)
c:info("{a} {b} {c}")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("A B C")
end)
it("when calling B->C then A->B", function()
local a = Logger.new()
local b = Logger.new()
local c = Logger.new()
local sink = newSink(Logger.Levels.Info)
a:addSink(sink)
a:setContext({a = "A"})
b:setContext({b = "B"})
c:setContext({c = "C"})
c:setParent(b)
b:setParent(a)
c:info("{a} {b} {c}")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("A B C")
end)
it("when moving D from B to C", function()
local a = Logger.new()
local b = Logger.new()
local c = Logger.new()
local d = Logger.new()
local sink = newSink(Logger.Levels.Info)
a:addSink(sink)
a:setContext({x = "A"})
b:setContext({y = "B"})
c:setContext({y = "C"})
d:setContext({z = "D"})
c:setParent(a)
b:setParent(a)
d:setParent(b)
d:info("{x} {y} {z}")
d:setParent(c)
d:info("{x} {y} {z}")
expect(#sink.seen).to.equal(2)
expect(sink.seen[1].message).to.equal("A B D")
expect(sink.seen[2].message).to.equal("A C D")
end)
it("when mixing setParent and static parents", function()
local a = Logger.new()
local b = Logger.new()
local c = b:new()
local sink = newSink(Logger.Levels.Info)
a:addSink(sink)
a:setContext({a = "A"})
b:setContext({b = "B"})
c:setContext({c = "C"})
b:setParent(a)
c:info("{a} {b} {c}")
expect(#sink.seen).to.equal(1)
expect(sink.seen[1].message).to.equal("A B C")
end)
end)
end
@@ -0,0 +1,47 @@
-- This mock has the same API as the Logger, but none of the calls do anything.
-- Note that MockLogger and Logger can't be parented to each other.
local MockLogger = {}
MockLogger.Levels = {
Error = "MockError",
Warning = "MockWarning",
Info = "MockInfo",
Debug = "MockDebug",
Trace = "MockTrace",
fromString = function()
return "MockInfo"
end,
}
MockLogger.__index = MockLogger
function MockLogger.new()
return setmetatable({}, MockLogger)
end
function MockLogger.setParent()
end
function MockLogger.setContext()
end
function MockLogger.addSink()
end
function MockLogger.error()
end
function MockLogger.warning()
end
function MockLogger.info()
end
function MockLogger.debug()
end
function MockLogger.trace()
end
return MockLogger
@@ -0,0 +1,22 @@
return function()
local MockLogger = require(script.Parent.MockLogger)
local Logger = require(script.Parent.Logger)
it("MockLogger should have the same API as Logger", function()
for k, v in pairs(Logger) do
local mock = MockLogger[k]
assert(mock, "Expected a mock of " .. k)
assert(type(mock) == type(v),
"Expected the type of " .. k .. " to be " .. type(v) .. ", got " .. type(mock))
end
end)
it("MockLogger.Levels should have the same API as Logger.Levels", function()
for k, v in pairs(Logger.Levels) do
local mock = MockLogger.Levels[k]
assert(mock, "Expected a mock of " .. k)
assert(type(mock) == type(v),
"Expected the type of " .. k .. " to be " .. type(v) .. ", got " .. type(mock))
end
end)
end
@@ -0,0 +1,159 @@
local Logger = require(Workspace.LoadedCode.Packages.Lumberyak.Logger)
local runs = 2000000
local function timeit(makeLogger, callLogger)
local log = makeLogger()
local t = tick()
for _ = 1, runs do
callLogger(log)
end
return (tick() - t) / runs
end
local function callSimple(log)
log:info("foo")
end
local function callInterp(log)
log:info("foo {}", 2)
end
local function simple()
local count = 0
local countSink = {
maxLevel = Logger.Levels.Info,
log = function(_, _, context)
count = count + context.number
end,
}
local log = Logger.new()
log:setContext({number = 1})
log:addSink(countSink)
return log
end
local function empty()
return {
info = function() end
}
end
local function off()
local count = 0
local countSink = {
maxLevel = Logger.Levels.Error,
log = function(_, _, context)
count = count + context.number
end,
}
local log = Logger.new()
log:setContext({number = 1})
log:addSink(countSink)
return log
end
local function short()
local count = 0
local countSink = {
maxLevel = Logger.Levels.Info,
log = function(_, _, context)
count = count + context.number
end,
}
local log = Logger.new()
local child = log:new()
log:setContext({number = 1})
log:addSink(countSink)
return child
end
local function long()
local count = 0
local countSink = {
maxLevel = Logger.Levels.Info,
log = function(_, _, context)
count = count + context.number
end,
}
local log = Logger.new()
log:setContext({number = 1})
log:addSink(countSink)
local a = log:new()
local b = a:new()
local c = b:new()
local d = c:new()
return d
end
local function many()
local count = 0
local sink1 = {
maxLevel = Logger.Levels.Error,
log = function(_, _, context)
count = count + context.number
end,
}
local sink2 = {
maxLevel = Logger.Levels.Warning,
log = function(_, _, context)
count = count + context.number
end,
}
local sink3 = {
maxLevel = Logger.Levels.Info,
log = function(_, _, context)
count = count + context.number
end,
}
local sink4 = {
maxLevel = Logger.Levels.Debug,
log = function(_, _, context)
count = count + context.number
end,
}
local sink5 = {
maxLevel = Logger.Levels.Trace,
log = function(_, _, context)
count = count + context.number
end,
}
local log = Logger.new()
log:setContext({number = 1})
log:addSink(sink1)
log:addSink(sink2)
log:addSink(sink3)
log:addSink(sink4)
log:addSink(sink5)
return log
end
local base = timeit(empty, callSimple)
local interp = timeit(empty, callInterp)
local tests = {
["Log level off"] = off,
["Simple logger"] = simple,
["Short chain"] = short,
["Long chain"] = long,
["Many sinks"] = many,
}
local function fmt(name, case, t)
print(string.format("%-40s %0.3e [%0.4fx]", name .. " - " .. case, t, t / base))
end
fmt("Empty function", "Simple", base)
fmt("Empty function", "Interpolation", interp)
for k, v in pairs(tests) do
local t1 = timeit(v, callSimple)
fmt(k, "Simple", t1)
local t2 = timeit(v, callInterp)
fmt(k, "Interpolation", t2)
end
@@ -0,0 +1,24 @@
local Logger = require(script.Parent.Parent.Parent.Logger)
local PrintSink = {}
function PrintSink.new(level)
local printer = {
maxLevel = level
}
setmetatable(printer, PrintSink)
return printer
end
function PrintSink:log(message, context)
if context.level == Logger.Levels.Error then
error(message, 5)
elseif context.level == Logger.Levels.Warning then
warn(message)
else
print(message)
end
end
return PrintSink
@@ -0,0 +1,13 @@
local log = require(script.Parent.appLogger)
-- The app has some way to import the page, but not vice versa.
local page = require(script.Parent.Parent.page.page)
local printer = require(script.Parent.PrintSink)
log:addSink(printer.new(log.Levels.Error))
return function()
log:info("calling {root}")
page.init(log)
return page.doSomething()
end
@@ -0,0 +1,4 @@
local Logger = require(script.Parent.Parent.Parent.Logger)
local log = Logger.new()
log:setContext({root = "root"})
return log
@@ -0,0 +1,26 @@
return function()
local app = require(script.Parent.app.app)
local log = require(script.Parent.app.appLogger)
local function newSink(level)
return {
maxLevel = level,
seen = {},
log = function(self, message, context)
table.insert(self.seen, {message=message, context=context})
end,
}
end
it("should generate expected messages", function()
local sink = newSink(log.Levels.Info)
log:addSink(sink)
local result = app()
expect(result).to.equal("done")
expect(#sink.seen).to.equal(3)
expect(sink.seen[1].message).to.equal("calling root")
expect(sink.seen[2].message).to.equal("calling root foo")
expect(sink.seen[3].message).to.equal("calling root foo bar")
end)
end
@@ -0,0 +1,7 @@
local log = require(script.Parent.pageLogger):new()
log:setContext({bar = "bar"})
return function()
log:info("calling {root} {foo} {bar}")
return "done"
end
@@ -0,0 +1,13 @@
-- foo does not need to require root
local log = require(script.Parent.pageLogger)
local component = require(script.Parent.component)
return {
init = function(logParent)
log:setParent(logParent)
end,
doSomething = function()
log:info("calling {root} {foo}")
return component()
end
}
@@ -0,0 +1,4 @@
local Logger = require(script.Parent.Parent.Parent.Logger)
local log = Logger.new()
log:setContext({foo = "foo"})
return log
@@ -0,0 +1,3 @@
return {
Logger = require(script.Logger),
}