changeset 4205:481c4d75e77d

mod_log_ringbuffer: New module to send logs to an in-memory ringbuffer
author Matthew Wild <mwild1@gmail.com>
date Thu, 15 Oct 2020 16:47:21 +0100
parents a5930a185806
children 432e948cd50e
files mod_log_ringbuffer/README.markdown mod_log_ringbuffer/mod_log_ringbuffer.lua
diffstat 2 files changed, 175 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /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.
--- /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);