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 }