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