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