1 /** 2 * Defines several common default serializer implementations. 3 */ 4 module slf4d.default_provider.serializers; 5 6 import slf4d.level; 7 import slf4d.logger : LogMessage, ExceptionInfo; 8 import slf4d.writer; 9 10 import std.datetime : SysTime; 11 12 immutable size_t ISO8601_TIMESTAMP_LENGTH = 23; 13 14 /** 15 * Log formatter that prints nicely formatted log messages to the console 16 * without any particular structured format. It can apply ANSI colors to the 17 * output. 18 */ 19 shared class ConsoleTextLogSerializer : LogSerializer { 20 import std.string : rightJustify, leftJustify; 21 import std.conv : to; 22 23 private static immutable LOG_LEVEL_TEXT_WIDTH = 5; 24 25 private bool ansiColors; 26 private size_t loggerNameLengthLimit; 27 28 this(bool ansiColors = true, size_t loggerNameLengthLimit = 24) { 29 this.ansiColors = ansiColors; 30 this.loggerNameLengthLimit = loggerNameLengthLimit; 31 } 32 33 string serialize(immutable LogMessage msg) { 34 string logStr = formatLoggerName(msg.loggerName) ~ " " ~ 35 formatLogLevel(msg.level) ~ " " ~ 36 formatTimestamp(msg.timestamp) ~ " " ~ 37 msg.message; 38 if (!msg.exception.isNull()) { 39 logStr ~= "\n" ~ formatExceptionInfo(msg.exception.get()); 40 } 41 if (msg.attributes.length > 0) { 42 logStr ~= "\n" ~ formatAttributes(msg.attributes); 43 } 44 return logStr; 45 } 46 47 private size_t messageTextOffset() { 48 return loggerNameLengthLimit + LOG_LEVEL_TEXT_WIDTH + ISO8601_TIMESTAMP_LENGTH + 3; 49 } 50 51 protected string formatLoggerName(string name) { 52 name = compactLoggerName(name); 53 const size_t originalNameLength = name.length; 54 if (ansiColors) { 55 name = "\033[33m" ~ name ~ "\033[0m"; 56 } 57 return rightJustify(name, loggerNameLengthLimit + (name.length - originalNameLength), ' '); 58 } 59 60 protected string compactLoggerName(string name) { 61 import std.algorithm : splitter, map, count; 62 import std.range; 63 if (name.length <= loggerNameLengthLimit) return name; 64 65 // See if we can split the name by dots to shorten it. 66 auto parts = name.splitter('.'); 67 auto firstPart = parts.take(1); 68 auto lastPart = parts.tail(1); 69 auto middleParts = parts.drop(1).dropBack(1); 70 if (!firstPart.empty && !lastPart.empty && !middleParts.empty) { 71 size_t lengthWithAbbreviatedMiddleParts = firstPart.front.length + 72 middleParts.count * 3 + 1 + 73 lastPart.front.length; 74 if (lengthWithAbbreviatedMiddleParts <= loggerNameLengthLimit) { 75 return chain(firstPart, only("."), middleParts.map!(p => p[0] ~ "."), lastPart).join(""); 76 } 77 // A basic abbreviation of the middle parts is too big, so now let's try to abbreviate the first part too. 78 size_t lengthWithAllButLastAbbreviated = 1 + middleParts.count * 2 + 1 + lastPart.front.length; 79 if (lengthWithAllButLastAbbreviated <= loggerNameLengthLimit) { 80 return chain(only(firstPart.front[0] ~ "."), middleParts.map!(p => p[0] ~ "."), lastPart).join(""); 81 } 82 // Even that's too big, so now we give up and just abbreviate using a simple ellipsis. 83 } 84 85 return "..." ~ name[3 + $ - loggerNameLengthLimit .. $]; 86 } 87 88 protected string formatLogLevel(Level level) { 89 string s = level.name; 90 size_t originalLength = s.length; 91 if (ansiColors) { 92 string colorStr = "\033[1m"; 93 if (level == Levels.ERROR) { 94 colorStr = "\033[31;1;4m"; 95 } else if (level == Levels.WARN) { 96 colorStr = "\033[33;1m"; 97 } else if (level == Levels.INFO) { 98 colorStr = "\033[34;1m"; 99 } else if (level == Levels.DEBUG) { 100 colorStr = "\033[36;1m"; 101 } else if (level == Levels.TRACE) { 102 colorStr = "\033[37;1m"; 103 } else { 104 colorStr = "\033[34;1m"; // Use blue as a fallback for unknown levels. 105 } 106 s = colorStr ~ s ~ "\033[0m"; 107 } 108 return leftJustify(s, LOG_LEVEL_TEXT_WIDTH + (s.length - originalLength), ' '); 109 } 110 111 protected string formatTimestamp(SysTime timestamp) { 112 string s = formatTimestampISO8601(timestamp); 113 if (ansiColors) { 114 s = "\033[90m" ~ s ~ "\033[0m"; 115 } 116 return s; 117 } 118 119 protected string formatExceptionInfo(ExceptionInfo info) { 120 import std.range; 121 import std.algorithm; 122 string exceptionName = info.exceptionClassName; 123 if (ansiColors) { 124 exceptionName = "\033[31;1m" ~ exceptionName ~ "\033[0m"; 125 } 126 string sourceLocation = info.sourceFileName ~ ":" ~ to!string(info.sourceLineNumber); 127 if (ansiColors) { 128 sourceLocation = "\033[97;4m" ~ sourceLocation ~ "\033[0m"; 129 } 130 auto s = only( 131 repeat(' ', messageTextOffset()).array.idup, 132 exceptionName, " thrown in ", sourceLocation, ": ", info.message 133 ); 134 if (!info.stackTrace.isNull) { 135 auto lines = info.stackTrace.get().splitter('\n') 136 .map!(line => only(repeat(' ', messageTextOffset() + 2).array.idup, line).joiner()); 137 string traceStr = lines.join("\n").to!string; 138 if (ansiColors) { 139 traceStr = "\033[31m" ~ traceStr ~ "\033[0m"; 140 } 141 return s.join() ~ "\n" ~ traceStr; 142 } 143 return s.join(); 144 } 145 146 protected string formatAttributes(immutable(string[string]) attributes) { 147 import std.range : repeat, array; 148 size_t attributesIndentation = messageTextOffset(); 149 string s; 150 if (ansiColors) { 151 s = repeat(' ', attributesIndentation).array.idup ~ "\033[4;37mAttributes:\033[0m"; 152 } else { 153 s = repeat(' ', attributesIndentation).array.idup ~ "Attributes:"; 154 } 155 foreach (key, value; attributes) { 156 string keyStr = key; 157 string valueStr = value; 158 if (ansiColors) { 159 keyStr = "\033[96m" ~ key ~ "\033[0m"; 160 valueStr = "\033[35m" ~ value ~ "\033[0m"; 161 } 162 s ~= "\n" ~ repeat(' ', attributesIndentation + 2).array.idup ~ keyStr ~ " = " ~ valueStr; 163 } 164 return s; 165 } 166 } 167 168 /** 169 * A log serializer that formats messages in a JSON string, containing all data 170 * in the original log message struct. 171 */ 172 class JsonLogSerializer : LogSerializer { 173 string serialize(immutable LogMessage msg) shared { 174 import std.json; 175 JSONValue obj = JSONValue(string[string].init); 176 obj.object["level"] = JSONValue(string[string].init); 177 obj.object["level"].object["value"] = JSONValue(msg.level.value); 178 obj.object["level"].object["name"] = JSONValue(msg.level.name); 179 obj.object["message"] = JSONValue(msg.message); 180 obj.object["timestamp"] = JSONValue(formatTimestampISO8601(msg.timestamp)); 181 obj.object["loggerName"] = JSONValue(msg.loggerName); 182 if (msg.exception.isNull) { 183 obj.object["exception"] = JSONValue(null); 184 } else { 185 import slf4d.logger : ExceptionInfo; 186 ExceptionInfo info = msg.exception.get(); 187 obj.object["exception"] = JSONValue(string[string].init); 188 obj.object["exception"].object["message"] = JSONValue(info.message); 189 obj.object["exception"].object["sourceFileName"] = JSONValue(info.sourceFileName); 190 obj.object["exception"].object["sourceLineNumber"] = JSONValue(info.sourceLineNumber); 191 obj.object["exception"].object["exceptionClassName"] = JSONValue(info.exceptionClassName); 192 if (info.stackTrace.isNull()) { 193 obj.object["exception"].object["stackTrace"] = JSONValue(null); 194 } else { 195 obj.object["exception"].object["stackTrace"] = JSONValue(info.stackTrace.get()); 196 } 197 } 198 obj.object["sourceContext"] = JSONValue(string[string].init); 199 obj.object["sourceContext"].object["moduleName"] = JSONValue(msg.sourceContext.moduleName); 200 obj.object["sourceContext"].object["functionName"] = JSONValue(msg.sourceContext.functionName); 201 obj.object["sourceContext"].object["fileName"] = JSONValue(msg.sourceContext.fileName); 202 obj.object["sourceContext"].object["lineNumber"] = JSONValue(msg.sourceContext.lineNumber); 203 204 obj.object["attributes"] = JSONValue(string[string].init); 205 foreach (key, value; msg.attributes) { 206 obj.object["attributes"].object[key] = JSONValue(value); 207 } 208 209 return obj.toString(); 210 } 211 } 212 213 /** 214 * Helper method to serialize a timestamp as an ISO-8601 datetime string, with 215 * minimal GC allocation. The format is "YYYY-MM-DDTHH:MM:SS.sss". 216 * Params: 217 * timestamp = The timestamp to format. 218 * Returns: The formatted timestamp as a string. 219 */ 220 string formatTimestampISO8601(SysTime timestamp) { 221 char[ISO8601_TIMESTAMP_LENGTH] buffer; 222 writeZeroPaddedInt(buffer, 0, 4, timestamp.year); 223 buffer[4] = '-'; 224 writeZeroPaddedInt(buffer, 5, 2, timestamp.month); 225 buffer[7] = '-'; 226 writeZeroPaddedInt(buffer, 8, 2, timestamp.day); 227 buffer[10] = 'T'; 228 writeZeroPaddedInt(buffer, 11, 2, timestamp.hour); 229 buffer[13] = ':'; 230 writeZeroPaddedInt(buffer, 14, 2, timestamp.minute); 231 buffer[16] = ':'; 232 writeZeroPaddedInt(buffer, 17, 2, timestamp.second); 233 buffer[19] = '.'; 234 writeZeroPaddedInt(buffer, 20, 3, cast(int) timestamp.fracSecs.total!"msecs"); 235 return buffer.idup; 236 } 237 238 /** 239 * Writes a zero-padded integer to a character buffer, starting at the 240 * specified index, and padding it to the specified length. 241 * Params: 242 * buffer = The character buffer to write to. 243 * startIndex = The index in the buffer to start writing at. 244 * pad = The number of digits to pad the integer to. 245 * value = The integer value to write. 246 */ 247 private void writeZeroPaddedInt(char[] buffer, size_t startIndex, size_t pad, int value) { 248 size_t index = startIndex + pad; 249 while (value > 0 || pad > 0) { 250 buffer[--index] = cast(char)('0' + (value % 10)); 251 value /= 10; 252 --pad; 253 } 254 while (index > startIndex) { 255 buffer[--index] = '0'; 256 } 257 } 258 259 unittest { 260 char[10] buffer; 261 writeZeroPaddedInt(buffer, 0, 3, 42); 262 assert(buffer[0..3] == "042"); 263 writeZeroPaddedInt(buffer, 0, 5, 123); 264 assert(buffer[0..5] == "00123"); 265 writeZeroPaddedInt(buffer, 0, 2, 7); 266 assert(buffer[0..2] == "07"); 267 }