1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17
18
19 package org.apache.juli;
20
21 import java.io.BufferedOutputStream;
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStream;
26 import java.io.OutputStreamWriter;
27 import java.io.PrintWriter;
28 import java.io.UnsupportedEncodingException;
29 import java.nio.file.DirectoryStream;
30 import java.nio.file.Files;
31 import java.nio.file.Path;
32 import java.security.AccessController;
33 import java.security.PrivilegedAction;
34 import java.sql.Timestamp;
35 import java.time.DateTimeException;
36 import java.time.LocalDate;
37 import java.time.format.DateTimeFormatter;
38 import java.time.temporal.ChronoUnit;
39 import java.util.concurrent.ExecutorService;
40 import java.util.concurrent.Executors;
41 import java.util.concurrent.ThreadFactory;
42 import java.util.concurrent.atomic.AtomicInteger;
43 import java.util.concurrent.locks.ReadWriteLock;
44 import java.util.concurrent.locks.ReentrantReadWriteLock;
45 import java.util.logging.ErrorManager;
46 import java.util.logging.Filter;
47 import java.util.logging.Formatter;
48 import java.util.logging.Handler;
49 import java.util.logging.Level;
50 import java.util.logging.LogManager;
51 import java.util.logging.LogRecord;
52 import java.util.regex.Pattern;
53
54 /**
55  * Implementation of <b>Handler</b> that appends log messages to a file
56  * named {prefix}{date}{suffix} in a configured directory.
57  *
58  * <p>The following configuration properties are available:</p>
59  *
60  * <ul>
61  *   <li><code>directory</code> - The directory where to create the log file.
62  *    If the path is not absolute, it is relative to the current working
63  *    directory of the application. The Apache Tomcat configuration files usually
64  *    specify an absolute path for this property,
65  *    <code>${catalina.base}/logs</code>
66  *    Default value: <code>logs</code></li>
67  *   <li><code>rotatable</code> - If <code>true</code>, the log file will be
68  *    rotated on the first write past midnight and the filename will be
69  *    <code>{prefix}{date}{suffix}</code>, where date is yyyy-MM-dd. If <code>false</code>,
70  *    the file will not be rotated and the filename will be <code>{prefix}{suffix}</code>.
71  *    Default value: <code>true</code></li>
72  *   <li><code>prefix</code> - The leading part of the log file name.
73  *    Default value: <code>juli.</code></li>
74  *   <li><code>suffix</code> - The trailing part of the log file name. Default value: <code>.log</code></li>
75  *   <li><code>bufferSize</code> - Configures buffering. The value of <code>0</code>
76  *    uses system default buffering (typically an 8K buffer will be used). A
77  *    value of <code>&lt;0</code> forces a writer flush upon each log write. A
78  *    value <code>&gt;0</code> uses a BufferedOutputStream with the defined
79  *    value but note that the system default buffering will also be
80  *    applied. Default value: <code>-1</code></li>
81  *   <li><code>encoding</code> - Character set used by the log file. Default value:
82  *    empty string, which means to use the system default character set.</li>
83  *   <li><code>level</code> - The level threshold for this Handler. See the
84  *    <code>java.util.logging.Level</code> class for the possible levels.
85  *    Default value: <code>ALL</code></li>
86  *   <li><code>filter</code> - The <code>java.util.logging.Filter</code>
87  *    implementation class name for this Handler. Default value: unset</li>
88  *   <li><code>formatter</code> - The <code>java.util.logging.Formatter</code>
89  *    implementation class name for this Handler. Default value:
90  *    <code>java.util.logging.SimpleFormatter</code></li>
91  *   <li><code>maxDays</code> - The maximum number of days to keep the log
92  *    files. If the specified value is <code>&lt;=0</code> then the log files
93  *    will be kept on the file system forever, otherwise they will be kept the
94  *    specified maximum days. Default value: <code>-1</code>.</li>
95  * </ul>
96  */

