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 }