cc-utilities/logging.lua
2024-11-28 01:43:25 +01:00

339 lines
8.3 KiB
Lua

local Logging = {}
local LogLevel = {
TRACE = 1,
DEBUG = 2,
INFO = 3,
WARNING = 4,
CRITICAL = 5,
ERROR = 6
}
LogLevel._DEFAULT = LogLevel.TRACE
for k,v in pairs(LogLevel) do
LogLevel[v] = k
end
function LogLevel.isValid(value)
return LogLevel[LogLevel[value]] ~= nil
end
function LogLevel.asNumber(value)
return type(value) == "number" and value or LogLevel[value]
end
function LogLevel.isGreaterOrEqual(a, b)
return a >= b
end
local Logger = {
ignoreGlobals = true
}
Logger.__index = Logger
local function combineOutputs(...)
local args = {...}
if #args == 0 then
return function() end
elseif #args == 1 then
return args[1]
end
return function(text, level)
for _,v in ipairs(args) do
v(text, level)
end
end
end
-- TODO: Split logger outputs and specify LogLevel for each
function Logger:new(...)
local logger = { output = combineOutputs(...) or function() end }
setmetatable(logger, self)
logger.__index = logger
return logger
end
function Logger:setIgnoreGlobals(ignoreGlobals)
self.ignoreGlobals = ignoreGlobals
end
local LogPlain = {}
function LogPlain.of(value, deep)
local obj = { value, deep = deep }
setmetatable(obj, LogPlain)
return obj
end
function LogPlain.is(value)
return type(value) == "table" and getmetatable(value) == LogPlain
end
function LogPlain.isDeep(value)
return LogPlain.is(value) and value.deep
end
function LogPlain.getValue(value)
return LogPlain.is(value) and value[1] or value
end
local RecursionSentinel = {}
function RecursionSentinel.make(table, index)
local obj = { table = table, index = index }
setmetatable(obj, RecursionSentinel)
return obj
end
function RecursionSentinel.isSentinel(value)
return type(value) == "table" and getmetatable(value) == RecursionSentinel
end
function RecursionSentinel.isKnown(knownTables, value)
return knownTables[value] ~= nil
end
function RecursionSentinel.getSentinel(knownTables, value)
return knownTables[value] or RecursionSentinel.add(knownTables, value)
end
function RecursionSentinel.add(knownTables, value)
local sentinel = RecursionSentinel.make(value, #knownTables)
knownTables[value] = sentinel
return sentinel
end
function RecursionSentinel.remove(knownTables, value)
local sentinel = knownTables[value]
if sentinel then
sentinel.index = nil
end
knownTables[value] = nil
end
local function cloneNonRecursive(inValue, sentinels)
local value = LogPlain.getValue(inValue)
if type(value) == "table" then
local wrapLogPlain = LogPlain.is(inValue) and function(v) return LogPlain.of(v, inValue.deep) end or function(v) return v end
if RecursionSentinel.isKnown(sentinels, value) then
return wrapLogPlain(RecursionSentinel.getSentinel(sentinels, value))
end
local sentinel = RecursionSentinel.getSentinel(sentinels, value)
local clone = {}
for i,v in ipairs(value) do
clone[i] = cloneNonRecursive(v, sentinels)
end
for k,v in pairs(value) do
clone[cloneNonRecursive(k, sentinels)] = cloneNonRecursive(v, sentinels)
end
RecursionSentinel.remove(sentinels, sentinel.table)
sentinel.value = clone
return wrapLogPlain(sentinel.value)
else
return inValue
end
end
local G_ = {}
for k,v in pairs(_G) do
G_[v] = k
end
local function _simpleStringify(inValue, builder, ignoreGlobals, skipFunctions, plain)
local value = LogPlain.getValue(inValue)
local isPlain = plain or LogPlain.is(inValue)
local isDeep = plain or LogPlain.isDeep(inValue)
if type(value) == "table" then
if not isPlain then
table.insert(builder, "<")
end
if ignoreGlobals and G_[value] ~= nil then
table.insert(builder, "_G.")
table.insert(builder, G_[value])
elseif RecursionSentinel.isSentinel(value) then
if isPlain then
table.insert(builder, "<recurse_")
table.insert(builder, tostring(value.index))
else
table.insert(builder, "recurse ")
table.insert(builder, tostring(value.value))
end
table.insert(builder, ">")
else
if not isPlain then
table.insert(builder, tostring(value))
table.insert(builder, ">")
end
table.insert(builder, "{")
local first = true
for _,v in ipairs(value) do
if not first then
table.insert(builder, ",")
else
first = false
end
_simpleStringify(v, builder, ignoreGlobals, skipFunctions, isDeep)
end
first = #value == 0
local len = #value
for k,v in pairs(value) do
-- Array elements already printed
if type(k) == "number" and k <= len then
goto continue
end
if not first then
table.insert(builder, ",")
else
first = false
end
-- String key values are always plain-printed
_simpleStringify(k, builder, ignoreGlobals, skipFunctions, isDeep or type(k) == "string")
table.insert(builder, "=")
_simpleStringify(v, builder, ignoreGlobals, skipFunctions, isDeep)
::continue::
end
table.insert(builder, "}")
end
elseif type(value) == "string" then
if not isPlain then
table.insert(builder, "\"")
end
table.insert(builder, tostring(value))
if not isPlain then
table.insert(builder, "\"")
end
elseif type(value) == "function" then
if not skipFunctions then
local info = debug.getinfo(value, "u")
if not isPlain then
table.insert(builder, tostring(value))
end
table.insert(builder, "(")
table.insert(builder, tostring(info.nparams))
if info.isvararg then
table.insert(builder, "*")
end
table.insert(builder, ")")
end
elseif not skipFunctions or type(value) ~= "function" then
table.insert(builder, tostring(value))
end
end
function Logging.deepStringify(value)
local collect = {}
_simpleStringify(cloneNonRecursive(value, {}), collect, false, false)
return table.concat(collect)
end
function Logger:_doPrint(level, message, ...)
local result = { tostring(LogLevel[level]), Logging.deepStringify(message) }
for _,v in ipairs({...}) do
table.insert(result, Logging.deepStringify(v))
end
self.output(table.concat(result, " "), level)
end
function Logger:trace(message, ...)
self:_doPrint(LogLevel.TRACE, message, ...)
end
function Logger:debug(message, ...)
self:_doPrint(LogLevel.DEBUG, message, ...)
end
function Logger:info(message, ...)
self:_doPrint(LogLevel.INFO, message, ...)
end
function Logger:warn(message, ...)
self:_doPrint(LogLevel.WARNING, message, ...)
end
function Logger:critical(message, ...)
self:_doPrint(LogLevel.CRITICAL, message, ...)
end
function Logger:error(message, ...)
self:_doPrint(LogLevel.ERROR, message, ...)
end
function Logger:setOutput(...)
self.output = combineOutputs(...)
end
function Logger.plain(value, deep)
return LogPlain.of(value, deep)
end
Logging.Logger = Logger
Logging.LogLevel = LogLevel
function Logging.getGlobalLogger()
if _G._GLOBAL_LOGGER == nil then
_G._GLOBAL_LOGGER = Logger:new{ output = function() end }
end
return _G._GLOBAL_LOGGER
end
function Logging.firstLoad(...)
Logging.resetAll()
local logger = Logging.getGlobalLogger()
logger:setOutput(...)
return logger
end
Logging.OUTPUTS = {}
function Logging.OUTPUTS.file(name, logLevel)
logLevel = logLevel or LogLevel._DEFAULT
local doWrite = function(file, text)
file.write(text)
file.write("\n")
file.flush()
end
local file = fs.open(name, "w+")
return function(text, level)
if level < logLevel then
return
end
if not pcall(doWrite, file, text) then
local dRes, dRet = pcall(fs.delete, name)
if not dRes then
print("Error deleting logfile ("..name.."): "..tostring(dRet))
else
file = fs.open(name, "w+")
local wRes, wRet = pcall(doWrite, file, text)
if not wRes then
print("Error writing to logfile ("..name.."): "..tostring(wRet))
end
end
end
end
end
function Logging.OUTPUTS.transmit(modem, channel, logLevel)
logLevel = logLevel or LogLevel._DEFAULT
end
function Logging.OUTPUTS.stdout(logLevel)
logLevel = logLevel or LogLevel._DEFAULT
return function(text, level)
if level < logLevel then
return
end
print(text)
end
end
function Logging.resetAll()
_G._GLOBAL_LOGGER = nil
end
return Logging