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.textout;
26 import ae.utils.time;
27 
28 string logDir;
29 
30 private void init()
31 {
32 	import core.runtime;
33 
34 	if (!logDir)
35 		logDir = getcwd().buildPath("logs");
36 }
37 
38 shared static this() { init(); }
39 static this() { init(); }
40 
41 enum TIME_FORMAT = "Y-m-d H:i:s.u";
42 
43 private SysTime getLogTime()
44 {
45 	return Clock.currTime(UTC());
46 }
47 
48 abstract class Logger
49 {
50 public:
51 	alias log opCall;
52 
53 	this(string name)
54 	{
55 		this.name = name;
56 		open();
57 	}
58 
59 	abstract void log(in char[] str);
60 
61 	void rename(string name)
62 	{
63 		close();
64 		this.name = name;
65 		open();
66 	}
67 
68 	void close() {}
69 
70 protected:
71 	string name;
72 
73 	void open() {}
74 	void reopen() {}
75 }
76 
77 class RawFileLogger : Logger
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 
143 class FileLogger : RawFileLogger
144 {
145 	this(string name, bool timestampedFilenames = false)
146 	{
147 		super(name, timestampedFilenames);
148 	}
149 
150 	override void log(in char[] str)
151 	{
152 		auto ut = getLogTime();
153 		if (ut.day != currentDay)
154 		{
155 			f.writeln("\n---- (continued in next day's log) ----");
156 			f.close();
157 			open();
158 			f.writeln("---- (continued from previous day's log) ----\n");
159 		}
160 
161 		enum TIMEBUFSIZE = 1 + timeFormatSize(TIME_FORMAT) + 2;
162 		static char[TIMEBUFSIZE] buf = "[";
163 		auto writer = BlindWriter!char(buf.ptr+1);
164 		putTime!TIME_FORMAT(writer, ut);
165 		writer.put(']');
166 		writer.put(' ');
167 
168 		super.logStartLine();
169 		super.logFragment(buf[0..writer.ptr-buf.ptr]);
170 		super.logFragment(str);
171 		super.logEndLine();
172 	}
173 
174 	override void close()
175 	{
176 		//assert(f !is null);
177 		if (f.isOpen)
178 			f.close();
179 	}
180 
181 private:
182 	int currentDay;
183 
184 protected:
185 	final override void open()
186 	{
187 		super.open();
188 		currentDay = getLogTime().day;
189 		f.writef("\n\n--------------- %s ---------------\n\n\n", getLogTime().formatTime!(TIME_FORMAT)());
190 		f.flush();
191 	}
192 
193 	final override void reopen()
194 	{
195 		super.reopen();
196 		f.writef("\n\n--------------- %s ---------------\n\n\n", getLogTime().formatTime!(TIME_FORMAT)());
197 		f.flush();
198 	}
199 }
200 
201 class ConsoleLogger : Logger
202 {
203 	this(string name)
204 	{
205 		super(name);
206 	}
207 
208 	override void log(in char[] str)
209 	{
210 		stderr.write(name, ": ", str, "\n");
211 		stderr.flush();
212 	}
213 }
214 
215 class NullLogger : Logger
216 {
217 	this() { super(null); }
218 	override void log(in char[] str) {}
219 }
220 
221 class MultiLogger : Logger
222 {
223 	this(Logger[] loggers ...)
224 	{
225 		this.loggers = loggers.dup;
226 		super(null);
227 	}
228 
229 	override void log(in char[] str)
230 	{
231 		foreach (logger; loggers)
232 			logger.log(str);
233 	}
234 
235 	override void rename(string name)
236 	{
237 		foreach (logger; loggers)
238 			logger.rename(name);
239 	}
240 
241 	override void close()
242 	{
243 		foreach (logger; loggers)
244 			logger.close();
245 	}
246 
247 private:
248 	Logger[] loggers;
249 }
250 
251 class FileAndConsoleLogger : MultiLogger
252 {
253 	this(string name)
254 	{
255 		super(new FileLogger(name), new ConsoleLogger(name));
256 	}
257 }
258 
259 bool quiet;
260 
261 shared static this()
262 {
263 	import core.runtime;
264 	foreach (arg; Runtime.args[1..$])
265 		if (arg == "-q" || arg == "--quiet")
266 			quiet = true;
267 }
268 
269 /// Create a logger depending on whether -q or --quiet was passed on the command line.
270 Logger createLogger(string name)
271 {
272 	version (unittest)
273 		return new ConsoleLogger(name);
274 	else
275 		return quiet ? new FileLogger(name) : new FileAndConsoleLogger(name);
276 }