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, "") 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