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 }