1 module logdefer.serializer.json; 2 3 4 import logdefer.common : DelegateWriter, EventContext, LogEntry; 5 6 import std.array : Appender; 7 import std.string : format; 8 import std.traits : isSafe; 9 import std.utf : stride, UTFException; 10 11 import logdefer.time.duration : Seconds; 12 13 /** 14 Basic JSON serializer that outputs in the standard Log::Defer format. 15 See https://metacpan.org/pod/Log::Defer for more details on the format 16 and tools to render it. 17 Since output format is fixed this uses an optimized algorithm. 18 This is about 2x faster than using std.json 19 */ 20 @safe 21 struct JSONSerializer(Writer = DelegateWriter) if (isSafe!Writer) 22 { 23 public: 24 25 @disable this(); 26 27 this(Writer writer) 28 { 29 writer_ = writer; 30 } 31 32 void opCall(const ref EventContext eventContext) 33 { 34 write("{"); 35 36 serializeEventTimers(eventContext); 37 serializeLogs(eventContext); 38 serializeMetadata(eventContext); 39 serializeUserTimers(eventContext); 40 41 write("}"); 42 43 writer_(buffer_[0..length_].idup); 44 45 length_ = 0; 46 } 47 48 private: 49 50 Writer writer_ = void; 51 52 // Maximum JSON size 53 immutable enum BUFFER_SIZE = 32 * 1024; 54 55 char[BUFFER_SIZE] buffer_; 56 uint length_; 57 58 void serializeEventTimers(const ref EventContext eventContext) 59 { 60 writeAll([ 61 `"start":`, 62 "%s".format(eventContext.realStartTime), 63 `,"end":`, 64 "%.6f".format(eventContext.endOffset.toUnits!Seconds), 65 ]); 66 } 67 68 void serializeMetadata(const ref EventContext eventContext) 69 { 70 if (eventContext.metadata) 71 { 72 write(`,"data":{`); 73 foreach(const ref entry; eventContext.metadata.byKeyValue()) 74 { 75 writeAll([ 76 `"`, 77 encode(entry.key), 78 `":"`, 79 encode(entry.value), 80 `",` 81 ]); 82 } 83 buffer_[length_ - 1] = '}'; 84 } 85 } 86 87 void serializeLogs(const ref EventContext eventContext) 88 { 89 if (eventContext.logs.data) 90 { 91 write(`,"logs":[`); 92 93 foreach(ref entry; eventContext.logs.data) 94 { 95 writeAll([ 96 "[", 97 "%.6f".format(entry.endOffset.toUnits!Seconds), 98 ",", 99 "%d".format(entry.verbosity), 100 `,"`, 101 encode(entry.msg), 102 `"],` 103 ]); 104 } 105 buffer_[length_ - 1] = ']'; 106 } 107 } 108 109 void serializeUserTimers(const ref EventContext eventContext) 110 { 111 if (eventContext.timers.data) 112 { 113 write(`,"timers":[`); 114 115 foreach(ref timer; eventContext.timers.data) 116 { 117 writeAll([ 118 `["`, 119 timer.name, 120 `",`, 121 "%.6f".format(timer.start.toUnits!Seconds), 122 `,`, 123 "%.6f".format(timer.end.toUnits!Seconds), 124 `],` 125 ]); 126 } 127 buffer_[length_ - 1] = ']'; 128 } 129 } 130 131 void writeAll(string[] strings) 132 { 133 foreach(ref str; strings) 134 { 135 write(str); 136 } 137 } 138 139 void write(string output) 140 { 141 foreach(ref c; output) 142 { 143 buffer_[length_++] = c; 144 } 145 } 146 147 148 // Fast lookup for escapes 149 static immutable string[256] mapping; 150 shared static this() 151 { 152 foreach (i; 0..32) 153 { 154 mapping[i] = `\u` ~ format("%04x", i); 155 } 156 157 mapping[8] = `\b`; 158 mapping[9] = `\t`; 159 mapping[10] = `\n`; 160 mapping[12] = `\f`; 161 mapping[13] = `\r`; 162 mapping[34] = `\"`; 163 mapping[47] = `\/`; 164 mapping[92] = `\\`; 165 } 166 167 /* 168 Encodes string literals. Input is assumed to be valid UTF8 string 169 and will throw a UTFException if this is not the case 170 171 - Control characters 0x0-0x1F plus 34, 47, 92 are escaped 172 - Multibyte characters are written as is (not encoded using \uXXXX) 173 */ 174 string encode(string input) 175 { 176 auto buffer = Appender!string(); 177 // Should be enough to handle most cases (except where most 178 // characters are control characters 179 buffer.reserve(input.length * 2); 180 181 uint index; 182 while (index < input.length) 183 { 184 auto size = input.stride(index); 185 // Single byte character 186 if (size == 1) 187 { 188 const char c = input[index++]; 189 const string mapped = mapping[c]; 190 191 // char is mapped to escape 192 if (mapped != string.init) 193 { 194 buffer.put(mapped); 195 } 196 // normal char 197 else 198 { 199 buffer.put(c); 200 } 201 } 202 // Multibyte 203 else 204 { 205 buffer.put(input[index..index+size]); 206 index += size; 207 } 208 } 209 210 return buffer.data; 211 } 212 } 213 214 version (unittest) 215 { 216 import core.exception : RangeError; 217 import std.conv : hexString; 218 import std.exception : assertThrown; 219 import std.json : JSONValue, parseJSON; 220 import std.stdio : writeln; 221 222 import logdefer.time.duration : Millis, Nanos; 223 import unixtime : UnixTimeHiRes; 224 225 auto now = UnixTimeHiRes(1434608500); 226 Nanos duration = Millis(250); 227 } 228 229 unittest 230 { 231 writeln("[UnitTest JSONSerializer] - basic start and end timer"); 232 233 auto ec = EventContext(); 234 ec.realStartTime = now; 235 ec.endOffset = duration; 236 237 JSONValue json; 238 auto serializer = JSONSerializer!(DelegateWriter)((string msg) { json = parseJSON(msg); }); 239 serializer(ec); 240 241 assert(json["start"].floating == 1434608500.0); 242 assert(json["end"].floating == 0.250000); 243 assert("logs" !in json); 244 assert("data" !in json); 245 246 } 247 248 unittest 249 { 250 writeln("[UnitTest JSONSerializer] - data section"); 251 252 auto ec = EventContext(); 253 ec.realStartTime = now; 254 ec.endOffset = duration; 255 ec.metadata["7"] = "2.1"; 256 ec.metadata["foo"] = "bar"; 257 258 JSONValue json; 259 auto serializer = JSONSerializer!(DelegateWriter)((string msg) { json = parseJSON(msg); }); 260 serializer(ec); 261 262 assert("start" in json); 263 assert("end" in json); 264 assert("logs" !in json); 265 assert("data" in json); 266 assert(json["data"].object().length == 2); 267 assert(json["data"]["7"].str == "2.1"); 268 assert(json["data"]["foo"].str == "bar"); 269 } 270 271 unittest 272 { 273 writeln("[UnitTest JSONSerializer] - log entries"); 274 275 auto ec = EventContext(); 276 ec.realStartTime = now; 277 ec.endOffset = duration; 278 ec.logs.put(LogEntry(duration, 10, "line 1")); 279 ec.logs.put(LogEntry((duration * 2), 15, "line 2")); 280 ec.logs.put(LogEntry((duration * 3), 15, "")); 281 282 JSONValue json; 283 auto serializer = JSONSerializer!(DelegateWriter)((string msg) { json = parseJSON(msg); }); 284 serializer(ec); 285 286 assert("start" in json); 287 assert("end" in json); 288 assert("logs" in json); 289 assert("data" !in json); 290 assert(json["logs"].array.length == 3); 291 292 assert(json["logs"][0][0].floating == 0.25); 293 assert(json["logs"][0][1].integer == 10); 294 assert(json["logs"][0][2].str == "line 1"); 295 296 assert(json["logs"][1][0].floating == 0.5); 297 assert(json["logs"][1][1].integer == 15); 298 assert(json["logs"][1][2].str == "line 2"); 299 300 assert(json["logs"][2][0].floating == 0.75); 301 assert(json["logs"][2][1].integer == 15); 302 assert(json["logs"][2][2].str == ""); 303 } 304 305 unittest 306 { 307 writeln("[UnitTest JSONSerializer] - UTF-8"); 308 309 auto ec = EventContext(); 310 ec.realStartTime = now; 311 ec.endOffset = duration; 312 ec.metadata[hexString!("e8af b7e6 b182 4944 0a00")] = hexString!("3731 202d 20e9 98bf e5b0 94e6 b395 0a00"); 313 ec.logs.put(LogEntry(duration, 1, hexString!("e994 99e8 afaf 0a00"))); 314 315 JSONValue json; 316 auto serializer = JSONSerializer!(DelegateWriter)((string msg) { json = parseJSON(msg); }); 317 serializer(ec); 318 319 assert("start" in json); 320 assert("end" in json); 321 assert("logs" in json); 322 assert("data" in json); 323 324 assert(json["logs"].array.length == 1); 325 assert(json["logs"][0][2].str == hexString!("e994 99e8 afaf 0a00")); 326 327 assert(json["data"][hexString!("e8af b7e6 b182 4944 0a00")].str == hexString!("3731 202d 20e9 98bf e5b0 94e6 b395 0a00")); 328 } 329 330 unittest 331 { 332 writeln("[UnitTest JSONSerializer] - invalid UTF-8"); 333 334 auto ec = EventContext(); 335 ec.realStartTime = now; 336 ec.endOffset = duration; 337 // Invalid sequence 338 ec.logs.put(LogEntry(duration, 1, hexString!("ff01"))); 339 340 JSONValue json; 341 auto serializer = JSONSerializer!(DelegateWriter)((string msg) { json = parseJSON(msg); }); 342 assertThrown!UTFException(serializer(ec)); 343 } 344 345 unittest 346 { 347 writeln("[UnitTest JSONSerializer] - control characters"); 348 349 auto ec = EventContext(); 350 ec.realStartTime = now; 351 ec.endOffset = duration; 352 ec.logs.put(LogEntry( 353 duration, 10, "\t\tconh\btrol\nchar//a\\cters\0") 354 ); 355 356 JSONValue json; 357 auto serializer = JSONSerializer!(DelegateWriter)((string msg) { json = parseJSON(msg); }); 358 serializer(ec); 359 360 assert("start" in json); 361 assert("end" in json); 362 assert("logs" in json); 363 364 assert(json["logs"][0][2].str == "\t\tconh\btrol\nchar//a\\cters\0"); 365 } 366 367 unittest 368 { 369 writeln("[UnitTest JSONSerializer] - message too long"); 370 371 auto ec = EventContext(); 372 ec.realStartTime = now; 373 ec.endOffset = duration; 374 375 foreach(i; 0..10000) 376 { 377 ec.logs.put(LogEntry( 378 duration, 10, "too long") 379 ); 380 } 381 382 JSONValue json; 383 auto serializer = JSONSerializer!(DelegateWriter)((string msg) { json = parseJSON(msg); }); 384 assertThrown!RangeError(serializer(ec)); 385 }