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 }