1 // Written in the D programming language.
2 /**
3 Source: $(PHOBOSSRC std/logger/filelogger.d)
4 */
5 module std.logger.filelogger;
6 
7 import std.logger.core;
8 import std.stdio;
9 
10 import std.typecons : Flag;
11 
12 /** An option to create $(LREF FileLogger) directory if it is non-existent.
13 */
14 alias CreateFolder = Flag!"CreateFolder";
15 
16 /** This `Logger` implementation writes log messages to the associated
17 file. The name of the file has to be passed on construction time. If the file
18 is already present new log messages will be append at its end.
19 */
20 class FileLogger : Logger
21 {
22     import std.concurrency : Tid;
23     import std.datetime.systime : SysTime;
24     import std.format.write : formattedWrite;
25 
26     /** A constructor for the `FileLogger` Logger.
27 
28     Params:
29       fn = The filename of the output file of the `FileLogger`. If that
30       file can not be opened for writting an exception will be thrown.
31       lv = The `LogLevel` for the `FileLogger`. By default the
32 
33     Example:
34     -------------
35     auto l1 = new FileLogger("logFile");
36     auto l2 = new FileLogger("logFile", LogLevel.fatal);
37     auto l3 = new FileLogger("logFile", LogLevel.fatal, CreateFolder.yes);
38     -------------
39     */
40     this(this This)(const string fn, const LogLevel lv = LogLevel.all)
41     {
42          this(fn, lv, CreateFolder.yes);
43     }
44 
45     /** A constructor for the `FileLogger` Logger that takes a reference to
46     a `File`.
47 
48     The `File` passed must be open for all the log call to the
49     `FileLogger`. If the `File` gets closed, using the `FileLogger`
50     for logging will result in undefined behaviour.
51 
52     Params:
53       fn = The file used for logging.
54       lv = The `LogLevel` for the `FileLogger`. By default the
55       `LogLevel` for `FileLogger` is `LogLevel.all`.
56       createFileNameFolder = if yes and fn contains a folder name, this
57       folder will be created.
58 
59     Example:
60     -------------
61     auto file = File("logFile.log", "w");
62     auto l1 = new FileLogger(file);
63     auto l2 = new FileLogger(file, LogLevel.fatal);
64     -------------
65     */
66     this(this This)(const string fn, const LogLevel lv, CreateFolder createFileNameFolder)
67     {
68         import std.file : exists, mkdirRecurse;
69         import std.path : dirName;
70         import std.conv : text;
71 
72         super(lv);
73         this.filename = fn;
74 
75         if (createFileNameFolder)
76         {
77             auto d = dirName(this.filename);
78             mkdirRecurse(d);
79             assert(exists(d), text("The folder the FileLogger should have",
80                                    " created in '", d,"' could not be created."));
81         }
82 
83         // Cast away `shared` when the constructor is inferred shared.
84         () @trusted { (cast() this.file_).open(this.filename, "a"); }();
85     }
86 
87     /** A constructor for the `FileLogger` Logger that takes a reference to
88     a `File`.
89 
90     The `File` passed must be open for all the log call to the
91     `FileLogger`. If the `File` gets closed, using the `FileLogger`
92     for logging will result in undefined behaviour.
93 
94     Params:
95       file = The file used for logging.
96       lv = The `LogLevel` for the `FileLogger`. By default the
97       `LogLevel` for `FileLogger` is `LogLevel.all`.
98 
99     Example:
100     -------------
101     auto file = File("logFile.log", "w");
102     auto l1 = new FileLogger(file);
103     auto l2 = new FileLogger(file, LogLevel.fatal);
104     -------------
105     */
106     this(File file, const LogLevel lv = LogLevel.all) @safe
107     {
108         super(lv);
109         this.file_ = file;
110     }
111 
112     /** If the `FileLogger` is managing the `File` it logs to, this
113     method will return a reference to this File.
114     */
115     @property File file() @safe
116     {
117         return this.file_;
118     }
119 
120     /* This method overrides the base class method in order to log to a file
121     without requiring heap allocated memory. Additionally, the `FileLogger`
122     local mutex is logged to serialize the log calls.
123     */
124     override protected void beginLogMsg(string file, int line, string funcName,
125         string prettyFuncName, string moduleName, LogLevel logLevel,
126         Tid threadId, SysTime timestamp, Logger logger)
127         @safe
128     {
129         import std.string : lastIndexOf;
130         ptrdiff_t fnIdx = file.lastIndexOf('/') + 1;
131         ptrdiff_t funIdx = funcName.lastIndexOf('.') + 1;
132 
133         auto lt = this.file_.lockingTextWriter();
134         systimeToISOString(lt, timestamp);
135         import std.conv : to;
136         formattedWrite(lt, " [%s] %s:%u:%s ", logLevel.to!string,
137                 file[fnIdx .. $], line, funcName[funIdx .. $]);
138     }
139 
140     /* This methods overrides the base class method and writes the parts of
141     the log call directly to the file.
142     */
143     override protected void logMsgPart(scope const(char)[] msg)
144     {
145         formattedWrite(this.file_.lockingTextWriter(), "%s", msg);
146     }
147 
148     /* This methods overrides the base class method and finalizes the active
149     log call. This requires flushing the `File` and releasing the
150     `FileLogger` local mutex.
151     */
152     override protected void finishLogMsg()
153     {
154         this.file_.lockingTextWriter().put("\n");
155         this.file_.flush();
156     }
157 
158     /* This methods overrides the base class method and delegates the
159     `LogEntry` data to the actual implementation.
160     */
161     override protected void writeLogMsg(ref LogEntry payload)
162     {
163         this.beginLogMsg(payload.file, payload.line, payload.funcName,
164             payload.prettyFuncName, payload.moduleName, payload.logLevel,
165             payload.threadId, payload.timestamp, payload.logger);
166         this.logMsgPart(payload.msg);
167         this.finishLogMsg();
168     }
169 
170     /** If the `FileLogger` was constructed with a filename, this method
171     returns this filename. Otherwise an empty `string` is returned.
172     */
173     string getFilename()
174     {
175         return this.filename;
176     }
177 
178     /** The `File` log messages are written to. */
179     protected File file_;
180 
181     /** The filename of the `File` log messages are written to. */
182     protected string filename;
183 }
184 
185 @system unittest
186 {
187     import std.array : empty;
188     import std.file : deleteme, remove;
189     import std.string : indexOf;
190 
191     string filename = deleteme ~ __FUNCTION__ ~ ".tempLogFile";
192     auto l = new FileLogger(filename);
193 
194     scope(exit)
195     {
196         remove(filename);
197     }
198 
199     string notWritten = "this should not be written to file";
200     string written = "this should be written to file";
201 
202     l.logLevel = LogLevel.critical;
203     l.log(LogLevel.warning, notWritten);
204     l.log(LogLevel.critical, written);
205     destroy(l);
206 
207     auto file = File(filename, "r");
208     string readLine = file.readln();
209     assert(readLine.indexOf(written) != -1, readLine);
210     readLine = file.readln();
211     assert(readLine.indexOf(notWritten) == -1, readLine);
212 }
213 
214 @safe unittest
215 {
216     import std.file : rmdirRecurse, exists, deleteme;
217     import std.path : dirName;
218 
219     const string tmpFolder = dirName(deleteme);
220     const string filepath = tmpFolder ~ "/bug15771/minas/oops/";
221     const string filename = filepath ~ "output.txt";
222     assert(!exists(filepath));
223 
224     auto f = new FileLogger(filename, LogLevel.all, CreateFolder.yes);
225     scope(exit) () @trusted { rmdirRecurse(tmpFolder ~ "/bug15771"); }();
226 
227     f.log("Hello World!");
228     assert(exists(filepath));
229     f.file.close();
230 }
231 
232 @system unittest
233 {
234     import std.array : empty;
235     import std.file : deleteme, remove;
236     import std.string : indexOf;
237 
238     string filename = deleteme ~ __FUNCTION__ ~ ".tempLogFile";
239     auto file = File(filename, "w");
240     auto l = new FileLogger(file);
241 
242     scope(exit)
243     {
244         remove(filename);
245     }
246 
247     string notWritten = "this should not be written to file";
248     string written = "this should be written to file";
249 
250     l.logLevel = LogLevel.critical;
251     l.log(LogLevel.warning, notWritten);
252     l.log(LogLevel.critical, written);
253     file.close();
254 
255     file = File(filename, "r");
256     string readLine = file.readln();
257     assert(readLine.indexOf(written) != -1, readLine);
258     readLine = file.readln();
259     assert(readLine.indexOf(notWritten) == -1, readLine);
260     file.close();
261 }
262 
263 @system unittest
264 {
265     auto dl = cast(FileLogger) sharedLog;
266     assert(dl !is null);
267     assert(dl.logLevel == LogLevel.info);
268     assert(globalLogLevel == LogLevel.all);
269 
270     auto tl = cast(StdForwardLogger) stdThreadLocalLog;
271     assert(tl !is null);
272     stdThreadLocalLog.logLevel = LogLevel.all;
273 }
274 
275 @safe unittest
276 {
277     // we don't need to actually run the code, only make sure
278     // it compiles
279     static void _() {
280         auto l = new shared FileLogger("");
281     }
282 }