97 public class FileHandler extends Handler {
98
99     public static final int DEFAULT_MAX_DAYS = -1;
100     public static final int DEFAULT_BUFFER_SIZE = -1;
101
102
103     private static final ExecutorService DELETE_FILES_SERVICE =
104             Executors.newSingleThreadExecutor(new ThreadFactory() {
105                 private static final String NAME_PREFIX = "FileHandlerLogFilesCleaner-";
106                 private final boolean isSecurityEnabled;
107                 private final ThreadGroup group;
108                 private final AtomicInteger threadNumber = new AtomicInteger(1);
109
110                 {
111                     SecurityManager s = System.getSecurityManager();
112                     if (s == null) {
113                         this.isSecurityEnabled = false;
114                         this.group = Thread.currentThread().getThreadGroup();
115                     } else {
116                         this.isSecurityEnabled = true;
117                         this.group = s.getThreadGroup();
118                     }
119                 }
120
121                 @Override
122                 public Thread newThread(Runnable r) {
123                     ClassLoader loader = Thread.currentThread().getContextClassLoader();
124                     try {
125                         // Threads should not be created by the webapp classloader
126                         if (isSecurityEnabled) {
127                             AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
128                                 Thread.currentThread()
129                                         .setContextClassLoader(getClass().getClassLoader());
130                                 return null;
131                             });
132                         } else {
133                             Thread.currentThread()
134                                     .setContextClassLoader(getClass().getClassLoader());
135                         }
136                         Thread t = new Thread(group, r,
137                                 NAME_PREFIX + threadNumber.getAndIncrement());
138                         t.setDaemon(true);
139                         return t;
140                     } finally {
141                         if (isSecurityEnabled) {
142                             AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
143                                 Thread.currentThread().setContextClassLoader(loader);
144                                 return null;
145                             });
146                         } else {
147                             Thread.currentThread().setContextClassLoader(loader);
148                         }
149                     }
150                 }
151             });
152
153     // ------------------------------------------------------------ Constructor
154
155
156     public FileHandler() {
157         this(nullnullnull);
158     }
159
160
161     public FileHandler(String directory, String prefix, String suffix) {
162         this(directory, prefix, suffix, null);
163     }
164
165
166     public FileHandler(String directory, String prefix, String suffix, Integer maxDays) {
167         this(directory, prefix, suffix, maxDays, nullnull);
168     }
169
170
171     public FileHandler(String directory, String prefix, String suffix, Integer maxDays,
172             Boolean rotatable, Integer bufferSize) {
173         this.directory = directory;
174         this.prefix = prefix;
175         this.suffix = suffix;
176         this.maxDays = maxDays;
177         this.rotatable = rotatable;
178         this.bufferSize = bufferSize;
179         configure();
180         openWriter();
181         clean();
182     }
183
184
185     // ----------------------------------------------------- Instance Variables
186
187
188     /**
189      * The as-of date for the currently open log file, or a zero-length
190      * string if there is no open log file.
191      */

192     private volatile String date = "";
193
194
195     /**
196      * The directory in which log files are created.
197      */

198     private String directory;
199
200
201     /**
202      * The prefix that is added to log file filenames.
203      */

204     private String prefix;
205
206
207     /**
208      * The suffix that is added to log file filenames.
209      */

210     private String suffix;
211
212
213     /**
214      * Determines whether the log file is rotatable
215      */

216     private Boolean rotatable;
217
218
219     /**
220      * Maximum number of days to keep the log files
221      */

222     private Integer maxDays;
223
224
225     /**
226      * The PrintWriter to which we are currently logging, if any.
227      */

228     private volatile PrintWriter writer = null;
229
230
231     /**
232      * Lock used to control access to the writer.
233      */

234     protected final ReadWriteLock writerLock = new ReentrantReadWriteLock();
235
236
237     /**
238      * Log buffer size.
239      */

240     private Integer bufferSize;
241
242
243     /**
244      * Represents a file name pattern of type {prefix}{date}{suffix}.
245      * The date is YYYY-MM-DD
246      */

247     private Pattern pattern;
248
249
250     // --------------------------------------------------------- Public Methods
251
252
253     /**
254      * Format and publish a <code>LogRecord</code>.
255      *
256      * @param  record  description of the log event
257      */

258     @Override
259     public void publish(LogRecord record) {
260
261         if (!isLoggable(record)) {
262             return;
263         }
264
265         // Construct the timestamp we will use, if requested
266         Timestamp ts = new Timestamp(System.currentTimeMillis());
267         String tsDate = ts.toString().substring(0, 10);
268
269         writerLock.readLock().lock();
270         try {
271             // If the date has changed, switch log files
272             if (rotatable.booleanValue() && !date.equals(tsDate)) {
273                 // Upgrade to writeLock before we switch
274                 writerLock.readLock().unlock();
275                 writerLock.writeLock().lock();
276                 try {
277                     // Make sure another thread hasn't already done this
278                     if (!date.equals(tsDate)) {
279                         closeWriter();
280                         date = tsDate;
281                         openWriter();
282                         clean();
283                     }
284                 } finally {
285                     // Downgrade to read-lock. This ensures the writer remains valid
286                     // until the log message is written
287                     writerLock.readLock().lock();
288                     writerLock.writeLock().unlock();
289                 }
290             }
291
292             String result = null;
293             try {
294                 result = getFormatter().format(record);
295             } catch (Exception e) {
296                 reportError(null, e, ErrorManager.FORMAT_FAILURE);
297                 return;
298             }
299
300             try {
301                 if (writer != null) {
302                     writer.write(result);
303                     if (bufferSize.intValue() < 0) {
304                         writer.flush();
305                     }
306                 } else {
307                     reportError("FileHandler is closed or not yet initialized, unable to log ["
308                             + result + "]"null, ErrorManager.WRITE_FAILURE);
309                 }
310             } catch (Exception e) {
311                 reportError(null, e, ErrorManager.WRITE_FAILURE);
312             }
313         } finally {
314             writerLock.readLock().unlock();
315         }
316     }
317
318
319     // -------------------------------------------------------- Private Methods
320
321
322     /**
323      * Close the currently open log file (if any).
324      */

