1 /** 2 * The handler module defines the `LogHandler` interface, as well as several 3 * simple handler implementations that can be used or extended by SLF4D 4 * providers. 5 */ 6 module slf4d.handler; 7 8 import slf4d.logger : LogMessage; 9 import slf4d.level; 10 11 /** 12 * The interface for any component that consumes log messages generated by a 13 * `Logger`. Only messages whose level is greater than or equal to the logger's 14 * level will be sent to handlers. For example, a Logger configured at an INFO 15 * log level will send INFO, WARN, and ERROR messages, but not DEBUG or TRACE. 16 * 17 * Each Logger has a single root LogHandler instance. This "root handler" can 18 * be a very simple handler that sends messages to stdout, or it could be a 19 * more complex composition of handlers to distribute logs to various locations 20 * according to filtering logic. 21 */ 22 shared interface LogHandler { 23 /** 24 * Handles a log message. 25 * Params: 26 * msg = The log message that was generated. 27 */ 28 void handle(immutable LogMessage msg); 29 } 30 31 /** 32 * A log handler that serializes incoming messages, and uses one or more 33 * `LogWriter` instances to write the serialized messages to some output 34 * resources, like the console, files, or network devices. 35 */ 36 shared class SerializingLogHandler : LogHandler { 37 import slf4d.writer : LogSerializer, LogWriter; 38 39 private LogSerializer serializer; 40 private LogWriter[] writers; 41 42 public this(shared LogSerializer serializer, shared LogWriter[] writers) { 43 this.serializer = serializer; 44 this.writers = writers; 45 } 46 47 void handle(immutable LogMessage msg) { 48 import std.stdio; 49 try { 50 string serialized = this.serializer.serialize(msg); 51 foreach (writer; this.writers) { 52 try { 53 writer.write(msg, serialized); 54 } catch (Exception e) { 55 stderr.writefln!"Failed to write log message: %s"(e.msg); 56 } 57 } 58 } catch (Exception e) { 59 stderr.writefln!"Failed to serialize log message: %s"(e.msg); 60 } 61 } 62 } 63 64 unittest { 65 import slf4d.writer; 66 import slf4d.logger; 67 68 class TestSerializer : LogSerializer { 69 string serialize(immutable LogMessage msg) shared { 70 return msg.message; 71 } 72 } 73 74 class TestWriter : LogWriter { 75 string[] messages; 76 void write(immutable LogMessage _, string serializedMessage) shared { 77 messages ~= serializedMessage; 78 } 79 } 80 81 auto writer = new shared TestWriter(); 82 auto writer2 = new shared TestWriter(); 83 auto handler = new SerializingLogHandler( 84 new shared TestSerializer(), 85 [writer, writer2] 86 ); 87 Logger logger = Logger(handler); 88 logger.info("Testing"); 89 logger.warn("This is another message."); 90 assert(writer.messages.length == 2); 91 assert(writer.messages[0] == "Testing"); 92 assert(writer.messages[1] == "This is another message."); 93 assert(writer2.messages.length == 2); 94 assert(writer2.messages[0] == "Testing"); 95 assert(writer2.messages[1] == "This is another message."); 96 } 97 98 /** 99 * A log handler that discards all messages. Useful for testing. 100 */ 101 class DiscardingLogHandler : LogHandler { 102 public void handle(immutable LogMessage _) shared { 103 // Do nothing. 104 } 105 } 106 107 /** 108 * A log handler that simply appends all messages it receives to an internal 109 * array. This can be useful for testing, but should not be used for general 110 * runtime logging due to the need to synchronize access to the internal 111 * message cache. 112 */ 113 shared class CachingLogHandler : LogHandler { 114 /** 115 * The internal cache of messages. 116 */ 117 private LogMessage[] messages; 118 119 /** 120 * "Handles" a log message by appending it to this handler's internal list 121 * of messages, for later inspection. 122 * Params: 123 * msg = The message to handle. 124 */ 125 public void handle(immutable LogMessage msg) shared { 126 synchronized(this) { 127 this.messages ~= msg; 128 } 129 } 130 131 /** 132 * Resets this handler's internal message cache to an empty list. 133 */ 134 public void reset() { 135 synchronized(this) { 136 this.messages = []; 137 } 138 } 139 140 /** 141 * Gets an immutable duplication of the list of messages this handler has 142 * handled since the last `reset()` call. 143 * Returns: The list of messages. 144 */ 145 public LogMessage[] getMessages() { 146 synchronized(this) { 147 return cast(LogMessage[]) messages.idup; 148 } 149 } 150 151 /** 152 * Gets the number of messages that this handler has handled since the last 153 * `reset()` call. 154 * Returns: The number of messages. 155 */ 156 public size_t messageCount() { 157 synchronized(this) { 158 return messages.length; 159 } 160 } 161 162 /** 163 * Determines if no messages have been logged since the last time `reset()` 164 * was called. 165 * Returns: True if there are no messages, or false otherwise. 166 */ 167 public bool empty() { 168 return messageCount == 0; 169 } 170 } 171 172 unittest {// Test basic functionality. 173 import slf4d.logger : Logger; 174 auto handler = new CachingLogHandler(); 175 auto logger = Logger(handler); 176 assert(handler.getMessages().length == 0); 177 logger.info("Hello world!"); 178 assert(handler.getMessages().length == 1); 179 } 180 181 unittest {// Test thread-safety. 182 import core.thread; 183 import slf4d.logger : Logger; 184 CachingLogHandler handler = new CachingLogHandler(); 185 Thread[] threads; 186 for (int i = 0; i < 10; i++) { 187 threads ~= new Thread({ 188 Logger logger = Logger(handler); 189 for (int j = 0; j < 100; j++) { 190 logger.infoF!"Message from thread %s: %d"(Thread.getThis().name, j); 191 } 192 }); 193 } 194 foreach (t; threads) t.start(); 195 foreach (t; threads) t.join(); 196 // Check that all messages were logged. 197 assert(handler.messageCount() == 1000); 198 } 199 200 /** 201 * A log handler that simply passes any log message it receives to a list of 202 * other handlers. Note that handlers should only be added at application 203 * startup, because the `handle` method is not synchronized to improve 204 * performance. 205 */ 206 shared class MultiLogHandler : LogHandler { 207 private shared(LogHandler)[] handlers; 208 209 /** 210 * Constructs this multi-log handler using the given list of sub-handlers. 211 * Params: 212 * handlers = The handlers that should each handle every message this 213 * multi-handler receives. 214 */ 215 public this(shared(LogHandler)[] handlers) { 216 this.handlers = handlers; 217 } 218 219 /** 220 * Adds a handler to this multi-handler's list of handlers. 221 * Params: 222 * handler = The handler to add. 223 * Returns: A reference to this multi-handler. 224 */ 225 public MultiLogHandler addHandler(shared(LogHandler) handler) { 226 this.handlers ~= handler; 227 return this; 228 } 229 230 /** 231 * Handles a log message by calling each of its sub-handlers' `handle` 232 * methods on it. 233 * Params: 234 * msg = The message to handle. 235 */ 236 public void handle(immutable LogMessage msg) shared { 237 foreach (handler; handlers) { 238 handler.handle(msg); 239 } 240 } 241 } 242 243 unittest { 244 import slf4d.logger : Logger; 245 auto h1 = new CachingLogHandler(); 246 auto h2 = new CachingLogHandler(); 247 auto multiHandler = new MultiLogHandler([h1, h2]); 248 auto logger = Logger(multiHandler); 249 logger.info("Hello world!"); 250 assert(h1.getMessages().length == 1); 251 assert(h2.getMessages().length == 1); 252 } 253 254 /** 255 * A handler that applies a filter to log messages, and only passes messages to 256 * its internal handler if the filter returns `true`. 257 */ 258 shared class FilterLogHandler : LogHandler { 259 private bool function (LogMessage) filterFunction; 260 private LogHandler handler; 261 262 public this(shared(LogHandler) handler, bool function (LogMessage) filterFunction) { 263 this.handler = handler; 264 this.filterFunction = filterFunction; 265 } 266 267 void handle(immutable LogMessage msg) { 268 if (this.filterFunction(msg)) { 269 this.handler.handle(msg); 270 } 271 } 272 } 273 274 unittest { 275 import slf4d.logger : Logger; 276 auto baseHandler = new CachingLogHandler(); 277 auto filterHandler = new FilterLogHandler( 278 baseHandler, 279 (msg) { 280 return msg.message.length > 10; 281 } 282 ); 283 Logger logger = Logger(filterHandler); 284 logger.info("Testing"); 285 assert(baseHandler.getMessages().length == 0); 286 logger.info("This is a long string!"); 287 assert(baseHandler.getMessages().length == 1); 288 } 289 290 /** 291 * A handler that sends log messages to different handlers depending on the 292 * level of the message. For example, you could create a level-mapped log 293 * handler that sends INFO messages to stdout, but sends ERROR messages to 294 * an email notification service. 295 */ 296 shared class LevelMappedLogHandler : LogHandler { 297 298 private static struct LevelRange { 299 public const int minValue; 300 public const bool hasMinValue; 301 public const int maxValue; 302 public const bool hasMaxValue; 303 304 public static LevelRange infinite() { 305 return LevelRange(-1, false, -1, false); 306 } 307 308 public static LevelRange of(int minValue, int maxValue) { 309 return LevelRange(minValue, true, maxValue, true); 310 } 311 312 public static LevelRange of(int value) { 313 return LevelRange(value, true, value, true); 314 } 315 316 public static LevelRange ofMin(int minValue) { 317 return LevelRange(minValue, true, -1, false); 318 } 319 320 public static LevelRange ofMax(int maxValue) { 321 return LevelRange(-1, false, maxValue, true); 322 } 323 } 324 325 private static struct Mapping { 326 public const LevelRange range; 327 public shared(LogHandler) handler; 328 } 329 330 private Mapping[] mappings; 331 332 public void addLevelMapping(Level level, shared(LogHandler) handler) { 333 this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.of(level.value), handler); 334 } 335 336 public void addRangeLevelMapping(Level minLevel, Level maxLevel, shared(LogHandler) handler) { 337 if (minLevel.value > maxLevel.value) { 338 Level tmp = minLevel; 339 minLevel = maxLevel; 340 maxLevel = tmp; 341 } 342 this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.of(minLevel.value, maxLevel.value), handler); 343 } 344 345 public void addMinLevelMapping(Level minLevel, shared(LogHandler) handler) { 346 this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.ofMin(minLevel.value), handler); 347 } 348 349 public void addMaxLevelMapping(Level maxLevel, shared(LogHandler) handler) { 350 this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.ofMax(maxLevel.value), handler); 351 } 352 353 public void addAnyLevelMapping(shared(LogHandler) handler) { 354 this.mappings ~= cast(shared(Mapping)) Mapping(LevelRange.infinite, handler); 355 } 356 357 public void handle(immutable LogMessage msg) shared { 358 foreach (mapping; mappings) { 359 if ( 360 (!mapping.range.hasMinValue || msg.level.value >= mapping.range.minValue) && 361 (!mapping.range.hasMaxValue || msg.level.value <= mapping.range.maxValue) 362 ) { 363 mapping.handler.handle(msg); 364 } 365 } 366 } 367 } 368 369 unittest { 370 import slf4d.logger : Logger; 371 CachingLogHandler baseHandler = new CachingLogHandler(); 372 373 // Test single-level mappings. 374 auto handler = new LevelMappedLogHandler(); 375 handler.addLevelMapping(Levels.INFO, baseHandler); 376 377 Logger logger = Logger(handler); 378 logger.debug_("This should not be logged."); 379 assert(baseHandler.empty); 380 logger.warn("This should also not be logged."); 381 assert(baseHandler.empty); 382 logger.trace("Also not logged."); 383 assert(baseHandler.empty); 384 logger.info("This should be logged!"); 385 assert(baseHandler.messageCount == 1); 386 baseHandler.reset(); 387 388 // Test range mappings. 389 handler = new LevelMappedLogHandler(); 390 handler.addRangeLevelMapping(Levels.DEBUG, Levels.WARN, baseHandler); 391 392 Logger logger2 = Logger(handler); 393 logger2.trace("This should not be logged."); 394 assert(baseHandler.empty); 395 logger2.error("This should also not be logged."); 396 assert(baseHandler.empty); 397 logger2.warn("This should be logged."); 398 logger2.info("Also this!"); 399 logger2.debug_("And this!"); 400 assert(baseHandler.messageCount == 3); 401 baseHandler.reset(); 402 403 // Test min mappings. 404 handler = new LevelMappedLogHandler(); 405 handler.addMinLevelMapping(Levels.INFO, baseHandler); 406 407 Logger logger3 = Logger(handler); 408 logger3.debug_("This should not be logged."); 409 assert(baseHandler.empty); 410 logger3.info("This should be logged."); 411 logger3.error("This too!"); 412 assert(baseHandler.messageCount == 2); 413 baseHandler.reset(); 414 415 // Test max mappings. 416 handler = new LevelMappedLogHandler(); 417 handler.addMaxLevelMapping(Levels.WARN, baseHandler); 418 419 Logger logger4 = Logger(handler); 420 logger4.error("This should not be logged."); 421 assert(baseHandler.empty); 422 logger4.warn("This should be logged."); 423 logger4.trace("This too!"); 424 assert(baseHandler.messageCount == 2); 425 }