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 <vladimir@thecybershadow.net>
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 string logDir;
30 
31 private void init()
32 {
33 	import core.runtime;
34 
35 	if (!logDir)
36 		logDir = getcwd().buildPath("logs");
37 }
38 
39 shared static this() { init(); }
40 static this() { init(); }
41 
42 enum TIME_FORMAT = "Y-m-d H:i:s.u";
43 
44 private SysTime getLogTime()
45 {
46 	return Clock.currTime(UTC());
47 }
48 
49 abstract class CLogger
50 {
51 public:
52 	alias log opCall;
53 
54 	this(string name)
55 	{
56 		this.name = name;
57 		open();
58 	}
59 
60 	abstract void log(in char[] str);
61 
62 	void rename(string name)
63 	{
64 		close();
65 		this.name = name;
66 		open();
67 	}
68 
69 	void close() {}
70 
71 protected:
72 	string name;
73 
74 	void open() {}
75 	void reopen() {}
76 }
77 alias RCClass!CLogger Logger;
78 
79 class CRawFileLogger : CLogger
80 {
81 	bool timestampedFilenames;
82 
83 	this(string name, bool timestampedFilenames = false)
84 	{
85 		this.timestampedFilenames = timestampedFilenames;
86 		super(name);
87 	}
88 
89 	private final void logStartLine()
90 	{
91 	/+
92 		if (!f.isOpen) // hack
93 		{
94 			if (fileName is null)
95 				throw new Exception("Can't write to a closed log");
96 			reopen();
97 			RawFileLogger.log(str);
98 			close();
99 		}
100 	+/
101 	}
102 
103 	private final void logFragment(in char[] str)
104 	{
105 		f.write(str);
106 	}
107 
108 	private final void logEndLine()
109 	{
110 		f.writeln();
111 		f.flush();
112 	}
113 
114 	override void log(in char[] str)
115 	{
116 		logStartLine();
117 		logFragment(str);
118 		logEndLine();
119 	}
120 
121 protected:
122 	string fileName;
123 	File f;
124 
125 	override void open()
126 	{
127 		// name may contain directory separators
128 		string path = buildPath(logDir, name);
129 		auto base = path.baseName();
130 		auto dir = path.dirName();
131 
132 		auto t = getLogTime();
133 		string timestamp = timestampedFilenames ? format(" %02d-%02d-%02d", t.hour, t.minute, t.second) : null;
134 		fileName = buildPath(dir, format("%04d-%02d-%02d%s - %s.log", t.year, t.month, t.day, timestamp, base));
135 		ensurePathExists(fileName);
136 		f = File(fileName, "ab");
137 	}
138 
139 	override void reopen()
140 	{
141 		f = File(fileName, "ab");
142 	}
143 }
144 alias RCClass!CRawFileLogger RawFileLogger;
145 alias rcClass!CRawFileLogger rawFileLogger;
146 
147 class CFileLogger : CRawFileLogger
148 {
149 	this(string name, bool timestampedFilenames = false)
150 	{
151 		super(name, timestampedFilenames);
152 	}
153 
154 	override void log(in char[] str)
155 	{
156 		auto ut = getLogTime();
157 		if (ut.day != currentDay)
158 		{
159 			f.writeln("\n---- (continued in next day's log) ----");
160 			f.close();
161 			open();
162 			f.writeln("---- (continued from previous day's log) ----\n");
163 		}
164 
165 		enum TIMEBUFSIZE = 1 + timeFormatSize(TIME_FORMAT) + 2;
166 		static char[TIMEBUFSIZE] buf = "[";
167 		auto writer = BlindWriter!char(buf.ptr+1);
168 		putTime!TIME_FORMAT(writer, ut);
169 		writer.put(']');
170 		writer.put(' ');
171 
172 		super.logStartLine();
173 		super.logFragment(buf[0..writer.ptr-buf.ptr]);
174 		super.logFragment(str);
175 		super.logEndLine();
176 	}
177 
178 	override void close()
179 	{
180 		//assert(f !is null);
181 		if (f.isOpen)
182 			f.close();
183 	}
184 
185 private:
186 	int currentDay;
187 
188 protected:
189 	final override void open()
190 	{
191 		super.open();
192 		currentDay = getLogTime().day;
193 		f.writef("\n\n--------------- %s ---------------\n\n\n", getLogTime().formatTime!(TIME_FORMAT)());
194 		f.flush();
195 	}
196 
197 	final override void reopen()
198 	{
199 		super.reopen();
200 		f.writef("\n\n--------------- %s ---------------\n\n\n", getLogTime().formatTime!(TIME_FORMAT)());
201 		f.flush();
202 	}
203 }
204 alias RCClass!CFileLogger FileLogger;
205 alias rcClass!CFileLogger fileLogger;
206 
207 class CConsoleLogger : CLogger
208 {
209 	this(string name)
210 	{
211 		super(name);
212 	}
213 
214 	override void log(in char[] str)
215 	{
216 		stderr.write(name, ": ", str, "\n");
217 		stderr.flush();
218 	}
219 }
220 alias RCClass!CConsoleLogger ConsoleLogger;
221 alias rcClass!CConsoleLogger consoleLogger;
222 
223 class CNullLogger : CLogger
224 {
225 	this() { super(null); }
226 	override void log(in char[] str) {}
227 }
228 alias RCClass!CNullLogger NullLogger;
229 alias rcClass!CNullLogger nullLogger;
230 
231 class CMultiLogger : CLogger
232 {
233 	this(Logger[] loggers ...)
234 	{
235 		this.loggers = loggers.dup;
236 		super(null);
237 	}
238 
239 	override void log(in char[] str)
240 	{
241 		foreach (logger; loggers)
242 			logger.log(str);
243 	}
244 
245 	override void rename(string name)
246 	{
247 		foreach (logger; loggers)
248 			logger.rename(name);
249 	}
250 
251 	override void close()
252 	{
253 		foreach (logger; loggers)
254 			logger.close();
255 	}
256 
257 private:
258 	Logger[] loggers;
259 }
260 alias RCClass!CMultiLogger MultiLogger;
261 alias rcClass!CMultiLogger multiLogger;
262 
263 class CFileAndConsoleLogger : CMultiLogger
264 {
265 	this(string name)
266 	{
267 		Logger f, c;
268 		f = fileLogger(name);
269 		c = consoleLogger(name);
270 		super(f, c);
271 	}
272 }
273 alias RCClass!CFileAndConsoleLogger FileAndConsoleLogger;
274 alias rcClass!CFileAndConsoleLogger fileAndConsoleLogger;
275 
276 bool quiet;
277 
278 shared static this()
279 {
280 	import core.runtime;
281 	foreach (arg; Runtime.args[1..$])
282 		if (arg == "-q" || arg == "--quiet")
283 			quiet = true;
284 }
285 
286 /// Create a logger depending on whether -q or --quiet was passed on the command line.
287 Logger createLogger(string name)
288 {
289 	Logger result;
290 	version (unittest)
291 		result = consoleLogger(name);
292 	else
293 		if (quiet)
294 			result = fileLogger(name);
295 		else
296 			result = fileAndConsoleLogger(name);
297 	return result;
298 }