1 /** 2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 Handles console logging in pretty colors. 7 8 The module disables colors when stdout and stderr isn't a TTY that support 9 colors. This is to avoid ASCII escape sequences in piped output. 10 11 The loggers use strings (their name) to form a hierarchy. Such that "a" is the 12 root and "a.b" is a child of "a". This can be used by you to change the log 13 level of a "sub tree" (`SpanMode.depth`). 14 */ 15 module colorlog; 16 17 import logger = std.experimental.logger; 18 import std.array : empty; 19 import std.conv : to; 20 import std.experimental.logger : LogLevel; 21 import std.stdio : writefln, stderr, stdout; 22 23 public import my.term_color; 24 25 /// The parent of all loggers. 26 immutable RootLogger = ""; 27 28 /// The verbosity level of the logging to use. 29 enum VerboseMode { 30 /// Info+ 31 info, 32 /// Trace+ 33 trace, 34 /// Warnings+ 35 warning, 36 } 37 38 LogLevel toLogLevel(VerboseMode mode) @safe pure nothrow @nogc { 39 final switch (mode) { 40 case VerboseMode.info: 41 return logger.LogLevel.info; 42 case VerboseMode.trace: 43 return logger.LogLevel.all; 44 case VerboseMode.warning: 45 return logger.LogLevel.warning; 46 } 47 } 48 49 /** Configure `std.experimental.logger` `sharedLog` with a colorlog instance 50 * and register it with name "_". 51 */ 52 void confLogger(VerboseMode mode) @trusted { 53 logger.globalLogLevel = toLogLevel(mode); 54 55 static if (__VERSION__ <= 2100) { 56 final switch (mode) { 57 case VerboseMode.info: 58 logger.sharedLog = new SimpleLogger(logger.LogLevel.info); 59 break; 60 case VerboseMode.trace: 61 logger.sharedLog = new DebugLogger(logger.LogLevel.all); 62 break; 63 case VerboseMode.warning: 64 logger.sharedLog = new SimpleLogger(logger.LogLevel.info); 65 break; 66 } 67 } else { 68 final switch (mode) { 69 case VerboseMode.info: 70 logger.sharedLog = cast(shared) new SimpleLogger(logger.LogLevel.info); 71 break; 72 case VerboseMode.trace: 73 logger.sharedLog = cast(shared) new DebugLogger(logger.LogLevel.all); 74 break; 75 case VerboseMode.warning: 76 logger.sharedLog = cast(shared) new SimpleLogger(logger.LogLevel.info); 77 break; 78 } 79 } 80 81 () @trusted { register(logger.sharedLog, RootLogger); }(); 82 } 83 84 /** Default logger with low clutter in the console which mean it is suitable 85 * for use with user interfacing log messages. 86 * 87 * Automatically colors the log messages depending on their severity. 88 * 89 * As you may note the time is not printed. It isn't particularly useful for a 90 * user of your program as a CLI. A user would just be annoyed. But for e.g. a 91 * system log it may be. In that case, use another logger than this one. 92 */ 93 class SimpleLogger : logger.Logger { 94 this(const LogLevel lvl = LogLevel.warning) @safe { 95 super(lvl); 96 initColors; 97 } 98 99 override void writeLogMsg(ref LogEntry payload) @trusted { 100 auto out_ = stderr; 101 auto use_color = Color.red; 102 auto use_mode = Mode.bold; 103 const use_bg = Background.black; 104 105 switch (payload.logLevel) { 106 case LogLevel.trace: 107 out_ = stdout; 108 use_color = Color.white; 109 use_mode = Mode.init; 110 break; 111 case LogLevel.info: 112 out_ = stdout; 113 use_color = Color.white; 114 break; 115 default: 116 } 117 118 out_.writefln("%s: %s", payload.logLevel.to!string.color(use_color) 119 .bg(use_bg).mode(use_mode), payload.msg); 120 } 121 } 122 123 /** Logger focused on debugging a program with colorization of the severity. 124 * 125 * The logger print the `file:line` at the end of the message to make it easier 126 * to read a log. By doing so this logger can be used to print e.g. an 127 * indented AST and still keep the `file:line`. 128 * 129 * Same motivation here as the `SimpleLogger` when it comes to the timestamp. 130 * It isn't particularly useful for what the logger is intended to be used for. 131 */ 132 class DebugLogger : logger.Logger { 133 this(const logger.LogLevel lvl = LogLevel.trace) @safe { 134 super(lvl); 135 initColors; 136 } 137 138 override void writeLogMsg(ref LogEntry payload) @trusted { 139 auto out_ = stderr; 140 auto use_color = Color.red; 141 auto use_mode = Mode.bold; 142 const use_bg = Background.black; 143 144 switch (payload.logLevel) { 145 case LogLevel.trace: 146 out_ = stdout; 147 use_color = Color.white; 148 use_mode = Mode.init; 149 break; 150 case LogLevel.info: 151 out_ = stdout; 152 use_color = Color.white; 153 break; 154 default: 155 } 156 157 out_.writefln("%s: %s [%s:%d]", payload.logLevel.to!string.color(use_color) 158 .bg(use_bg).mode(use_mode), payload.msg, payload.funcName, payload.line); 159 } 160 } 161 162 /// A string mixin to create a SimpleLogger for the module. 163 string mixinModuleLogger(logger.LogLevel defaultLogLvl = logger.LogLevel.all) @safe pure { 164 return "shared static this() { import std.experimental.logger : LogLevel; make!SimpleLogger(LogLevel." 165 ~ defaultLogLvl.to!string ~ "); }"; 166 } 167 168 /// Register a logger for the module and make it configurable from "outside" via the registry. 169 static if (__VERSION__ <= 2100) { 170 void register(logger.Logger logger, string name = __MODULE__) { 171 synchronized (poolLock) { 172 loggers[name] = cast(shared) logger; 173 } 174 } 175 } else { 176 void register(shared logger.Logger logger, string name = __MODULE__) { 177 synchronized (poolLock) { 178 loggers[name] = logger; 179 } 180 } 181 } 182 183 /// Create a logger for the module and make it configurable from "outside" via the registry. 184 void make(LoggerT)(const logger.LogLevel lvl = logger.LogLevel.all, string name = __MODULE__) @trusted { 185 static if (__VERSION__ <= 2100) { 186 register(new LoggerT(lvl), name); 187 } else { 188 register(cast(shared) new LoggerT(lvl), name); 189 } 190 } 191 192 /// Returns: the name of all registered loggers. 193 string[] getRegisteredLoggers() @trusted { 194 import std.array : appender; 195 196 auto app = appender!(string[])(); 197 synchronized (poolLock) { 198 foreach (a; (cast() loggers).byKey) 199 app.put(a); 200 } 201 202 return app.data; 203 } 204 205 /// Remove all registered loggers. 206 void clearAllLoggers() @trusted { 207 synchronized (poolLock) { 208 loggers = null; 209 } 210 } 211 212 enum SpanMode { 213 /// Set only the specified logger. 214 single, 215 /// Set the logger and all its children. 216 depth 217 } 218 219 /// Set the log level for `name`. 220 void setLogLevel(const string name, const logger.LogLevel lvl, const SpanMode span = SpanMode 221 .single) @trusted { 222 static void setSingle(const string name, const logger.LogLevel lvl) { 223 if (auto v = name in loggers) { 224 auto uv = cast()*v; 225 uv.logLevel = lvl; 226 } else { 227 throw new UnknownLogger("no such logger registered: " ~ name); 228 } 229 } 230 231 static void depth(string startName, const logger.LogLevel lvl) { 232 import std.algorithm : filter, startsWith; 233 234 auto loggersU = cast(logger.Logger[string]) loggers; 235 foreach (a; loggersU.byKeyValue.filter!(a => a.key.startsWith(startName))) { 236 a.value.logLevel = lvl; 237 } 238 } 239 240 synchronized (poolLock) { 241 final switch (span) { 242 case SpanMode.single: 243 setSingle(name, lvl); 244 break; 245 case SpanMode.depth: 246 setSingle(name, lvl); 247 depth(name, lvl); 248 break; 249 } 250 } 251 } 252 253 /// Set the log level for all loggers in `names`. 254 void setLogLevel(const string[] names, const logger.LogLevel lvl, 255 const SpanMode span = SpanMode.single) @safe { 256 if (names.empty) 257 return; 258 259 logger.globalLogLevel = lvl; 260 foreach (a; names) 261 setLogLevel(a, lvl, span); 262 } 263 264 /// Set the log level for all loggers in `names`. 265 void setLogLevel(const NameLevel[] names, const SpanMode span = SpanMode.single) @safe { 266 import std.algorithm : min; 267 268 if (names.empty) 269 return; 270 271 auto global = logger.LogLevel.off; 272 273 foreach (a; names) { 274 setLogLevel(a.name, a.level, span); 275 global = min(a.level, global); 276 } 277 278 logger.globalLogLevel = global; 279 } 280 281 /** Log a mesage to the specified logger. 282 * 283 * This only takes the global lock one time and then cache the logger. 284 */ 285 logger.Logger log(string name = __MODULE__)() @trusted { 286 static logger.Logger local; 287 288 if (local !is null) 289 return local; 290 291 synchronized (poolLock) { 292 if (auto v = name in loggers) { 293 local = cast()*v; 294 return cast()*v; 295 } 296 } 297 298 throw new UnknownLogger("no such logger registered: " ~ name); 299 } 300 301 struct NameLevel { 302 string name; 303 LogLevel level; 304 } 305 306 /// Parse a comma+equal separated string for logger names that can be used with `setLogLevel`. 307 NameLevel[] parseLogNames(string arg, logger.LogLevel defaultLogLvl = logger.LogLevel.all) @safe pure { 308 import std.algorithm : splitter, joiner, map; 309 import std.array : array; 310 import std.string : split; 311 312 NameLevel[] conv(string s) { 313 try { 314 auto sp = split(s, '='); 315 if (sp.length != 2) 316 return [NameLevel(s, defaultLogLvl)]; 317 return [NameLevel(sp[0], sp[1].to!(logger.LogLevel))]; 318 } catch (Exception e) { 319 } 320 return NameLevel[].init; 321 } 322 323 return arg.splitter(',').map!conv.joiner.array; 324 } 325 326 /** Always takes the global lock to find the logger. 327 * 328 * This is safe to use whenever because if they logger is replaced it will be 329 * returned. Normally though this feature isn't needed. Normally all loggers 330 * are registered during module initialization and then they are not changed. 331 */ 332 logger.Logger logSlow(string name = __MODULE__)() @trusted { 333 synchronized (poolLock) { 334 if (auto v = name in loggers) { 335 return cast()*v; 336 } 337 } 338 339 throw new UnknownLogger("no such logger registered: " ~ name); 340 } 341 342 /// Unknown logger. 343 class UnknownLogger : Exception { 344 this(string msg, string file = __FILE__, int line = __LINE__) @safe pure nothrow { 345 super(msg, file, line); 346 } 347 } 348 349 @("shall instantiate a logger and register") 350 unittest { 351 scope (exit) 352 clearAllLoggers; 353 make!TestLogger(); 354 assert([__MODULE__] == getRegisteredLoggers()); 355 } 356 357 @("shall register a logger and register") 358 unittest { 359 scope (exit) 360 clearAllLoggers; 361 register(new TestLogger); 362 assert([__MODULE__] == getRegisteredLoggers()); 363 } 364 365 @("shall log a message") 366 unittest { 367 scope (exit) 368 clearAllLoggers; 369 make!TestLogger(); 370 371 logSlow.warning("hej"); 372 373 synchronized (poolLock) { 374 assert(!((cast(TestLogger) loggers[__MODULE__]).lastMsg.empty), "no message logged"); 375 } 376 } 377 378 @("shall change the log level") 379 unittest { 380 scope (exit) 381 clearAllLoggers; 382 make!TestLogger(logger.LogLevel.warning); 383 make!TestLogger(logger.LogLevel.warning, __MODULE__ ~ ".b"); 384 385 setLogLevel(__MODULE__, logger.LogLevel.trace); 386 387 logSlow.trace("hej"); 388 logSlow!(__MODULE__ ~ ".b").trace("hej"); 389 390 synchronized (poolLock) { 391 assert(!((cast(TestLogger) loggers[__MODULE__]).lastMsg.empty), 392 "message found when it shouldn't"); 393 assert(((cast(TestLogger) loggers[__MODULE__ ~ ".b"]).lastMsg.empty), 394 "message found when it shouldn't"); 395 } 396 } 397 398 @("shall change the log level from parent and up") 399 unittest { 400 scope (exit) 401 clearAllLoggers; 402 make!TestLogger(logger.LogLevel.warning); 403 make!TestLogger(logger.LogLevel.warning, __MODULE__ ~ ".b"); 404 405 setLogLevel(__MODULE__, logger.LogLevel.trace, SpanMode.depth); 406 407 logSlow.trace("hej"); 408 logSlow!(__MODULE__ ~ ".b").trace("hej"); 409 410 synchronized (poolLock) { 411 assert(!((cast(TestLogger) loggers[__MODULE__]).lastMsg.empty), 412 "message found when it shouldn't"); 413 assert(!((cast(TestLogger) loggers[__MODULE__ ~ ".b"]).lastMsg.empty), 414 "message found when it shouldn't"); 415 } 416 } 417 418 @("shall parse a comma separate list") 419 unittest { 420 assert(parseLogNames("hej=trace,foo") == [ 421 NameLevel("hej", logger.LogLevel.trace), 422 NameLevel("foo", logger.LogLevel.all) 423 ]); 424 } 425 426 private: 427 428 import core.sync.mutex : Mutex; 429 430 // The width of the prefix. 431 immutable _prefixWidth = 8; 432 433 // Mutex for the logger pool. 434 shared Mutex poolLock; 435 shared logger.Logger[string] loggers; 436 437 shared static this() { 438 poolLock = cast(shared) new Mutex(); 439 } 440 441 class TestLogger : logger.Logger { 442 this(const logger.LogLevel lvl = LogLevel.trace) @safe { 443 super(lvl); 444 } 445 446 string lastMsg; 447 448 override void writeLogMsg(ref LogEntry payload) @trusted { 449 import std.format : format; 450 451 lastMsg = format!"%s: %s [%s:%d]"(payload.logLevel, payload.msg, 452 payload.funcName, payload.line); 453 } 454 }