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 	if (!logDir)
34 		logDir = getcwd().buildPath("logs");
35 }
36 
37 shared static this() { init(); }
38 static this() { init(); }
39 
40 enum TIME_FORMAT = "Y-m-d H:i:s.u";
41 
42 private SysTime getLogTime()
43 {
44 	return Clock.currTime(UTC());
45 }
46 
47 abstract class CLogger
48 {
49 public:
50 	alias log opCall;
51 
52 	this(string name)
53 	{
54 		this.name = name;
55 		open();
56 	}
57 
58 	abstract void log(in char[] str);
59 
60 	void rename(string name)
61 	{
62 		close();
63 		this.name = name;
64 		open();
65 	}
66 
67 	void close() {}
68 
69 protected:
70 	string name;
71 
72 	void open() {}
73 	void reopen() {}
74 }
75 alias RCClass!CLogger Logger;
76 
77 class CRawFileLogger : CLogger
78 {
79 	bool timestampedFilenames;
80 
81 	this(string name, bool timestampedFilenames = false)
82 	{
83 		this.timestampedFilenames = timestampedFilenames;
84 		super(name);
85 	}
86 
87 	private final void logStartLine()
88 	{
89 	/+
90 		if (!f.isOpen) // hack
91 		{
92 			if (fileName is null)
93 				throw new Exception("Can't write to a closed log");
94 			reopen();
95 			RawFileLogger.log(str);
96 			close();
97 		}
98 	+/
99 	}
100 
101 	private final void logFragment(in char[] str)
102 	{
103 		f.write(str);
104 	}
105 
106 	private final void logEndLine()
107 	{
108 		f.writeln();
109 		f.flush();
110 	}
111 
112 	override void log(in char[] str)
113 	{
114 		logStartLine();
115 		logFragment(str);
116 		logEndLine();
117 	}
118 
119 protected:
120 	string fileName;
121 	File f;
122 
123 	override void open()
124 	{
125 		// name may contain directory separators
126 		string path = buildPath(logDir, name);
127 		auto base = path.baseName();
128 		auto dir = path.dirName();
129 
130 		auto t = getLogTime();
131 		string timestamp = timestampedFilenames ? format(" %02d-%02d-%02d", t.hour, t.minute, t.second) : null;
132 		fileName = buildPath(dir, format("%04d-%02d-%02d%s - %s.log", t.year, t.month, t.day, timestamp, base));
133 		ensurePathExists(fileName);
134 		f = File(fileName, "ab");
135 	}
136 
137 	override void reopen()
138 	{
139 		f = File(fileName, "ab");
140 	}
141 }
142 alias RCClass!CRawFileLogger RawFileLogger;
143 alias rcClass!CRawFileLogger rawFileLogger;
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 : 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 	{
292 		if (quiet)
293 			result = fileLogger(name);
294 		else
295 			result = fileAndConsoleLogger(name);
296 	}
297 	return result;
298 }
299 
300 /// Create a logger using a user-supplied log directory or transport.
301 Logger createLogger(string name, string target)
302 {
303 	Logger result;
304 	switch (target)
305 	{
306 		case "/dev/stderr":
307 			result = consoleLogger(name);
308 			break;
309 		case "/dev/null":
310 			result = nullLogger();
311 			break;
312 		default:
313 			result = fileLogger(target.buildPath(name));
314 			break;
315 	}
316 	return result;
317 }