--[[ Centralized place to handle logging. Lets us: - Unit test log output via `Logging.capture` - Disable verbose log messages when not debugging Roact This should be broken out into a separate library with the addition of scoping and logging configuration. ]] -- Determines whether log messages will go to stdout/stderr local outputEnabled = true -- A set of LogInfo objects that should have messages inserted into them. -- This is a set so that nested calls to Logging.capture will behave. local collectors = {} -- A set of all stack traces that have called warnOnce. local onceUsedLocations = {} --[[ Indent a potentially multi-line string with the given number of tabs, in addition to any indentation the string already has. ]] local function indent(source, indentLevel) local indentString = ("\t"):rep(indentLevel) return indentString .. source:gsub("\n", "\n" .. indentString) end --[[ Indents a list of strings and then concatenates them together with newlines into a single string. ]] local function indentLines(lines, indentLevel) local outputBuffer = {} for _, line in ipairs(lines) do table.insert(outputBuffer, indent(line, indentLevel)) end return table.concat(outputBuffer, "\n") end local logInfoMetatable = {} --[[ Automatic coercion to strings for LogInfo objects to enable debugging them more easily. ]] function logInfoMetatable:__tostring() local outputBuffer = { "LogInfo {" } local errorCount = #self.errors local warningCount = #self.warnings local infosCount = #self.infos if errorCount + warningCount + infosCount == 0 then table.insert(outputBuffer, "\t(no messages)") end if errorCount > 0 then table.insert(outputBuffer, ("\tErrors (%d) {"):format(errorCount)) table.insert(outputBuffer, indentLines(self.errors, 2)) table.insert(outputBuffer, "\t}") end if warningCount > 0 then table.insert(outputBuffer, ("\tWarnings (%d) {"):format(warningCount)) table.insert(outputBuffer, indentLines(self.warnings, 2)) table.insert(outputBuffer, "\t}") end if infosCount > 0 then table.insert(outputBuffer, ("\tInfos (%d) {"):format(infosCount)) table.insert(outputBuffer, indentLines(self.infos, 2)) table.insert(outputBuffer, "\t}") end table.insert(outputBuffer, "}") return table.concat(outputBuffer, "\n") end local function createLogInfo() local logInfo = { errors = {}, warnings = {}, infos = {}, } setmetatable(logInfo, logInfoMetatable) return logInfo end local Logging = {} --[[ Invokes `callback`, capturing all output that happens during its execution. Output will not go to stdout or stderr and will instead be put into a LogInfo object that is returned. If `callback` throws, the error will be bubbled up to the caller of `Logging.capture`. ]] function Logging.capture(callback) local collector = createLogInfo() local wasOutputEnabled = outputEnabled outputEnabled = false collectors[collector] = true local success, result = pcall(callback) collectors[collector] = nil outputEnabled = wasOutputEnabled assert(success, result) return collector end --[[ Issues a warning with an automatically attached stack trace. ]] function Logging.warn(messageTemplate, ...) local message = messageTemplate:format(...) for collector in pairs(collectors) do table.insert(collector.warnings, message) end -- debug.traceback inserts a leading newline, so we trim it here local trace = debug.traceback("", 2):sub(2) local fullMessage = ("%s\n%s"):format(message, indent(trace, 1)) if outputEnabled then warn(fullMessage) end end --[[ Issues a warning like `Logging.warn`, but only outputs once per call site. This is useful for marking deprecated functions that might be called a lot; using `warnOnce` instead of `warn` will reduce output noise while still correctly marking all call sites. ]] function Logging.warnOnce(messageTemplate, ...) local trace = debug.traceback() if onceUsedLocations[trace] then return end onceUsedLocations[trace] = true Logging.warn(messageTemplate, ...) end return Logging