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 package org.apache.catalina.valves;
18
19
20 import java.io.BufferedWriter;
21 import java.io.CharArrayWriter;
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStreamWriter;
26 import java.io.PrintWriter;
27 import java.io.UnsupportedEncodingException;
28 import java.nio.charset.Charset;
29 import java.nio.charset.StandardCharsets;
30 import java.text.SimpleDateFormat;
31 import java.util.Date;
32 import java.util.Locale;
33 import java.util.TimeZone;
34
35 import org.apache.catalina.LifecycleException;
36 import org.apache.juli.logging.Log;
37 import org.apache.juli.logging.LogFactory;
38 import org.apache.tomcat.util.ExceptionUtils;
39 import org.apache.tomcat.util.buf.B2CConverter;
40
41
42 /**
43  * This is a concrete implementation of {@link AbstractAccessLogValve} that
44  * outputs the access log to a file. The features of this implementation
45  * include:
46  * <ul>
47  * <li>Automatic date-based rollover of log files</li>
48  * <li>Optional log file rotation</li>
49  * </ul>
50  * <p>
51  * For UNIX users, another field called <code>checkExists</code> is also
52  * available. If set to true, the log file's existence will be checked before
53  * each logging. This way an external log rotator can move the file
54  * somewhere and Tomcat will start with a new file.
55  * </p>
56  *
57  * <p>
58  * For JMX junkies, a public method called <code>rotate</code> has
59  * been made available to allow you to tell this instance to move
60  * the existing log file to somewhere else and start writing a new log file.
61  * </p>
62  */

