├── example.lua ├── delog ├── README.md └── delog.lua /example.lua: -------------------------------------------------------------------------------- 1 | local log = require("delog").level("trace").output(io.stdout) 2 | log.info("An informational message") 3 | log.warn("A pretty-printed table: ${%ddp}", { answer=42 }) 4 | 5 | log.prepend(log.PREPEND_DEBUG) 6 | log.debug("This shows the location") 7 | 8 | log.prepend(log.PREPEND_DEBUG_FUNC) 9 | function this_is_a_function() 10 | log.info("Printing the function name") 11 | end 12 | this_is_a_function() 13 | -------------------------------------------------------------------------------- /delog: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env lua 2 | -- 3 | -- delog.lua 4 | -- Copyright (C) 2016 Adrian Perez 5 | -- Licensed under the MIT license or, at your option the Apache-2.0 license. 6 | -- 7 | 8 | if #arg == 0 then 9 | -- Use stdin/stdout 10 | elseif #arg == 1 then 11 | io.input(arg[1]) 12 | elseif #arg == 2 then 13 | io.input(arg[1]) 14 | io.output(arg[2]) 15 | else 16 | io.stderr:write("Usage: delog [input [output]]") 17 | os.exit(1) 18 | end 19 | 20 | local import_pattern = "^local%s+([%w_]*)%s*=%s*require%s*%(?%s*[\"'][%w%._]*delog[\"']%s*%)?" 21 | local varname = nil 22 | for line in io.lines() do 23 | varname = line:match(import_pattern) 24 | if varname then 25 | io.write("--[[ " .. line .. " ]]\n") 26 | break 27 | end 28 | io.write(line, "\n") 29 | end 30 | if not varname then 31 | io.stderr:write("Warning: Input source does not use the delog module\n") 32 | os.exit(0) 33 | end 34 | 35 | local delog_pattern = "(" .. varname .. "%.[%a]+%s*%b())" 36 | io.write((io.read("*a"):gsub(delog_pattern, "--[[ %1 ]]"))) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | delog 2 | ===== 3 | 4 | `delog` is a fast, extensible, optionally zero-impact logging module for 5 | [Lua](http://www.lua.org). 6 | 7 | Quickstart 8 | ---------- 9 | 10 | ```lua 11 | -- Import and configure the module 12 | local log = require("delog").level("trace") 13 | 14 | local levels = { "trace", "debug", "info", "warn", "error", "fatal" } 15 | 16 | for _, level in ipairs(levels) do 17 | log[level]("This is a message of '${}' level", level) 18 | end 19 | ``` 20 | 21 | Features 22 | -------- 23 | 24 | * Multiple logging levels: `trace`, `debug`, `info`, `warn`, `error`, and 25 | `fatal`. 26 | 27 | * Lazy evaluation of log format arguments. 28 | 29 | * Convenient log format string formatting, supporting 30 | [string.format](http://www.lua.org/manual/5.3/manual.html#pdf-string.format) 31 | modifiers (`"${%i}"`), built-in pretty-printers for tables (`"${%p}"` 32 | and `"${%pp}"`), and table field extraction (`"${fieldname}"`). 33 | 34 | * Support for plugging-in additional user-provided formatters. 35 | 36 | * `delog` filter for stripping logging calls from Lua sources. The filter 37 | takes Lua source code as input, removed the calls to functions in the 38 | `delog` module. Stripped sources can then be used to run applications 39 | for which logging introduces performance issues. 40 | 41 | * Colored console output, with optional automatic detection of terminals 42 | (requires [lua-isatty](https://bitbucket.org/telemachus/lua-isatty), 43 | [lua-term](https://github.com/hoelzro/lua-term), 44 | [luaposix](https://github.com/luaposix/luaposix), or the 45 | [LuaJIT FFI](http://luajit.org/ext_ffi_api.html)). 46 | 47 | 48 | Usage 49 | ----- 50 | 51 | ### `delog.level(l)` 52 | 53 | Sets the logging level to `l`, which should be one of: 54 | 55 | 1. `"trace"` 56 | 2. `"debug"` 57 | 3. `"info"` 58 | 4. `"warn"` 59 | 5. `"error"` 60 | 6. `"fatal"` 61 | 62 | By default the logging level is `"warn"`. Enabling a certain level also 63 | enables the rest of levels over it, e.g. enabling `"error"` also enables 64 | `"fatal"`, and enabling `"trace"` enables _all_ logging levels. 65 | 66 | ### `delog.prepend(s)` 67 | 68 | Sets the string which gets prepended to each log line. The default value 69 | produces lines like the following, optionally coloered: 70 | 71 | [WARN 20:45:14] This is a log message. 72 | 73 | This is achieved with the following “prepend” format string (which is 74 | available as `delog.PREPEND_DEFAULT`): 75 | 76 | "${%color%}[${%level-upper%} ${%time-hhmmss%}] ${%nocolor%}" 77 | 78 | Additionally, the module provides two more “prepend” format strings ready to 79 | use, aimed for debugging: 80 | 81 | - `delog.PREPEND_DEBUG`, same as the default, plus the source file name and 82 | line where the logging function was called. 83 | - `delog.PREPEND_DEBUG_FUNC`, same as `PREPEND_DEBUG`, plus the name of the 84 | function where the logging function was called (if available). 85 | 86 | **Tip:** If you are planning to use `delog` for debugging, probably you want 87 | to load the module as follows: 88 | 89 | ```lua 90 | local log = require("delog").level("debug") 91 | log.prepend(log.PREPEND_DEBUG) -- or PREPEND_DEBUG_FUNC 92 | ``` 93 | 94 | ### `delog.append(s)` 95 | 96 | Sets the string which gets appended to each log line. The default value is 97 | a newline character (`"\n"`). 98 | 99 | 100 | ### `delog.color(flag)` 101 | 102 | Manually sets whether to use ANSI color escape sequences. This is useful if 103 | the module cannot detect by itself whether the output is being sent to 104 | a terminal, then colors can still be obtained by using this function: 105 | 106 | ```lua 107 | local log = require("delog").color(true) 108 | log.warn("This will be always colored") 109 | ``` 110 | 111 | ### `delog.output(file)` 112 | 113 | Sets the logging output. If a string is passed, it is interpreted as a 114 | file path, and the file will be opened in _append mode_. By default, 115 | output is sent to `io.stderr`. 116 | 117 | 118 | ### `delog.format(name, func)` 119 | 120 | Registers a custom formatting function (`func`), with a given format `name`. 121 | This allows client code to supply additional format specifiers. Example: 122 | 123 | ```lua 124 | local log = require("delog").format("upper", function (value) 125 | return tostring(value):upper() 126 | end) 127 | log.warn("${%upper}", "this will be output in upper case") 128 | ``` 129 | 130 | ### `delog.trace(format, ...)` 131 | 132 | Formats a message and writes and logs it in the `"trace"` level. 133 | 134 | ### `delog.debug(format, ...)` 135 | 136 | Formats a message and writes and logs it in the `"debug"` level. 137 | 138 | ### `delog.info(format, ...)` 139 | 140 | Formats a message and writes and logs it in the `"info"` level. 141 | 142 | ### `delog.warn(format, ...)` 143 | 144 | Formats a message and writes and logs it in the `"warn"` level. 145 | 146 | ### `delog.error(format, ...)` 147 | 148 | Formats a message and writes and logs it in the `"error"` level. 149 | 150 | ### `delog.fatal(format, ...)` 151 | 152 | Formats a message and writes and logs it in the `"fatal"` level. 153 | -------------------------------------------------------------------------------- /delog.lua: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env lua 2 | -- 3 | -- delog.lua 4 | -- Copyright (C) 2016 Adrian Perez 5 | -- Licensed under the MIT license or, at your option the Apache-2.0 license. 6 | -- 7 | 8 | local isatty = (function () 9 | local choices = { 10 | { module = "isatty", func = "isatty" }, 11 | { module = "term", func = "isatty" }, 12 | { module = "posix", func = 13 | function (P) 14 | local isatty, fileno = P.isatty, P.fileno 15 | return function (file) 16 | return isatty(fileno(file)) == 1 17 | end 18 | end }, 19 | { module = "ffi", func = 20 | function (ffi) 21 | -- 22 | -- With the FFI module (likely, running LuaJIT), allows us to hack 23 | -- our way to retrieve the FILE* from the file userdata, and use 24 | -- fileno() on that. 25 | -- 26 | ffi.cdef [[ int fileno (void *fp); int isatty (int fd); ]] 27 | local ffi_cast, C = ffi.cast, ffi.C 28 | local voidp = ffi.typeof("void*") 29 | return function (file) 30 | return C.isatty(C.fileno(ffi_cast(voidp, file))) == 1 31 | end 32 | end }, 33 | } 34 | for _, choice in pairs(choices) do 35 | local ok, mod = pcall(require, choice.module) 36 | if ok then 37 | if type(choice.func) == "string" then 38 | return mod[choice.func] 39 | else 40 | local ok, func = pcall(choice.func, mod) 41 | if ok then 42 | return func 43 | end 44 | end 45 | end 46 | end 47 | -- None found, do not ever use colors by default 48 | return function (file) return false end 49 | end)() 50 | 51 | 52 | local delog = {} 53 | local format = {} 54 | local level = "warn" 55 | local color = false 56 | local output = false 57 | local prepend = "${%color%}[${%level-upper%} ${%time-hhmmss%}]${%nocolor%} " 58 | local append = "\n" 59 | 60 | delog.PREPEND_DEFAULT, delog.PREPEND_DEBUG = prepend, prepend .. "${%debug-location%}: " 61 | delog.PREPEND_DEBUG_FUNC = prepend .. "${%debug-func-location%}(): " 62 | 63 | local str_match, str_gsub, str_sub = string.match, string.gsub, string.sub 64 | local str_upper, str_format = string.upper, string.format 65 | local table_insert, table_concat = table.insert, table.concat 66 | local _type, _pairs, _tostring, _tonumber = type, pairs, tostring, tonumber 67 | local _pcall, _error = pcall, error 68 | local io_open = io.open 69 | local os_date = os.date 70 | 71 | 72 | function delog.level(name) 73 | if not delog[name] then 74 | _error("#1: Invalid level name: " .. _tostring(name)) 75 | end 76 | level = name 77 | return delog 78 | end 79 | 80 | function delog.prepend(format) 81 | prepend = format 82 | return delog 83 | end 84 | 85 | function delog.append(format) 86 | append = format 87 | return delog 88 | end 89 | 90 | function delog.color(enable) 91 | if output ~= false then 92 | if enable == nil then 93 | color = isatty(output) 94 | else 95 | color = enable 96 | end 97 | end 98 | return delog 99 | end 100 | 101 | function delog.output(file) 102 | if file == nil then 103 | file = io.stderr 104 | elseif _type(file) == "string" then 105 | file = io_open(file, "a") 106 | end 107 | 108 | if _type(file.write) ~= "function" then 109 | _error("#1: Argument does not have a :write() method") 110 | end 111 | 112 | output = file 113 | return delog.color() 114 | end 115 | 116 | function delog.format(name, func) 117 | if _type(name) ~= "string" then 118 | _error("#1: Argument is not a string") 119 | end 120 | if str_sub(name, -1) == "%" then 121 | _error("#1: Formatter names cannot end in '%'") 122 | end 123 | if _type(func) ~= "function" then 124 | _error("#2: Argument is not a function") 125 | end 126 | format[name] = func 127 | return delog 128 | end 129 | 130 | local colors = {} 131 | 132 | -- 133 | -- Built-in formatters 134 | -- 135 | format["level%"] = function (l) return str_format("%-5s", l) end 136 | format["level-upper%"] = function (l) return str_format("%-5s", str_upper(l)) end 137 | format["nocolor%"] = function (l) return color and "\27[0m" or "" end 138 | format["color%"] = function (l) return color and colors[l] or "" end 139 | format["time-hhmmss%"] = function (l) return os_date("%H:%S:%M") end 140 | 141 | -- 142 | -- Formatters which use the debug library 143 | -- 144 | local dbg_getinfo = debug.getinfo 145 | format["debug-location%"] = function (l) 146 | local info = dbg_getinfo(5, "Sl") 147 | return info.short_src .. ":" .. info.currentline 148 | end 149 | format["debug-func-location%"] = function (l) 150 | local info = dbg_getinfo(5, "Sln") 151 | return str_format("%s:%i:%s", info.short_src, info.currentline, info.name) 152 | end 153 | 154 | -- TODO: Handle cycles in tables. 155 | local function do_pprint (value) 156 | if _type(value) == "table" then 157 | local items = {} 158 | for k, v in _pairs(value) do 159 | table_insert(items, str_format("%s=%s", k, do_pprint(v))) 160 | end 161 | return "{ " .. table_concat(items, ", ") .. " }" 162 | elseif _type(value) == "string" then 163 | return str_format("%q", value) 164 | else 165 | return _tostring(value) 166 | end 167 | end 168 | format.p = do_pprint 169 | 170 | 171 | local function do_pprint_indent (value, indent) 172 | if _type(value) == "table" then 173 | local items = {} 174 | for k, v in _pairs(value) do 175 | table_insert(items, str_format("%s = %s", k, do_pprint_indent(v, indent .. " "))) 176 | end 177 | return "{\n " .. indent .. table_concat(items, ",\n " .. indent) .. "\n" .. indent .. "}" 178 | elseif _type(value) == "string" then 179 | return str_format("%q", value) 180 | else 181 | return _tostring(value) 182 | end 183 | end 184 | format.pp = function (value) return do_pprint_indent(value, "") end 185 | 186 | 187 | local spec_pattern = "^([^%%]*)%%?(.*)$" 188 | 189 | local function interpolate (fmtstring, ...) 190 | local current_index = 1 191 | local args = { ... } 192 | 193 | return (str_gsub(fmtstring, "%$%{(.-)}", function (spec) 194 | local element, conversion = str_match(spec, spec_pattern) 195 | 196 | local value 197 | if #element == 0 then 198 | -- Pick from current_index without increment 199 | value = args[current_index] 200 | elseif element == "." then 201 | -- Current index with increment 202 | value = args[current_index] 203 | current_index = current_index + 1 204 | else 205 | local index = _tonumber(element) 206 | if index then 207 | -- Numeric index 208 | value = args[index] 209 | else 210 | -- Named index 211 | local table = args[current_index] 212 | if str_sub(element, 1, 1) == "." then 213 | value = table[str_sub(element, 2)] 214 | current_index = current_index + 1 215 | else 216 | value = table[element] 217 | end 218 | end 219 | end 220 | 221 | if #conversion == 0 then 222 | return _tostring(value) 223 | elseif format[conversion] then 224 | return format[conversion](value) 225 | else 226 | local ok, result = _pcall(str_format, "%" .. conversion, value) 227 | return ok and result or ("${" .. spec .. "}") 228 | end 229 | end)) 230 | end 231 | 232 | 233 | local levels = {} 234 | local function make_logger(code, name, color) 235 | levels[name], colors[name] = code, color 236 | --colors[name] = color 237 | delog[name] = function (fmtstring, arg1, ...) 238 | if code < levels[level] then 239 | return 240 | end 241 | if prepend then 242 | output:write(interpolate(prepend, name)) 243 | end 244 | if _type(arg1) == "function" then 245 | -- Arguments are retrieved by invoking the function 246 | output:write(interpolate(fmtstring, arg1())) 247 | else 248 | output:write(interpolate(fmtstring, arg1, ...)) 249 | end 250 | if append then 251 | output:write(interpolate(append, name)) 252 | end 253 | output:flush() 254 | end 255 | end 256 | 257 | for i, m in ipairs {{ "trace", "\27[34m" }; 258 | { "debug", "\27[36m" }; 259 | { "info" , "\27[32m" }; 260 | { "warn" , "\27[33m" }; 261 | { "error", "\27[31m" }; 262 | { "fatal", "\27[35m" }} do 263 | make_logger(i, m[1], m[2]) 264 | end 265 | return delog.output() 266 | --------------------------------------------------------------------------------