325     @Override
326     public void close() {
327         closeWriter();
328     }
329
330     protected void closeWriter() {
331
332         writerLock.writeLock().lock();
333         try {
334             if (writer == null) {
335                 return;
336             }
337             writer.write(getFormatter().getTail(this));
338             writer.flush();
339             writer.close();
340             writer = null;
341             date = "";
342         } catch (Exception e) {
343             reportError(null, e, ErrorManager.CLOSE_FAILURE);
344         } finally {
345             writerLock.writeLock().unlock();
346         }
347     }
348
349
350     /**
351      * Flush the writer.
352      */

353     @Override
354     public void flush() {
355
356         writerLock.readLock().lock();
357         try {
358             if (writer == null) {
359                 return;
360             }
361             writer.flush();
362         } catch (Exception e) {
363             reportError(null, e, ErrorManager.FLUSH_FAILURE);
364         } finally {
365             writerLock.readLock().unlock();
366         }
367
368     }
369
370
371     /**
372      * Configure from <code>LogManager</code> properties.
373      */

374     private void configure() {
375
376         Timestamp ts = new Timestamp(System.currentTimeMillis());
377         date = ts.toString().substring(0, 10);
378
379         String className = this.getClass().getName(); //allow classes to override
380
381         ClassLoader cl = Thread.currentThread().getContextClassLoader();
382
383         // Retrieve configuration of logging file name
384         if (rotatable == null) {
385             rotatable = Boolean.valueOf(getProperty(className + ".rotatable""true"));
386         }
387         if (directory == null) {
388             directory = getProperty(className + ".directory""logs");
389         }
390         if (prefix == null) {
391             prefix = getProperty(className + ".prefix""juli.");
392         }
393         if (suffix == null) {
394             suffix = getProperty(className + ".suffix"".log");
395         }
396
397         // https://bz.apache.org/bugzilla/show_bug.cgi?id=61232
398         boolean shouldCheckForRedundantSeparator =
399                 !rotatable.booleanValue() && !prefix.isEmpty() && !suffix.isEmpty();
400         // assuming separator is just one charif there are use cases with
401         // more, the notion of separator might be introduced
402         if (shouldCheckForRedundantSeparator &&
403                 (prefix.charAt(prefix.length() - 1) == suffix.charAt(0))) {
404             suffix = suffix.substring(1);
405         }
406
407         pattern = Pattern.compile("^(" + Pattern.quote(prefix) + ")\\d{4}-\\d{1,2}-\\d{1,2}("
408                 + Pattern.quote(suffix) + ")$");
409
410         if (maxDays == null) {
411             String sMaxDays = getProperty(className + ".maxDays", String.valueOf(DEFAULT_MAX_DAYS));
412             try {
413                 maxDays = Integer.valueOf(sMaxDays);
414             } catch (NumberFormatException ignore) {
415                 maxDays = Integer.valueOf(DEFAULT_MAX_DAYS);
416             }
417         }
418
419         if (bufferSize == null) {
420             String sBufferSize = getProperty(className + ".bufferSize",
421                     String.valueOf(DEFAULT_BUFFER_SIZE));
422             try {
423                 bufferSize = Integer.valueOf(sBufferSize);
424             } catch (NumberFormatException ignore) {
425                 bufferSize = Integer.valueOf(DEFAULT_BUFFER_SIZE);
426             }
427         }
428
429         // Get encoding for the logging file
430         String encoding = getProperty(className + ".encoding"null);
431         if (encoding != null && encoding.length() > 0) {
432             try {
433                 setEncoding(encoding);
434             } catch (UnsupportedEncodingException ex) {
435                 // Ignore
436             }
437         }
438
439         // Get logging level for the handler
440         setLevel(Level.parse(getProperty(className + ".level""" + Level.ALL)));
441
442         // Get filter configuration
443         String filterName = getProperty(className + ".filter"null);
444         if (filterName != null) {
445             try {
446                 setFilter((Filter) cl.loadClass(filterName).getConstructor().newInstance());
447             } catch (Exception e) {
448                 // Ignore
449             }
450         }
451
452         // Set formatter
453         String formatterName = getProperty(className + ".formatter"null);
454         if (formatterName != null) {
455             try {
456                 setFormatter((Formatter) cl.loadClass(
457                         formatterName).getConstructor().newInstance());
458             } catch (Exception e) {
459                 // Ignore and fallback to defaults
460                 setFormatter(new OneLineFormatter());
461             }
462         } else {
463             setFormatter(new OneLineFormatter());
464         }
465
466         // Set error manager
467         setErrorManager(new ErrorManager());
468     }
469
470
471     private String getProperty(String name, String defaultValue) {
472         String value = LogManager.getLogManager().getProperty(name);
473         if (value == null) {
474             value = defaultValue;
475         } else {
476             value = value.trim();
477         }
478         return value;
479     }
480
481
482     /**
483      * Open the new log file for the date specified by <code>date</code>.
484      */

