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 
145 class CFileLogger : CRawFileLogger
146 {
147 	this(string name, bool timestampedFilenames = false)
148 	{
149 		super(name, timestampedFilenames);
150 	}
151 
152 	override void log(in char[] str)
153 	{
154 		auto ut = getLogTime();
155 		if (ut.day != currentDay)
156 		{
157 			f.writeln("\n---- (continued in next day's log) ----");
158 			f.close();
159 			open();
160 			f.writeln("---- (continued from previous day's log) ----\n");
161 		}
162 
163 		enum TIMEBUFSIZE = 1 + timeFormatSize(TIME_FORMAT) + 2;
164 		static char[TIMEBUFSIZE] buf = "[";
165 		auto writer = BlindWriter!char(buf.ptr+1);
166 		putTime!TIME_FORMAT(writer, ut);
167 		writer.put(']');
168 		writer.put(' ');
169 
170 		super.logStartLine();
171 		super.logFragment(buf[0..writer.ptr-buf.ptr]);
172 		super.logFragment(str);
173 		super.logEndLine();
174 	}
175 
176 	override void close()
177 	{
178 		//assert(f !is null);
179 		if (f.isOpen)
180 			f.close();
181 	}
182 
183 private:
184 	int currentDay;
185 
186 protected:
187 	final override void open()
188 	{
189 		super.open();
190 		currentDay = getLogTime().day;
191 		f.writef("\n\n--------------- %s ---------------\n\n\n", getLogTime().formatTime!(TIME_FORMAT)());
192 		f.flush();
193 	}
194 
195 	final override void reopen()
196 	{
197 		super.reopen();
198 		f.writef("\n\n--------------- %s ---------------\n\n\n", getLogTime().formatTime!(TIME_FORMAT)());
199 		f.flush();
200 	}
201 }
202 alias RCClass!CFileLogger FileLogger;
203 alias rcClass!CFileLogger fileLogger;
204 
205 class CConsoleLogger : CLogger
206 {
207 	this(string name)
208 	{
209 		super(name);
210 	}
211 
212 	override void log(in char[] str)
213 	{
214 		stderr.write(name, ": ", str, "\n");
215 		stderr.flush();
216 	}
217 }
218 alias RCClass!CConsoleLogger ConsoleLogger;
219 alias rcClass!CConsoleLogger consoleLogger;
220 
221 class CNullLogger : CLogger
222 {
223 	this() { super(null); }
224 	override void log(in char[] str) {}
225 }
226 alias RCClass!CNullLogger NullLogger;
227 alias rcClass!CNullLogger nullLogger;
228 
229 class CMultiLogger : CLogger
230 {
231 	this(Logger[] loggers ...)
232 	{
233 		this.loggers = loggers.dup;
234 		super(null);
235 	}
236 
237 	override void log(in char[] str)
238 	{
239 		foreach (logger; loggers)
240 			logger.log(str);
241 	}
242 
243 	override void rename(string name)
244 	{
245 		foreach (logger; loggers)
246 			logger.rename(name);
247 	}
248 
249 	override void close()
250 	{
251 		foreach (logger; loggers)
252 			logger.close();
253 	}
254 
255 private:
256 	Logger[] loggers;
257 }
258 alias RCClass!CMultiLogger MultiLogger;
259 alias rcClass!CMultiLogger multiLogger;
260 
261 class CFileAndConsoleLogger : CMultiLogger
262 {
263 	this(string name)
264 	{
265 		Logger f, c;
266 		f = fileLogger(name);
267 		c = consoleLogger(name);
268 		super(f, c);
269 	}
270 }
271 alias RCClass!CFileAndConsoleLogger FileAndConsoleLogger;
272 alias rcClass!CFileAndConsoleLogger fileAndConsoleLogger;
273 
274 bool quiet;
275 
276 shared static this()
277 {
278 	import core.runtime;
279 	foreach (arg; Runtime.args[1..$])
280 		if (arg == "-q" || arg == "--quiet")
281 			quiet = true;
282 }
283 
284 /// Create a logger depending on whether -q or --quiet was passed on the command line.
285 Logger createLogger(string name)
286 {
287 	Logger result;
288 	version (unittest)
289 		result = consoleLogger(name);
290 	else
291 		if (quiet)
292 			result = fileLogger(name);
293 		else
294 			result = fileAndConsoleLogger(name);
295 	return result;
296 }