63 public class AccessLogValve extends AbstractAccessLogValve {
64
65     private static final Log log = LogFactory.getLog(AccessLogValve.class);
66
67     //------------------------------------------------------ Constructor
68     public AccessLogValve() {
69         super();
70     }
71
72     // ----------------------------------------------------- Instance Variables
73
74
75     /**
76      * The as-of date for the currently open log file, or a zero-length
77      * string if there is no open log file.
78      */

79     private volatile String dateStamp = "";
80
81
82     /**
83      * The directory in which log files are created.
84      */

85     private String directory = "logs";
86
87     /**
88      * The prefix that is added to log file filenames.
89      */

90     protected volatile String prefix = "access_log";
91
92
93     /**
94      * Should we rotate our log file? Default is true (like old behavior)
95      */

96     protected boolean rotatable = true;
97
98     /**
99      * Should we defer inclusion of the date stamp in the file
100      * name until rotate time? Default is false.
101      */

102     protected boolean renameOnRotate = false;
103
104
105     /**
106      * Buffered logging.
107      */

108     private boolean buffered = true;
109
110
111     /**
112      * The suffix that is added to log file filenames.
113      */

114     protected volatile String suffix = "";
115
116
117     /**
118      * The PrintWriter to which we are currently logging, if any.
119      */

120     protected PrintWriter writer = null;
121
122
123     /**
124      * A date formatter to format a Date using the format
125      * given by <code>fileDateFormat</code>.
126      */

127     protected SimpleDateFormat fileDateFormatter = null;
128
129
130     /**
131      * The current log file we are writing to. Helpful when checkExists
132      * is true.
133      */

134     protected File currentLogFile = null;
135
136     /**
137      * Instant when the log daily rotation was last checked.
138      */

139     private volatile long rotationLastChecked = 0L;
140
141     /**
142      * Do we check for log file existence? Helpful if an external
143      * agent renames the log file so we can automagically recreate it.
144      */

145     private boolean checkExists = false;
146
147     /**
148      * Date format to place in log file name.
149      */

150     protected String fileDateFormat = ".yyyy-MM-dd";
151
152     /**
153      * Character set used by the log file. If it is <code>null</code>, the
154      * system default character set will be used. An empty string will be
155      * treated as <code>null</code> when this property is assigned.
156      */

157     protected volatile String encoding = null;
158
159     /**
160      * The number of days to retain the access log files before they are
161      * removed.
162      */

163     private int maxDays = -1;
164     private volatile boolean checkForOldLogs = false;
165
166     // ------------------------------------------------------------- Properties
167
168
169     public int getMaxDays() {
170         return maxDays;
171     }
172
173
174     public void setMaxDays(int maxDays) {
175         this.maxDays = maxDays;
176     }
177
178
179     /**
180      * @return the directory in which we create log files.
181      */

182     public String getDirectory() {
183         return directory;
184     }
185
186
187     /**
188      * Set the directory in which we create log files.
189      *
190      * @param directory The new log file directory
191      */

192     public void setDirectory(String directory) {
193         this.directory = directory;
194     }
195
196     /**
197      * Check for file existence before logging.
198      * @return <code>true</code> if file existence is checked first
199      */

200     public boolean isCheckExists() {
201
202         return checkExists;
203
204     }
205
206
207     /**
208      * Set whether to check for log file existence before logging.
209      *
210      * @param checkExists true meaning to check for file existence.
211      */

212     public void setCheckExists(boolean checkExists) {
213
214         this.checkExists = checkExists;
215
216     }
217
218
219     /**
220      * @return the log file prefix.
221      */

222     public String getPrefix() {
223         return prefix;
224     }
225
226
227     /**
228      * Set the log file prefix.
229      *
230      * @param prefix The new log file prefix
231      */

232     public void setPrefix(String prefix) {
233         this.prefix = prefix;
234     }
235
236
237     /**
238      * Should we rotate the access log.
239      *
240      * @return <code>true</code> if the access log should be rotated
241      */

242     public boolean isRotatable() {
243         return rotatable;
244     }
245
246
247     /**
248      * Configure whether the access log should be rotated.
249      *
250      * @param rotatable true if the log should be rotated
251      */

252     public void setRotatable(boolean rotatable) {
253         this.rotatable = rotatable;
254     }
255
256
257     /**
258      * Should we defer inclusion of the date stamp in the file
259      * name until rotate time.
260      * @return <code>true</code> if the logs file names are time stamped
261      *  only when they are rotated
262      */

263     public boolean isRenameOnRotate() {
264         return renameOnRotate;
265     }
266
267
268     /**
269      * Set the value if we should defer inclusion of the date
270      * stamp in the file name until rotate time
271      *
272      * @param renameOnRotate true if defer inclusion of date stamp
273      */

274     public void setRenameOnRotate(boolean renameOnRotate) {
275         this.renameOnRotate = renameOnRotate;
276     }
277
278
279     /**
280      * Is the logging buffered. Usually buffering can increase performance.
281      * @return <code>true</code> if the logging uses a buffer
282      */

283     public boolean isBuffered() {
284         return buffered;
285     }
286
287
288     /**
289      * Set the value if the logging should be buffered
290      *
291      * @param buffered <code>true</code> if buffered.
292      */

293     public void setBuffered(boolean buffered) {
294         this.buffered = buffered;
295     }
296
297
298     /**
299      * @return the log file suffix.
300      */

301     public String getSuffix() {
302         return suffix;
303     }
304
305
306     /**
307      * Set the log file suffix.
308      *
309      * @param suffix The new log file suffix
310      */

311     public void setSuffix(String suffix) {
312         this.suffix = suffix;
313     }
314
315     /**
316      * @return the date format date based log rotation.
317      */

318     public String getFileDateFormat() {
319         return fileDateFormat;
320     }
321
322
323     /**
324      * Set the date format date based log rotation.
325      * @param fileDateFormat The format for the file timestamp
326      */

327     public void setFileDateFormat(String fileDateFormat) {
328         String newFormat;
329         if (fileDateFormat == null) {
330             newFormat = "";
331         } else {
332             newFormat = fileDateFormat;
333         }
334         this.fileDateFormat = newFormat;
335
336         synchronized (this) {
337             fileDateFormatter = new SimpleDateFormat(newFormat, Locale.US);
338             fileDateFormatter.setTimeZone(TimeZone.getDefault());
339         }
340     }
341
342     /**
343      * Return the character set name that is used to write the log file.
344      *
345      * @return Character set name, or <code>null</code> if the system default
346      *  character set is used.
347      */

348     public String getEncoding() {
349         return encoding;
350     }
351
352     /**
353      * Set the character set that is used to write the log file.
354      *
355      * @param encoding The name of the character set.
356      */

357     public void setEncoding(String encoding) {
358         if (encoding != null && encoding.length() > 0) {
359             this.encoding = encoding;
360         } else {
361             this.encoding = null;
362         }
363     }
364
365     // --------------------------------------------------------- Public Methods
366
367     /**
368      * Execute a periodic task, such as reloading, etc. This method will be
369      * invoked inside the classloading context of this container. Unexpected
370      * throwables will be caught and logged.
371      */

372     @Override
373     public synchronized void backgroundProcess() {
374         if (getState().isAvailable() && getEnabled() && writer != null &&
375                 buffered) {
376             writer.flush();
377         }
378
379         int maxDays = this.maxDays;
380         String prefix = this.prefix;
381         String suffix = this.suffix;
382
383         if (rotatable && checkForOldLogs && maxDays > 0) {
384             long deleteIfLastModifiedBefore =
385                     System.currentTimeMillis() - (maxDays * 24L * 60 * 60 * 1000);
386             File dir = getDirectoryFile();
387             if (dir.isDirectory()) {
388                 String[] oldAccessLogs = dir.list();
389
390                 if (oldAccessLogs != null) {
391                     for (String oldAccessLog : oldAccessLogs) {
392                         boolean match = false;
393
394                         if (prefix != null && prefix.length() > 0) {
395                             if (!oldAccessLog.startsWith(prefix)) {
396                                 continue;
397                             }
398                             match = true;
399                         }
400
401                         if (suffix != null && suffix.length() > 0) {
402                             if (!oldAccessLog.endsWith(suffix)) {
403                                 continue;
404                             }
405                             match = true;
406                         }
407
408                         if (match) {
409                             File file = new File(dir, oldAccessLog);
410                             if (file.isFile() && file.lastModified() < deleteIfLastModifiedBefore) {
411                                 if (!file.delete()) {
412                                     log.warn(sm.getString(
413                                             "accessLogValve.deleteFail", file.getAbsolutePath()));
414                                 }
415                             }
416                         }
417                     }
418                 }
419             }
420             checkForOldLogs = false;
421         }
422     }
423
424     /**
425      * Rotate the log file if necessary.
426      */

427     public void rotate() {
428         if (rotatable) {
429             // Only do a logfile switch check once a second, max.
430             long systime = System.currentTimeMillis();
431             if ((systime - rotationLastChecked) > 1000) {
432                 synchronized(this) {
433                     if ((systime - rotationLastChecked) > 1000) {
434                         rotationLastChecked = systime;
435
436                         String tsDate;
437                         // Check for a change of date
438                         tsDate = fileDateFormatter.format(new Date(systime));
439
440                         // If the date has changed, switch log files
441                         if (!dateStamp.equals(tsDate)) {
442                             close(true);
443                             dateStamp = tsDate;
444                             open();
445                         }
446                     }
447                 }
448             }
449         }
450     }
451
452     /**
453      * Rename the existing log file to something else. Then open the
454      * old log file name up once again. Intended to be called by a JMX
455      * agent.
456      *
457      * @param newFileName The file name to move the log file entry to
458      * @return true if a file was rotated with no error
459      */

460     public synchronized boolean rotate(String newFileName) {
461
462         if (currentLogFile != null) {
463             File holder = currentLogFile;
464             close(false);
465             try {
466                 holder.renameTo(new File(newFileName));
467             } catch (Throwable e) {
468                 ExceptionUtils.handleThrowable(e);
469                 log.error(sm.getString("accessLogValve.rotateFail"), e);
470             }
471
472             /* Make sure date is correct */
473             dateStamp = fileDateFormatter.format(
474                     new Date(System.currentTimeMillis()));
475
476             open();
477             return true;
478         } else {
479             return false;
480         }
481
482     }
483
484     // -------------------------------------------------------- Private Methods
485
486
487     private File getDirectoryFile() {
488         File dir = new File(directory);
489         if (!dir.isAbsolute()) {
490             dir = new File(getContainer().getCatalinaBase(), directory);
491         }
492         return dir;
493     }
494
495
496     /**
497      * Create a File object based on the current log file name.
498      * Directories are created as needed but the underlying file
499      * is not created or opened.
500      *
501      * @param useDateStamp include the timestamp in the file name.
502      * @return the log file object
503      */

504     private File getLogFile(boolean useDateStamp) {
505         // Create the directory if necessary
506         File dir = getDirectoryFile();
507         if (!dir.mkdirs() && !dir.isDirectory()) {
508             log.error(sm.getString("accessLogValve.openDirFail", dir));
509         }
510
511         // Calculate the current log file name
512         File pathname;
513         if (useDateStamp) {
514             pathname = new File(dir.getAbsoluteFile(), prefix + dateStamp
515                     + suffix);
516         } else {
517             pathname = new File(dir.getAbsoluteFile(), prefix + suffix);
518         }
519         File parent = pathname.getParentFile();
520         if (!parent.mkdirs() && !parent.isDirectory()) {
521             log.error(sm.getString("accessLogValve.openDirFail", parent));
522         }
523         return pathname;
524     }
525
526     /**
527      * Move a current but rotated log file back to the unrotated
528      * one. Needed if date stamp inclusion is deferred to rotation
529      * time.
530      */

531     private void restore() {
532         File newLogFile = getLogFile(false);
533         File rotatedLogFile = getLogFile(true);
534         if (rotatedLogFile.exists() && !newLogFile.exists() &&
535             !rotatedLogFile.equals(newLogFile)) {
536             try {
537                 if (!rotatedLogFile.renameTo(newLogFile)) {
538                     log.error(sm.getString("accessLogValve.renameFail", rotatedLogFile, newLogFile));
539                 }
540             } catch (Throwable e) {
541                 ExceptionUtils.handleThrowable(e);
542                 log.error(sm.getString("accessLogValve.renameFail", rotatedLogFile, newLogFile), e);
543             }
544         }
545     }
546
547
548     /**
549      * Close the currently open log file (if any)
550      *
551      * @param rename Rename file to final name after closing
552      */

553     private synchronized void close(boolean rename) {
554         if (writer == null) {
555             return;
556         }
557         writer.flush();
558         writer.close();
559         if (rename && renameOnRotate) {
560             File newLogFile = getLogFile(true);
561             if (!newLogFile.exists()) {
562                 try {
563                     if (!currentLogFile.renameTo(newLogFile)) {
564                         log.error(sm.getString("accessLogValve.renameFail", currentLogFile, newLogFile));
565                     }
566                 } catch (Throwable e) {
567                     ExceptionUtils.handleThrowable(e);
568                     log.error(sm.getString("accessLogValve.renameFail", currentLogFile, newLogFile), e);
569                 }
570             } else {
571                 log.error(sm.getString("accessLogValve.alreadyExists", currentLogFile, newLogFile));
572             }
573         }
574         writer = null;
575         dateStamp = "";
576         currentLogFile = null;
577     }
578
579
580     /**
581      * Log the specified message to the log file, switching files if the date
582      * has changed since the previous log call.
583      *
584      * @param message Message to be logged
585      */

586     @Override
587     public void log(CharArrayWriter message) {
588
589         rotate();
590
591         /* In case something external rotated the file instead */
592         if (checkExists) {
593             synchronized (this) {
594                 if (currentLogFile != null && !currentLogFile.exists()) {
595                     try {
596                         close(false);
597                     } catch (Throwable e) {
598                         ExceptionUtils.handleThrowable(e);
599                         log.info(sm.getString("accessLogValve.closeFail"), e);
600                     }
601
602                     /* Make sure date is correct */
603                     dateStamp = fileDateFormatter.format(
604                             new Date(System.currentTimeMillis()));
605
606                     open();
607                 }
608             }
609         }
610
611         // Log this message
612         try {
613             message.write(System.lineSeparator());
614             synchronized(this) {
615                 if (writer != null) {
616                     message.writeTo(writer);
617                     if (!buffered) {
618                         writer.flush();
619                     }
620                 }
621             }
622         } catch (IOException ioe) {
623             log.warn(sm.getString(
624                     "accessLogValve.writeFail", message.toString()), ioe);
625         }
626     }
627
628
629     /**
630      * Open the new log file for the date specified by <code>dateStamp</code>.
631      */

632     protected synchronized void open() {
633         // Open the current log file
634         // If no rotate - no need for dateStamp in fileName
635         File pathname = getLogFile(rotatable && !renameOnRotate);
636
637         Charset charset = null;
638         if (encoding != null) {
639             try {
640                 charset = B2CConverter.getCharset(encoding);
641             } catch (UnsupportedEncodingException ex) {
642                 log.error(sm.getString(
643                         "accessLogValve.unsupportedEncoding", encoding), ex);
644             }
645         }
646         if (charset == null) {
647             charset = StandardCharsets.ISO_8859_1;
648         }
649
650         try {
651             writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
652                     new FileOutputStream(pathname, true), charset), 128000),
653                     false);
654
655             currentLogFile = pathname;
656         } catch (IOException e) {
657             writer = null;
658             currentLogFile = null;
659             log.error(sm.getString("accessLogValve.openFail", pathname), e);
660         }
661         // Rotating a log file will always trigger a new file to be opened so
662         // when a new file is opened, check to see if any old files need to be
663         // removed.
664         checkForOldLogs = true;
665     }
666
667     /**
668      * Start this component and implement the requirements
669      * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
670      *
671      * @exception LifecycleException if this component detects a fatal error
672      *  that prevents this component from being used
673      */

674     @Override
675     protected synchronized void startInternal() throws LifecycleException {
676
677         // Initialize the Date formatters
678         String format = getFileDateFormat();
679         fileDateFormatter = new SimpleDateFormat(format, Locale.US);
680         fileDateFormatter.setTimeZone(TimeZone.getDefault());
681         dateStamp = fileDateFormatter.format(new Date(System.currentTimeMillis()));
682         if (rotatable && renameOnRotate) {
683             restore();
684         }
685         open();
686
687         super.startInternal();
688     }
689
690
691     /**
692      * Stop this component and implement the requirements
693      * of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
694      *
695      * @exception LifecycleException if this component detects a fatal error
696      *  that prevents this component from being used
697      */

698     @Override
699     protected synchronized void stopInternal() throws LifecycleException {
700
701         super.stopInternal();
702         close(false);
703     }
704 }
705