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 }