1 // Written in the D programming language.
2 /*
3 NYSL Version 0.9982
4 
5 A. This software is "Everyone'sWare". It means:
6   Anybody who has this software can use it as if he/she is
7   the author.
8 
9   A-1. Freeware. No fee is required.
10   A-2. You can freely redistribute this software.
11   A-3. You can freely modify this software. And the source
12       may be used in any software with no limitation.
13   A-4. When you release a modified version to public, you
14       must publish it with your name.
15 
16 B. The author is not responsible for any kind of damages or loss
17   while using or misusing this software, which is distributed
18   "AS IS". No warranty of any kind is expressed or implied.
19   You use AT YOUR OWN RISK.
20 
21 C. Copyrighted to Kazuki KOMATSU
22 
23 D. Above three clauses are applied both to source and binary
24   form of this software.
25 */
26 
27 /**
28 このモジュールは、ロガーおよびログ出力を行いやすくするための機能を提供します。
29 
30 Examples:
31 -------------
32 module foo;
33 
34 import std.stdio;
35 import graphite.utils.logger;
36 import carbon.templates;
37 
38 mixin defGlobalVariables!("logger", "logFile",
39 {
40     auto file = File("foo.txt", "w");
41     return tuple(.logger!(LogFormat.readable)(file), file);
42 });
43 
44 
45 void main()
46 {
47     int a = 12;
48     // 文字列として出力
49     logger.writeln!"notice"("a is ", a);
50 
51     // 値をそのまま出力
52     logger.trace!"notice"(a, a * 2, a * 3);
53 
54     // 例外の通知
55     try logger.captureException(enforce(0));
56     catch(Exception ex){
57         writeln(ex);
58         throw ex;
59     }
60 }
61 -------------
62 */
63 module graphite.utils.log;
64 
65 import std.range;
66 import std.format;
67 import std.stdio;
68 import std.variant;
69 import std.json;
70 
71 import graphite.utils.json;
72 
73 
74 /**
75 ログの重要度を表します
76 */
77 enum Level
78 {
79     verbose,    ///
80     notice,     ///
81     warning,    ///
82     error,      ///
83     fatalError, ///
84     silent,     ///
85 }
86 
87 
88 /**
89 ログのフロントエンドとバックエンドでやりとりされるデータです。
90 */
91 struct LogElement(T...)
92 {
93     Level level;
94     string id;
95     string file;
96     size_t line;
97     string func;
98     T msg;
99 }
100 
101 
102 /**
103 ロガー
104 */
105 struct Logger(Backend)
106 if(isOutputRange!(Backend, LogElement!(Variant)))
107 {
108     version(D_Ddoc)
109     {
110         /**
111         ロガーに値を出力します。
112         値は$(D LogElement!T)に格納され、バックエンドに渡されます。
113         */
114         void trace(Level lv, T...)(T args);
115         void trace(string lv, T...)(T args);    /// ditto
116 
117 
118         /**
119         ロガーに文字列を出力します。
120         文字列は$(D LogElement!string)に格納され、バックエンドに渡されます。
121         */
122         void writeln(Level lv, T...)(T args);
123         void writeln(string lv, T...)(T args);                  /// ditto
124         void writefln(Level lv, T...)(string format, T args);   /// ditto
125         void writefln(Level lv, T...)(string format, T args);   /// ditto
126 
127 
128         /**
129         式を評価した結果例外やエラーが発生したら、ロガーにそれを出力します。
130         例外は$(D LogElement!Ex)に格納され、エラーは$(D LogElement!Er)に格納され、バックエンドに渡されます。
131 
132         例外が発生しない場合、式の評価結果をそのまま返します。
133         */
134         auto ref T captureException(Ex = Exception, Er = Error, T)(lazy T dg);
135     }
136 
137     alias instance this;
138 
139     LoggerImpl instance(string file = __FILE__, size_t line = __LINE__, string fn = __PRETTY_FUNCTION__)
140     {
141         return LoggerImpl(&this, file, line, fn);
142     }
143 
144 
145     static struct LoggerImpl
146     {
147         @disable this(this);
148 
149 
150         void trace(Level lv, T...)(T args)
151         if(lv != Level.silent)
152         {
153             if(lv >= logger.thrLv){
154                 LogElement!T log;
155                 log.level = lv;
156                 log.id = logger._id;
157                 log.file = _file;
158                 log.line = _line;
159                 log.func = _func;
160                 log.msg = args;
161                 logger._b.put(log);
162             }
163         }
164 
165 
166         void trace(string lv, T...)(T args)
167         {
168             trace!(mixin(`Level.` ~ lv))(args);
169         }
170 
171 
172         void writeln(Level lv, T...)(T args)
173         {
174             if(lv >= logger.thrLv){
175                 auto app = appender!string();
176                 foreach(i, U; T)
177                     app.formattedWrite("%s", args[i]);
178                 trace!lv(app.data);
179             }
180         }
181 
182 
183         void writeln(string lv, T...)(T args)
184         {
185             writeln!(mixin(`Level.` ~ lv))(args);
186         }
187 
188 
189         void writefln(Level lv, T...)(string format, T args)
190         {
191             if(lv >= logger.thrLv){
192                 auto app = appender!string();
193                 app.formattedWrite(format, args);
194                 trace!lv(app.data);
195             }
196         }
197 
198 
199         void writefln(string lv, T...)(string format, T args)
200         {
201             writefln!(mixin(`Level.` ~ lv))(format, args);
202         }
203 
204 
205         auto ref T captureException(Ex = Exception, Er = Error, T)(lazy T dg)
206         {
207             try return dg();
208             catch(Ex ex){
209                 trace!(Level.error)(ex);
210                 throw ex;
211             }
212             catch(Er er){
213                 trace!(Level.fatalError)(er);
214                 throw er;
215             }
216         }
217 
218         Logger* logger;
219 
220       private:
221         string _file;
222         size_t _line;
223         string _func;
224     }
225 
226 
227     /**
228     ログに出力するしきい値レベルを設定・取得します。
229 
230     Examples:
231     ----------------
232     logger.thrLv = Level.warning;
233     logger.writeln!"notice"("このメッセージはログに残らない");
234     logger.writeln!"warning"("ログに残るメッセージ");
235     ----------------
236     */
237     Level thrLv() @property
238     {
239         return _thrLv;
240     }
241 
242 
243     /// ditto
244     void thrLv(Level level) @property
245     {
246         this._thrLv = level;
247     }
248 
249 
250   static if(is(typeof(_b.writer)))
251   {
252     /**
253     バックエンドが$(D writer)プロパティをもつ場合、その$(D writer)プロパティをそのまま返します。
254     */
255     auto ref writer() @property
256     {
257         return _b.writer;
258     }
259   }
260 
261 
262   private:
263     Backend _b;
264     string _id;
265     Level _thrLv;
266 }
267 
268 
269 /**
270 バックエンドを指定してロガーを構築します。
271 どのような$(D T...)に対しても$(D isOuputRange!(R, LogElement!T))が$(D true)となるような出力レンジがバックエンドとなります。
272 */
273 auto logger(alias id = __MODULE__, B)(B backend)
274 if(isOutputRange!(B, LogElement!Variant))
275 {
276   static if(is(typeof(id) : string) && is(typeof({ enum _unused_ = id; })))
277     enum idstr = id;
278   else
279     enum idstr = id.stringof;
280 
281     return Logger!B(backend, idstr, Level.verbose);
282 }
283 
284 unittest {
285     static struct Backend1(T)
286     {
287         LogElement!T[] logs;
288 
289         void put(U...)(LogElement!U log)
290         {
291           static if(U.length == 1 && is(U[0] == T))
292             logs ~= log;
293         }
294     }
295 
296     import std.typecons;
297 
298     LogElement!string[] arr;
299     auto b = Backend1!string(arr);
300     auto logger = .logger(&b);
301     logger.trace!"fatalError"("foo");     size_t instLine = __LINE__;
302     assert(b.logs[0].level == Level.fatalError);
303     assert(b.logs[0].id == __MODULE__);
304     assert(b.logs[0].file == __FILE__);
305     assert(b.logs[0].line == instLine);
306     assert(b.logs[0].func == __PRETTY_FUNCTION__);
307     assert(b.logs[0].msg[0] == "foo");
308 
309     logger.writefln!"warning"("%s is %s?", 124, "124"); instLine = __LINE__;
310     assert(b.logs[1].level == Level.warning);
311     assert(b.logs[1].id == __MODULE__);
312     assert(b.logs[1].file == __FILE__);
313     assert(b.logs[1].line == instLine);
314     assert(b.logs[1].func == __PRETTY_FUNCTION__);
315     assert(b.logs[1].msg[0] == "124 is 124?");
316 
317     logger.writeln!"notice"(124, " is " "124", "?"); instLine = __LINE__;
318     assert(b.logs[2].level == Level.notice);
319     assert(b.logs[2].id == __MODULE__);
320     assert(b.logs[2].file == __FILE__);
321     assert(b.logs[2].line == instLine);
322     assert(b.logs[2].func == __PRETTY_FUNCTION__);
323     assert(b.logs[2].msg[0] == "124 is 124?");
324 }
325 
326 unittest {
327     import std.exception;
328     import std.conv;
329 
330     static struct Backend1
331     {
332         string[] logs;
333 
334         void put(U...)(LogElement!U log)
335         {
336             auto app = appender!string();
337 
338             foreach(i, E; U)
339                 app.formattedWrite("%s", log.msg[i]);
340 
341             logs ~= app.data;
342         }
343     }
344 
345     Backend1 b;
346     auto logger = .logger(&b);
347 
348     string exStr;
349     try logger.captureException(enforce(0));
350     catch(Exception ex)
351         exStr = ex.to!string();
352 
353     assert(b.logs[0] == exStr);
354 }
355 
356 
357 /**
358 出力レンジ、もしくは$(D std.stdio.File)を最終的な出力先としてロガーを構築します。
359 出力のフォーマットや形式は、$(D formattedWriter)によって決定されます。
360 ロガーに渡されたログは、$(D formattedWriter(w, log))によって$(D Writer w)に出力されます。
361 */
362 auto logger(alias formattedWriter, alias id = __MODULE__, Writer)(Writer w)
363 if(is(typeof(backend!formattedWriter(w))))
364 {
365     auto b = backend!formattedWriter(w);
366     return .logger!id(b);
367 }
368 
369 
370 /**
371 ファイルを最終出力先とするようなバックエンドを構築します。
372 ファイルへの出力フォーマットや形式は、$(D formattedWriter(file.lockingTextWriter, log))により決定されます。
373 */
374 auto backend(alias formattedWriter)(File file)
375 if(is(typeof(backend!formattedWriter(file.lockingTextWriter))))
376 {
377     static struct Backend
378     {
379         void put(T...)(LogElement!T log)
380         {
381             auto w = _file.lockingTextWriter;
382             formattedWriter(w, log);
383         }
384 
385       private:
386         File _file;
387     }
388 
389     return Backend(file);
390 }
391 
392 
393 unittest
394 {
395     import std.conv : to;
396     import std.string : chomp;
397 
398     immutable filename = "unittest_file.txt";
399     scope(exit)
400             std.file.remove(filename);
401 
402     LogElement!() log;
403     {
404         auto b = backend!((ref w, log){ w.put(to!string(log)); })(File(filename, "w"));
405         b.put(log);
406     }
407 
408     {
409         assert(chomp(std.file.readText(filename)) == to!string(log));
410     }
411 }
412 
413 
414 /**
415 出力レンジを最終出力先とするようなバックエンドを構築します。
416 出力レンジの型および出力フォーマットや形式は、$(D formattedWriter(outputRange, log))により決定されます。
417 */
418 auto backend(alias formattedWriter, Writer)(Writer w)
419 if(is(typeof((ref Writer w, LogElement!Variant log){ formattedWriter(w, log); })))
420 {
421     static struct Backend
422     {
423         void put(T...)(LogElement!T log)
424         {
425             formattedWriter(_w, log);
426         }
427 
428 
429         ref Writer writer() @property
430         {
431             return _w;
432         }
433 
434 
435       private:
436         Writer _w;
437     }
438 
439     return Backend(w);
440 }
441 
442 
443 /// ditto
444 auto backend(alias formattedWriter, Writer)(Writer* w)
445 if(is(typeof(backend!formattedWriter(*w))) && !is(Unqual!Writer == File))
446 {
447     static struct Backend
448     {
449         void put(T...)(LogElement!T log)
450         {
451             formattedWriter(*_w, log);
452         }
453 
454 
455         ref Writer writer() @property
456         {
457             return *_w;
458         }
459 
460 
461       private:
462         Writer* _w;
463     }
464 
465 
466     return Backend(w);
467 }
468 
469 
470 struct LogFormat
471 {
472     @disable this();
473     @disable this(this);
474 
475   static:
476     void readable(Sink, T...)(ref Sink sink, LogElement!T log)
477     if(isOutputRange!(Sink, dchar))
478     {
479         sink.formattedWrite("[%s] @%s[%s(%s) in %s]", log.level, log.id, log.file, log.line, log.func);
480 
481       static if(T.length == 0)
482         sink.put('\n');
483       else static if(T.length == 1)
484         sink.formattedWrite(": %s\n", log.msg);
485       else{
486         sink.put(": {");
487         foreach(i, U; T){
488             sink.formattedWrite("%s", log.msg[i]);
489 
490             if(i != T.length - 1)
491                 sink.put(", ");
492         }
493         sink.put("}\n");
494       }
495     }
496 
497     unittest {
498         auto app = appender!string();
499         readable(app, LogElement!()(Level.verbose, "ID", "FILE", 1111111, "FUNC"));
500         assert(app.data == "[verbose] @ID[FILE(1111111) in FUNC]\n");
501 
502         app = appender!string();
503         readable(app, LogElement!(string)(Level.verbose, "ID", "FILE", 123123, "FUNC", "FOOBAR"));
504         assert(app.data == "[verbose] @ID[FILE(123123) in FUNC]: FOOBAR\n");
505 
506         app = appender!string();
507         readable(app, LogElement!(string, int)(Level.verbose, "ID", "FILE", 123123, "FUNC", "FOOBAR", 323232));
508         assert(app.data == `[verbose] @ID[FILE(123123) in FUNC]: {FOOBAR, 323232}`"\n");
509     }
510 
511 
512     void json(Sink, T...)(ref Sink sink, LogElement!T log)
513     if(isOutputRange!(Sink, dchar))
514     {
515         Variant[string] v;
516 
517         v["level"] = log.level;
518         v["id"] = log.id;
519         v["file"] = log.file;
520         v["line"] = log.line;
521         v["func"] = log.func;
522         v["msg"] = variantArray(log.msg);
523 
524         sink.formattedWrite("%s\n", JSONEnv!null.toJSONValue(v));
525     }
526 
527     unittest {
528         import std.string : chomp;
529 
530         auto app = appender!string();
531         json(app, LogElement!()(Level.verbose, "ID", "FILE", 1111111, "FUNC"));
532         JSONValue jv = parseJSON(chomp(app.data));
533         assert(jv.type == JSON_TYPE.OBJECT);
534         assert(jv["level"].str == "verbose");
535         assert(jv["id"].str == "ID");
536         assert(jv["file"].str == "FILE");
537         assert(1111111 == (jv["line"].type == JSON_TYPE.UINTEGER ? jv["line"].uinteger : jv["line"].integer));
538         assert(jv["func"].str == "FUNC");
539         assert(jv["msg"].array.length == 0);
540 
541         app = appender!string();
542         json(app, LogElement!(string)(Level.verbose, "ID", "FILE", 123123, "FUNC", "FOOBAR"));
543         jv = parseJSON(chomp(app.data));
544         assert(jv.type == JSON_TYPE.OBJECT);
545         assert(jv["level"].str == "verbose");
546         assert(jv["id"].str == "ID");
547         assert(jv["file"].str == "FILE");
548         assert(123123 == (jv["line"].type == JSON_TYPE.UINTEGER ? jv["line"].uinteger : jv["line"].integer));
549         assert(jv["func"].str == "FUNC");
550         assert(jv["msg"].array.length == 1);
551         assert(jv["msg"][0].str == "FOOBAR");
552 
553         app = appender!string();
554         json(app, LogElement!(string, int)(Level.verbose, "ID", "FILE", 123123, "FUNC", "FOOBAR", 323232));
555         jv = parseJSON(chomp(app.data));
556         assert(jv.type == JSON_TYPE.OBJECT);
557         assert(jv["level"].str == "verbose");
558         assert(jv["id"].str == "ID");
559         assert(jv["file"].str == "FILE");
560         assert(123123 == (jv["line"].type == JSON_TYPE.UINTEGER ? jv["line"].uinteger : jv["line"].integer));
561         assert(jv["func"].str == "FUNC");
562         assert(jv["msg"].array.length == 2);
563         assert(jv["msg"][0].str == "FOOBAR");
564         assert(323232 == (jv["msg"][1].type == JSON_TYPE.UINTEGER ? jv["msg"][1].uinteger : jv["msg"][1].integer));
565     }
566 }
567 
568 
569 /**
570 std.stdio.Fileに書き込むLogger
571 */
572 template FileLogger(alias formattedWriter)
573 {
574     alias FileLogger = typeof(.logger!formattedWriter(std.stdio.stdin));
575 }