485     protected void open() {
486         openWriter();
487     }
488
489     protected void openWriter() {
490
491         // Create the directory if necessary
492         File dir = new File(directory);
493         if (!dir.mkdirs() && !dir.isDirectory()) {
494             reportError("Unable to create [" + dir + "]"null, ErrorManager.OPEN_FAILURE);
495             writer = null;
496             return;
497         }
498
499         // Open the current log file
500         writerLock.writeLock().lock();
501         FileOutputStream fos = null;
502         OutputStream os = null;
503         try {
504             File pathname = new File(dir.getAbsoluteFile(), prefix
505                     + (rotatable.booleanValue() ? date : "") + suffix);
506             File parent = pathname.getParentFile();
507             if (!parent.mkdirs() && !parent.isDirectory()) {
508                 reportError("Unable to create [" + parent + "]"null, ErrorManager.OPEN_FAILURE);
509                 writer = null;
510                 return;
511             }
512             String encoding = getEncoding();
513             fos = new FileOutputStream(pathname, true);
514             os = bufferSize.intValue() > 0 ? new BufferedOutputStream(fos, bufferSize.intValue()) : fos;
515             writer = new PrintWriter(
516                     (encoding != null) ? new OutputStreamWriter(os, encoding)
517                                        : new OutputStreamWriter(os), false);
518             writer.write(getFormatter().getHead(this));
519         } catch (Exception e) {
520             reportError(null, e, ErrorManager.OPEN_FAILURE);
521             writer = null;
522             if (fos != null) {
523                 try {
524                     fos.close();
525                 } catch (IOException e1) {
526                     // Ignore
527                 }
528             }
529             if (os != null) {
530                 try {
531                     os.close();
532                 } catch (IOException e1) {
533                     // Ignore
534                 }
535             }
536         } finally {
537             writerLock.writeLock().unlock();
538         }
539     }
540
541     private void clean() {
542         if (maxDays.intValue() <= 0) {
543             return;
544         }
545         DELETE_FILES_SERVICE.submit(() -> {
546             try (DirectoryStream<Path> files = streamFilesForDelete()) {
547                 for (Path file : files) {
548                     Files.delete(file);
549                 }
550             } catch (IOException e) {
551                 reportError("Unable to delete log files older than [" + maxDays + "] days"null,
552                         ErrorManager.GENERIC_FAILURE);
553             }
554         });
555     }
556
557     private DirectoryStream<Path> streamFilesForDelete() throws IOException {
558         LocalDate maxDaysOffset = LocalDate.now().minus(maxDays.intValue(), ChronoUnit.DAYS);
559         return Files.newDirectoryStream(new File(directory).toPath(), path -> {
560             boolean result = false;
561             String date = obtainDateFromPath(path);
562             if (date != null) {
563                 try {
564                     LocalDate dateFromFile = LocalDate.from(DateTimeFormatter.ISO_LOCAL_DATE.parse(date));
565                     result = dateFromFile.isBefore(maxDaysOffset);
566                 } catch (DateTimeException e) {
567                     // no-op
568                 }
569             }
570             return result;
571         });
572     }
573
574     private String obtainDateFromPath(Path path) {
575         Path fileName = path.getFileName();
576         if (fileName == null) {
577             return null;
578         }
579         String date = fileName.toString();
580         if (pattern.matcher(date).matches()) {
581             date = date.substring(prefix.length());
582             return date.substring(0, date.length() - suffix.length());
583         } else {
584             return null;
585         }
586     }
587 }
588