1 /** 2 3 D module for GELF format. 4 5 Graylog Extended Logging Format (https://www.graylog.org/resources/gelf/) 6 The Graylog Extended Log Format (GELF) is a log format that avoids the shortcomings of classic plain syslog, when logging to Graylog (graylog.org) 7 8 GELF is a pure JSON format. 9 This module aims to provide a very simple way of generating log messages in GELF format. 10 11 Author: Adil Baig 12 */ 13 14 module gelf.protocol; 15 16 import std.conv : to; 17 import std.datetime : SysTime; 18 19 public : 20 21 enum Level { 22 EMERGENCY = 0, 23 ALERT = 1, 24 CRITICAL = 2, 25 ERROR = 3, 26 WARNING = 4, 27 NOTICE = 5, 28 INFO = 6, 29 DEBUG = 7, 30 } 31 32 /** 33 34 This struct provides a convenient way to create and inspect a GELF message. 35 36 Example: 37 ------------------------- 38 writeln(Message("localhost","HUGE ERROR!")); //This creates a bare minimum GELF message 39 writeln(Message("localhost","HUGE ERROR!", Level.ERROR)); //This example uses the overloaded contructor to report an error 40 ------------------------- 41 42 GELF messages can also be created in multiple steps. This allows you to add in custom values using loops or other code 43 44 Example: 45 ------------------------- 46 // Let's create a GELF message using properties 47 auto m = Message("localhost","HUGE ERROR!"); 48 m.level = Level.ERROR; 49 m.timestamp = Clock.currTime(); 50 m.a_number = 7; 51 52 // Now let's add some environment variables in 53 import std.process; 54 foreach(v, k; environment.toAA()) 55 m[k] = v; 56 57 writeln(m); // {"version":1.1, "host:"localhost", "short_message":"HUGE ERROR!", "timestamp":1447275799, "level":3, "_a_number":7, "_PATH":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games", ...} 58 ------------------------- 59 60 A simpler method is to use a fluent interface. This example also shows how values from a Message can be read and used in a conditional statement. 61 62 Example: 63 ------------------------- 64 // Use the fluent interface .. 65 auto m1 = Message("localhost", "Divide by zero error").level(Level.ERROR).timestamp(Clock.currTime()).numerator(1000).PATH("/usr/bin/"); 66 67 // Values can be checked for conditions. Here we only send messages of Level.ERROR or more severity to Graylog 68 if(m1.level <= Level.ERROR) { 69 auto s = new UdpSocket(); 70 s.connect(new InternetAddress("localhost", 11200)); 71 s.send(m1.toString()); 72 } 73 74 writeln(m1); //{"version":1.1, "host:"localhost", "short_message":"Divide by zero error", "timestamp":1447274923, "level":3, "_numerator":1000, "_PATH":"/usr/bin/"} 75 ------------------------- 76 77 */ 78 struct Message 79 { 80 const string host; 81 const string short_message; 82 string full_message; 83 84 private Level lvl = Level.ALERT; 85 private Field[string] fields; 86 private string ts; // The timestamp 87 88 this(string host, string short_message) @safe nothrow @nogc 89 { 90 this.host = host; 91 this.short_message = short_message; 92 } 93 94 this(string host, string short_message, Level level) @safe nothrow @nogc 95 { 96 this(host, short_message).level(level); 97 } 98 99 auto level() { return lvl; } 100 auto level(Level l) { lvl = l; return this; } 101 102 auto fullMessage() { return full_message; } 103 auto fullMessage(string msg) { full_message = msg; return this; } 104 105 auto timestamp(SysTime sysTime) 106 { 107 ts = to!string(sysTime.toUnixTime()); 108 return this; 109 } 110 111 auto timestamp(size_t timestamp) 112 { 113 ts = to!string(timestamp); 114 return this; 115 } 116 117 auto timestamp(double timestamp) 118 { 119 ts = to!string(timestamp); 120 return this; 121 } 122 123 auto opDispatch(string s, T)(T i) 124 { 125 fields[s] = Field(i); 126 return this; 127 } 128 129 string opDispatch(string s)() 130 { 131 return fields[s].val; 132 } 133 134 void opIndexAssign(string key, string value) @safe 135 { 136 fields[key] = Field(value); 137 } 138 139 string toString() @safe 140 { 141 import std.array : appender; 142 143 auto app = appender!string(); 144 toString((const(char)[] s) { app.put(s); }); 145 146 return app.data; 147 } 148 149 immutable(ubyte[]) toBytes() @safe 150 { 151 return cast(typeof(return))this.toString(); 152 } 153 154 void toString(Dg)(scope Dg sink) const 155 if (__traits(compiles, sink(['a']))) 156 { 157 import std.string : replace; 158 159 sink("{\"version\":1.1, \"host\":\"" ~ host ~ "\", \"short_message\":\"" ~ replace(short_message,"\"", "\\\"") ~ "\""); 160 161 if (full_message) 162 sink(", \"full_message\":\"" ~ replace(full_message, "\"", "\\\"") ~ "\""); 163 164 if (ts) 165 sink(", \"timestamp\":" ~ ts); 166 167 sink(", \"level\":" ~ to!string(cast(int)lvl)); 168 169 foreach(k, v; fields) { 170 sink(", \"_"~k~"\":"); 171 sink((v.enclose == 0) ? v.val : "\"" ~ replace(v.val,"\"", "\\\"") ~ "\""); 172 } 173 174 sink("}"); 175 } 176 } 177 178 private: 179 180 struct Field 181 { 182 bool enclose = false; 183 string val; 184 185 this(V)(V val) 186 { 187 static if (!is(V : size_t) || is(V == enum)) 188 enclose = true; 189 190 this.val = to!string(val); 191 } 192 } 193 194 @safe unittest 195 { 196 auto s = Message("localhost","SOME ERROR!").toString(); 197 auto s1 = "{\"version\":1.1, \"host\":\"localhost\", \"short_message\":\"SOME ERROR!\", \"level\":1}"; 198 199 s = Message("localhost","SOME ERROR!", Level.ERROR).toString(); 200 s1 = "{\"version\":1.1, \"host\":\"localhost\", \"short_message\":\"SOME ERROR!\", \"level\":3}"; 201 assert(s == s1); 202 203 auto m = Message("localhost","SOME ERROR!").PATH("/usr/bin/").Timeout(3000).level(Level.ERROR); 204 assert(m.PATH == "/usr/bin/"); 205 assert(m.Timeout == "3000"); //NOTE : Numbers are converted to strings and stored 206 assert(m.level == Level.ERROR); 207 208 // Test if serialization of Enums is correct 209 enum { 210 blah 211 } 212 213 enum SomeEnum { 214 SE_A, 215 SE_B, 216 SE_C 217 } 218 219 s = Message("localhost","SOME ERROR!").blah(blah).aenum(SomeEnum.SE_A).toString(); 220 s1 = "{\"version\":1.1, \"host\":\"localhost\", \"short_message\":\"SOME ERROR!\", \"level\":1, \"_aenum\":\"SE_A\", \"_blah\":0}"; 221 assert(s == s1); 222 223 // Strings are automatically escaped. 224 s = Message("localhost", "SOME ERROR!").fullMessage("{\"name\" : \"Adil\"}").toString(); 225 s1 = "{\"version\":1.1, \"host\":\"localhost\", \"short_message\":\"SOME ERROR!\", \"full_message\":\"{\\\"name\\\" : \\\"Adil\\\"}\", \"level\":1}"; 226 assert(s == s1); 227 228 //Check toBytes 229 assert(m.toBytes() == [123, 34, 118, 101, 114, 115, 105, 111, 110, 34, 58, 49, 46, 49, 44, 32, 34, 104, 111, 115, 116, 34, 58, 34, 108, 111, 99, 97, 108, 104, 111, 115, 116, 34, 44, 32, 34, 115, 104, 111, 114, 116, 95, 109, 101, 115, 115, 97, 103, 101, 34, 58, 34, 83, 79, 77, 69, 32, 69, 82, 82, 79, 82, 33, 34, 44, 32, 34, 108, 101, 118, 101, 108, 34, 58, 51, 44, 32, 34, 95, 84, 105, 109, 101, 111, 117, 116, 34, 58, 51, 48, 48, 48, 44, 32, 34, 95, 80, 65, 84, 72, 34, 58, 34, 47, 117, 115, 114, 47, 98, 105, 110, 47, 34, 125]); 230 }