# HG changeset patch # User Matthew Wild # Date 1602776841 -3600 # Node ID 481c4d75e77de46cb043435a42de0a482cd888bd # Parent a5930a1858066bce795895a445181c586e373427 mod_log_ringbuffer: New module to send logs to an in-memory ringbuffer diff -r a5930a185806 -r 481c4d75e77d mod_log_ringbuffer/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_log_ringbuffer/README.markdown Thu Oct 15 16:47:21 2020 +0100 @@ -0,0 +1,90 @@ +--- +labels: +- 'Stage-Beta' +summary: 'Log to in-memory ringbuffer' +... + +Introduction +============ + +Sometimes debug logs are too verbose for continuous logging to disk. However +occasionally you may be interested in the debug logs when a certain event occurs. + +This module allows you to store all logs in a fixed-size buffer in Prosody's memory, +and dump them to a file whenever you want. + +# Configuration + +First of all, you need to load the module by adding it to your global `modules_enabled`: + +``` {.lua} +modules_enabled = { + ... + "log_ringbuffer"; + ... +} +``` + +By default the module will do nothing - you need to configure a log sink, using Prosody's +usual [logging configuration](https://prosody.im/doc/advanced_logging). + +``` {.lua} +log = { + -- Log errors to a file + error = "/var/log/prosody/prosody.err"; + + -- Log debug and higher to a 2MB buffer + { level = "debug", to = "ringbuffer", size = 1024*1024*2, filename = "debug-logs-{pid}-{count}.log", signal = "SIGUSR2" }; +} +``` + +The possible fields of the logging config entry are: + +`to` +: Set this to `"ringbuffer"`. + +`level` +: The minimum log level to capture, e.g. `"debug"`. + +`size` +: The size, in bytes, of the buffer. When the buffer fills, + old data will be overwritten by new data. + +`filename` +: The name of the file to dump logs to when triggered. The filename may + contain a number of variables, described below. + +Only one of the following triggers may be specified: + +`signal` +: A signal that will cause the buffer to be dumped, e.g. `"SIGUSR2"`. + Do not use any signal that is used by any other Prosody module, to + avoid conflicts. + +`event` +: Alternatively, the name of a Prosody global event that will trigger + the logs to be dumped, e.g. `"config-reloaded"`. + +## Filename variables + +`pid` +: The PID of the current process + +`count` +: A counter that begins at 0 and increments for each dump made by + the current process. + +`time` +: The unix timestamp at which the dump is made. It can be formatted + to human-readable local time using `{time|yyyymmdd}` and `{time|hhmmss}`. + +`paths` +: Allows access to Prosody's known filesystem paths, use e.g. `{paths.data}` + for the path to Prosody's data directory. + +The filename does not have to be unique for every dump - if a file with the same +name already exists, it will be appended to. + +# Compatibility + +0.11 and later. diff -r a5930a185806 -r 481c4d75e77d mod_log_ringbuffer/mod_log_ringbuffer.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_log_ringbuffer/mod_log_ringbuffer.lua Thu Oct 15 16:47:21 2020 +0100 @@ -0,0 +1,85 @@ +module:set_global(); + +local loggingmanager = require "core.loggingmanager"; +local format = require "util.format".format; +local pposix = require "util.pposix"; +local rb = require "util.ringbuffer"; + +local default_timestamp = "%b %d %H:%M:%S "; +local max_chunk_size = module:get_option_number("log_ringbuffer_chunk_size", 16384); + +local os_date = os.date; + +local default_filename_template = "ringbuffer-logs-{pid}-{count}.log"; +local render_filename = require "util.interpolation".new("%b{}", function (s) return s; end, { + yyyymmdd = function (t) + return os_date("%Y%m%d", t); + end; + hhmmss = function (t) + return os_date("%H%M%S", t); + end; +}); + +local dump_count = 0; + +local function dump_buffer(buffer, filename) + dump_count = dump_count + 1; + local f, err = io.open(filename, "a+"); + if not f then + module:log("error", "Unable to open output file: %s", err); + return; + end + local bytes_remaining = buffer:length(); + f:write(("-- Dumping %d bytes at %s --\n"):format(bytes_remaining, os_date(default_timestamp))); + while bytes_remaining > 0 do + local chunk_size = math.min(bytes_remaining, max_chunk_size); + local chunk = buffer:read(chunk_size); + if not chunk then + f:write("-- Dump aborted due to error --\n\n"); + f:close(); + return; + end + f:write(chunk); + bytes_remaining = bytes_remaining - chunk_size; + end + f:write("-- End of dump --\n\n"); + f:close(); +end + +local function get_filename(filename_template) + filename_template = filename_template or default_filename_template; + return render_filename(filename_template, { + paths = prosody.paths; + pid = pposix.getpid(); + count = dump_count; + time = os.time(); + }); +end + +local function ringbuffer_log_sink_maker(sink_config) + local buffer = rb.new(sink_config.size or 100*1024); + + local timestamps = sink_config.timestamps; + + if timestamps == true or timestamps == nil then + timestamps = default_timestamp; -- Default format + elseif timestamps then + timestamps = timestamps .. " "; + end + + local function dump() + dump_buffer(buffer, get_filename(sink_config.filename)); + end + + if sink_config.signal then + require "util.signal".signal(sink_config.signal, dump); + elseif sink_config.event then + module:hook_global(sink_config.global_event, dump); + end + + return function (name, level, message, ...) + buffer:write(format("%s%s\t%s\t%s\n", timestamps and os_date(timestamps) or "", name, level, format(message, ...))); + end; +end + +loggingmanager.register_sink_type("ringbuffer", ringbuffer_log_sink_maker);