aboutsummaryrefslogtreecommitdiffstats
path: root/core/loggingmanager.lua
blob: a8f6e1dc73a1a6429f21b68185e8e71833624610 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240

local format, rep = string.format, string.rep;
local pcall = pcall;
local debug = debug;
local tostring, setmetatable, rawset, pairs, ipairs, type = 
	tostring, setmetatable, rawset, pairs, ipairs, type;
local io_open, io_write = io.open, io.write;
local math_max, rep = math.max, string.rep;
local os_date, os_getenv = os.date, os.getenv;
local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring;

local config = require "core.configmanager";
local eventmanager = require "core.eventmanager";
local logger = require "util.logger";
local debug_mode = config.get("*", "core", "debug");

_G.log = logger.init("general");

module "loggingmanager"

-- The log config used if none specified in the config file
local default_logging = { { to = "console" } };
local default_file_logging = { { to = "file", levels = { min = (debug_mode and "debug") or "info" } } };
local default_timestamp = "%b %d %T";
-- The actual config loggingmanager is using
local logging_config = config.get("*", "core", "log") or default_logging;

local apply_sink_rules;
local log_sink_types = setmetatable({}, { __newindex = function (t, k, v) rawset(t, k, v); apply_sink_rules(k); end; });
local get_levels;
local logging_levels = { "debug", "info", "warn", "error", "critical" }

-- Put a rule into action. Requires that the sink type has already been registered.
-- This function is called automatically when a new sink type is added [see apply_sink_rules()]
local function add_rule(sink_config)
	local sink_maker = log_sink_types[sink_config.to];
	if sink_maker then
		if sink_config.levels and not sink_config.source then
			-- Create sink
			local sink = sink_maker(sink_config);
			
			-- Set sink for all chosen levels
			for level in pairs(get_levels(sink_config.levels)) do
				logger.add_level_sink(level, sink);
			end
		elseif sink_config.source and not sink_config.levels then
			logger.add_name_sink(sink_config.source, sink_maker(sink_config));
		elseif sink_config.source and sink_config.levels then
			local levels = get_levels(sink_config.levels);
			local sink = sink_maker(sink_config);
			logger.add_name_sink(sink_config.source,
				function (name, level, ...)
					if levels[level] then
						return sink(name, level, ...);
					end
				end);
		else
			-- All sources
			-- Create sink
			local sink = sink_maker(sink_config);
			
			-- Set sink for all levels
			for _, level in pairs(logging_levels) do
				logger.add_level_sink(level, sink);
			end
		end
	else
		-- No such sink type
	end
end

-- Search for all rules using a particular sink type, and apply
-- them. Called automatically when a new sink type is added to
-- the log_sink_types table.
function apply_sink_rules(sink_type)
	if type(logging_config) == "table" then
		for _, sink_config in pairs(logging_config) do
			if sink_config.to == sink_type then
				add_rule(sink_config);
			end
		end
	elseif type(logging_config) == "string" and (not logging_config:match("^%*")) and sink_type == "file" then
		-- User specified simply a filename, and the "file" sink type 
		-- was just added
		for _, sink_config in pairs(default_file_logging) do
			sink_config.filename = logging_config;
			add_rule(sink_config);
			sink_config.filename = nil;
		end
	elseif type(logging_config) == "string" and logging_config:match("^%*(.+)") == sink_type then
		-- Log all levels (debug+) to this sink
		add_rule({ levels = { min = "debug" }, to = sink_type });
	end
end



--- Helper function to get a set of levels given a "criteria" table
function get_levels(criteria, set)
	set = set or {};
	if type(criteria) == "string" then
		set[criteria] = true;
		return set;
	end
	local min, max = criteria.min, criteria.max;
	if min or max then
		local in_range;
		for _, level in ipairs(logging_levels) do
			if min == level then
				set[level] = true;
				in_range = true;
			elseif max == level then
				set[level] = true;
				return set;
			elseif in_range then
				set[level] = true;
			end	
		end
	end
	
	for _, level in ipairs(criteria) do
		set[level] = true;
	end
	return set;
end

--- Definition of built-in logging sinks ---

-- Null sink, must enter log_sink_types *first*
function log_sink_types.nowhere()
	return function () return false; end;
end

-- Column width for "source" (used by stdout and console)
local sourcewidth = 20;

function log_sink_types.stdout()
	local timestamps = config.timestamps;
	
	if timestamps == true then
		timestamps = default_timestamp; -- Default format
	end
	
	return function (name, level, message, ...)
		sourcewidth = math_max(#name+2, sourcewidth);
		local namelen = #name;
		if timestamps then
			io_write(os_date(timestamps), " ");
		end
		if ... then 
			io_write(name, rep(" ", sourcewidth-namelen), level, "\t", format(message, ...), "\n");
		else
			io_write(name, rep(" ", sourcewidth-namelen), level, "\t", message, "\n");
		end
	end	
end

do
	local do_pretty_printing = not os_getenv("WINDIR");
	
	local logstyles = {};
	if do_pretty_printing then
		logstyles["info"] = getstyle("bold");
		logstyles["warn"] = getstyle("bold", "yellow");
		logstyles["error"] = getstyle("bold", "red");
	end
	function log_sink_types.console(config)
		-- Really if we don't want pretty colours then just use plain stdout
		if not do_pretty_printing then
			return log_sink_types.stdout(config);
		end
		
		local timestamps = config.timestamps;

		if timestamps == true then
			timestamps = default_timestamp; -- Default format
		end

		return function (name, level, message, ...)
			sourcewidth = math_max(#name+2, sourcewidth);
			local namelen = #name;
			if timestamps then
				io_write(os_date(timestamps), " ");
			end
			if ... then 
				io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", format(message, ...), "\n");
			else
				io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", message, "\n");
			end
		end
	end
end

local empty_function = function () end;
function log_sink_types.file(config)
	local log = config.filename;
	local logfile = io_open(log, "a+");
	if not logfile then
		return empty_function;
	end
	local write, flush = logfile.write, logfile.flush;

	eventmanager.add_event_hook("reopen-log-files", function ()
			if logfile then
				logfile:close();
			end
			logfile = io_open(log, "a+");
			if not logfile then
				write, flush = empty_function, empty_function;
			else
				write, flush = logfile.write, logfile.flush;
			end
		end);

	local timestamps = config.timestamps;

	if timestamps == true then
		timestamps = default_timestamp; -- Default format
	end

	return function (name, level, message, ...)
		if timestamps then
			write(logfile, os_date(timestamps), " ");
		end
		if ... then 
			write(logfile, name, "\t", level, "\t", format(message, ...), "\n");
		else
			write(logfile, name, "\t" , level, "\t", message, "\n");
		end
		flush(logfile);
	end;
end

function register_sink_type(name, sink_maker)
	local old_sink_maker = log_sink_types[name];
	log_sink_types[name] = sink_maker;
	return old_sink_maker;
end

return _M;