1 /**
2  * Logging support.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  *   Simon Arlott
13  */
14 
15 module ae.sys.log;
16 
17 import std.datetime;
18 import std.file;
19 import std.path;
20 import std.stdio;
21 import std.string;
22 
23 import ae.sys.file;
24 
25 import ae.utils.meta.rcclass;
26 import ae.utils.textout;
27 import ae.utils.time;
28 
29 /// Directory where log files will be saved.
30 /// The default is "logs".
31 string logDir;
32 
33 private void init()
34 {
35 	if (!logDir)
36 		logDir = getcwd().buildPath("logs");
37 }
38 
39 shared static this() { init(); }
40 static this() { init(); }
41 
42 /// Default time format used for timestamps.
43 enum TIME_FORMAT = "Y-m-d H:i:s.u";
44 
45 private SysTime getLogTime()
46 {
47 	return Clock.currTime(UTC());
48 }
49 
50 /// Base logger class.
51 abstract class CLogger
52 {
53 public:
54 	this(string name)
55 	{
56 		this.name = name;
57 		open();
58 	} ///
59 
60 	/// Log a line.
61 	abstract void log(in char[] str);
62 	alias opCall = log; /// ditto
63 
64 	/// Change the output (rotate) the log file.
65 	void rename(string name)
66 	{
67 		close();
68 		this.name = name;
69 		open();
70 	}
71 
72 	/// Close the log ifel.
73 	void close() {}
74 
75 protected:
76 	string name;
77 
78 	void open() {}
79 	void reopen() {}
80 }
81 alias Logger = RCClass!CLogger; /// ditto
82 
83 /// File logger without formatting.
84 class CRawFileLogger : CLogger
85 {
86 	bool timestampedFilenames; /// Whether to include the current time in the log file name.
87 
88 	this(string name, bool timestampedFilenames = false)
89 	{
90 		this.timestampedFilenames = timestampedFilenames;
91 		super(name);
92 	} ///
93 
94 	private final void logStartLine()
95 	{
96 	/+
97 		if (!f.isOpen) // hack
98 		{
99 			if (fileName is null)
100 				throw new Exception("Can't write to a closed log");
101 			reopen();
102 			RawFileLogger.log(str);
103 			close();
104 		}
105 	+/
106 	}
107 
108 	private final void logFragment(in char[] str)
109 	{
110 		f.write(str);
111 	}
112 
113 	private final void logEndLine()
114 	{
115 		f.writeln();
116 		f.flush();
117 	}
118 
119 	override void log(in char[] str)
120 	{
121 		logStartLine();
122 		logFragment(str);
123 		logEndLine();
124 	} ///
125 
126 protected:
127 	string fileName;
128 	File f;
129 
130 	override void open()
131 	{
132 		// name may contain directory separators
133 		string path = buildPath(logDir, name);
134 		auto base = path.baseName();
135 		auto dir = path.dirName();
136 
137 		auto t = getLogTime();
138 		string timestamp = timestampedFilenames ? format(" %02d-%02d-%02d", t.hour, t.minute, t.second) : null;
139 		fileName = buildPath(dir, format("%04d-%02d-%02d%s - %s.log", t.year, t.month, t.day, timestamp, base));
140 		ensurePathExists(fileName);
141 		f = File(fileName, "ab");
142 	}
143 
144 	override void reopen()
145 	{
146 		f = File(fileName, "ab");
147 	}
148 }
149 alias RawFileLogger = RCClass!CRawFileLogger; /// ditto
150 alias rawFileLogger = rcClass!CRawFileLogger; /// ditto
151 
152 /// Basic file logger.
153 class CFileLogger : CRawFileLogger
154 {
155 	this(string name, bool timestampedFilenames = false)
156 	{
157 		super(name, timestampedFilenames);
158 	} ///
159 
160 	override void log(in char[] str)
161 	{
162 		auto ut = getLogTime();
163 		if (ut.day != currentDay)
164 		{
165 			f.writeln("\n---- (continued in next day's log) ----");
166 			f.close();
167 			open();
168 			f.writeln("---- (continued from previous day's log) ----\n");
169 		}
170 
171 		enum TIMEBUFSIZE = 1 + timeFormatSize(TIME_FORMAT) + 2;
172 		static char[TIMEBUFSIZE] buf = "[";
173 		auto writer = BlindWriter!char(buf.ptr+1);
174 		putTime!TIME_FORMAT(writer, ut);
175 		writer.put(']');
176 		writer.put(' ');
177 
178 		super.logStartLine();
179 		super.logFragment(buf[0..writer.ptr-buf.ptr]);
180 		super.logFragment(str);
181 		super.logEndLine();
182 	} ///
183 
184 	override void close()
185 	{
186 		//assert(f !is null);
187 		if (f.isOpen)
188 			f.close();
189 	} ///
190 
191 private:
192 	int currentDay;
193 
194 protected:
195 	final override void open()
196 	{
197 		super.open();
198 		currentDay = getLogTime().day;
199 		f.writef("\n\n--------------- %s ---------------\n\n\n", getLogTime().formatTime!(TIME_FORMAT)());
200 		f.flush();
201 	}
202 
203 	final override void reopen()
204 	{
205 		super.reopen();
206 		f.writef("\n\n--------------- %s ---------------\n\n\n", getLogTime().formatTime!(TIME_FORMAT)());
207 		f.flush();
208 	}
209 }
210 alias FileLogger = RCClass!CFileLogger; /// ditto
211 alias fileLogger = rcClass!CFileLogger; /// ditto
212 
213 /// Logs to the console (standard error).
214 class CConsoleLogger : CLogger
215 {
216 	this(string name)
217 	{
218 		super(name);
219 	} ///
220 
221 	override void log(in char[] str)
222 	{
223 		stderr.write(name, ": ", str, "\n");
224 		stderr.flush();
225 	} ///
226 }
227 alias ConsoleLogger = RCClass!CConsoleLogger; /// ditto
228 alias consoleLogger = rcClass!CConsoleLogger; /// ditto
229 
230 /// Logs to nowhere.
231 class CNullLogger : CLogger
232 {
233 	this() { super(null); } ///
234 	override void log(in char[] str) {} ///
235 }
236 alias NullLogger = RCClass!CNullLogger; /// ditto
237 alias nullLogger = rcClass!CNullLogger; /// ditto
238 
239 /// Logs to several other loggers.
240 class CMultiLogger : CLogger
241 {
242 	this(Logger[] loggers ...)
243 	{
244 		this.loggers = loggers.dup;
245 		super(null);
246 	} ///
247 
248 	override void log(in char[] str)
249 	{
250 		foreach (logger; loggers)
251 			logger.log(str);
252 	} ///
253 
254 	override void rename(string name)
255 	{
256 		foreach (logger; loggers)
257 			logger.rename(name);
258 	} ///
259 
260 	override void close()
261 	{
262 		foreach (logger; loggers)
263 			logger.close();
264 	} ///
265 
266 private:
267 	Logger[] loggers;
268 }
269 alias MultiLogger = RCClass!CMultiLogger; /// ditto
270 alias multiLogger = rcClass!CMultiLogger; /// ditto
271 
272 /// Logs to a file and the console.
273 class CFileAndConsoleLogger : CMultiLogger
274 {
275 	this(string name)
276 	{
277 		Logger f, c;
278 		f = fileLogger(name);
279 		c = consoleLogger(name);
280 		super(f, c);
281 	} ///
282 }
283 alias FileAndConsoleLogger = RCClass!CFileAndConsoleLogger; /// ditto
284 alias fileAndConsoleLogger = rcClass!CFileAndConsoleLogger; /// ditto
285 
286 bool quiet; /// True if "-q" or ~--quiet" is present on the command line.
287 
288 shared static this()
289 {
290 	import core.runtime : Runtime;
291 	foreach (arg; Runtime.args[1..$])
292 		if (arg == "-q" || arg == "--quiet")
293 			quiet = true;
294 }
295 
296 /// Create a logger depending on whether -q or --quiet was passed on the command line.
297 Logger createLogger(string name)
298 {
299 	Logger result;
300 	version (unittest)
301 		result = consoleLogger(name);
302 	else
303 	{
304 		if (quiet)
305 			result = fileLogger(name);
306 		else
307 			result = fileAndConsoleLogger(name);
308 	}
309 	return result;
310 }
311 
312 /// Create a logger using a user-supplied log directory or transport.
313 Logger createLogger(string name, string target)
314 {
315 	Logger result;
316 	switch (target)
317 	{
318 		case "/dev/stderr":
319 			result = consoleLogger(name);
320 			break;
321 		case "/dev/null":
322 			result = nullLogger();
323 			break;
324 		default:
325 			result = fileLogger(target.buildPath(name));
326 			break;
327 	}
328 	return result;
329 }