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.catalina.valves;
20
21
22 import java.io.BufferedWriter;
23 import java.io.File;
24 import java.io.FileWriter;
25 import java.io.IOException;
26 import java.io.PrintWriter;
27 import java.net.InetAddress;
28 import java.text.SimpleDateFormat;
29 import java.util.ArrayList;
30 import java.util.Date;
31 import java.util.List;
32 import java.util.TimeZone;
33
34 import javax.servlet.ServletException;
35 import javax.servlet.http.Cookie;
36 import javax.servlet.http.HttpSession;
37
38 import org.apache.catalina.Lifecycle;
39 import org.apache.catalina.LifecycleException;
40 import org.apache.catalina.LifecycleListener;
41 import org.apache.catalina.connector.Request;
42 import org.apache.catalina.connector.Response;
43 import org.apache.catalina.util.LifecycleSupport;
44 import org.apache.catalina.util.StringManager;
45 import org.apache.coyote.RequestInfo;
46 import org.apache.juli.logging.Log;
47 import org.apache.juli.logging.LogFactory;
48
49
50 /**
51 * <p>Implementation of the <b>Valve</b> interface that generates a web server
52 * access log with the detailed line contents matching a configurable pattern.
53 * The syntax of the available patterns is similar to that supported by the
54 * Apache <code>mod_log_config</code> module. As an additional feature,
55 * automatic rollover of log files when the date changes is also supported.</p>
56 *
57 * <p>Patterns for the logged message may include constant text or any of the
58 * following replacement strings, for which the corresponding information
59 * from the specified Response is substituted:</p>
60 * <ul>
61 * <li><b>%a</b> - Remote IP address
62 * <li><b>%A</b> - Local IP address
63 * <li><b>%b</b> - Bytes sent, excluding HTTP headers, or '-' if no bytes
64 * were sent
65 * <li><b>%B</b> - Bytes sent, excluding HTTP headers
66 * <li><b>%h</b> - Remote host name
67 * <li><b>%H</b> - Request protocol
68 * <li><b>%l</b> - Remote logical username from identd (always returns '-')
69 * <li><b>%m</b> - Request method
70 * <li><b>%p</b> - Local port
71 * <li><b>%q</b> - Query string (prepended with a '?' if it exists, otherwise
72 * an empty string
73 * <li><b>%r</b> - First line of the request
74 * <li><b>%s</b> - HTTP status code of the response
75 * <li><b>%S</b> - User session ID
76 * <li><b>%t</b> - Date and time, in Common Log Format format
77 * <li><b>%u</b> - Remote user that was authenticated
78 * <li><b>%U</b> - Requested URL path
79 * <li><b>%v</b> - Local server name
80 * <li><b>%D</b> - Time taken to process the request, in millis
81 * <li><b>%T</b> - Time taken to process the request, in seconds
82 * <li><b>%I</b> - current Request thread name (can compare later with stacktraces)
83 * </ul>
84 * <p>In addition, the caller can specify one of the following aliases for
85 * commonly utilized patterns:</p>
86 * <ul>
87 * <li><b>common</b> - <code>%h %l %u %t "%r" %s %b</code>
88 * <li><b>combined</b> -
89 * <code>%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"</code>
90 * </ul>
91 *
92 * <p>
93 * There is also support to write information from the cookie, incoming
94 * header, the Session or something else in the ServletRequest.<br>
95 * It is modeled after the apache syntax:
96 * <ul>
97 * <li><code>%{xxx}i</code> for incoming headers
98 * <li><code>%{xxx}o</code> for outgoing response headers
99 * <li><code>%{xxx}c</code> for a specific cookie
100 * <li><code>%{xxx}r</code> xxx is an attribute in the ServletRequest
101 * <li><code>%{xxx}s</code> xxx is an attribute in the HttpSession
102 * </ul>
103 * </p>
104 *
105 * <p>
106 * Conditional logging is also supported. This can be done with the
107 * <code>condition</code> property.
108 * If the value returned from ServletRequest.getAttribute(condition)
109 * yields a non-null value. The logging will be skipped.
110 * </p>
111 *
112 * @author Craig R. McClanahan
113 * @author Jason Brittain
114 * @author Remy Maucherat
115 * @author Takayuki Kaneko
116 * @author Peter Rossbach
117 *
118 * @version $Revision: 832123 $ $Date: 2009-11-02 22:57:21 +0100 (Mon, 02 Nov 2009) $
119 */
120
121 public class AccessLogValve
122 extends ValveBase
123 implements Lifecycle {
124
125 private static Log log = LogFactory.getLog(AccessLogValve.class);
126
127 // ----------------------------------------------------- Instance Variables
128
129
130 /**
131 * The as-of date for the currently open log file, or a zero-length
132 * string if there is no open log file.
133 */
134 private volatile String dateStamp = "";
135
136
137 /**
138 * The directory in which log files are created.
139 */
140 private String directory = "logs";
141
142
143 /**
144 * The descriptive information about this implementation.
145 */
146 protected static final String info =
147 "org.apache.catalina.valves.AccessLogValve/2.1";
148
149
150 /**
151 * The lifecycle event support for this component.
152 */
153 protected LifecycleSupport lifecycle = new LifecycleSupport(this);
154
155
156 /**
157 * The set of month abbreviations for log messages.
158 */
159 protected static final String months[] =
160 { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
161 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
162
163
164 /**
165 * enabled this component
166 */
167 protected boolean enabled = true;
168
169 /**
170 * The pattern used to format our access log lines.
171 */
172 protected String pattern = null;
173
174
175 /**
176 * The prefix that is added to log file filenames.
177 */
178 protected String prefix = "access_log.";
179
180
181 /**
182 * Should we rotate our log file? Default is true (like old behavior)
183 */
184 protected boolean rotatable = true;
185
186
187 /**
188 * Buffered logging.
189 */
190 private boolean buffered = true;
191
192
193 /**
194 * The string manager for this package.
195 */
196 protected StringManager sm =
197 StringManager.getManager(Constants.Package);
198
199
200 /**
201 * Has this component been started yet?
202 */
203 protected boolean started = false;
204
205
206 /**
207 * The suffix that is added to log file filenames.
208 */
209 protected String suffix = "";
210
211
212 /**
213 * The PrintWriter to which we are currently logging, if any.
214 */
215 protected PrintWriter writer = null;
216
217
218 /**
219 * A date formatter to format a Date into a date in the format
220 * "yyyy-MM-dd".
221 */
222 protected SimpleDateFormat fileDateFormatter = null;
223
224
225 /**
226 * The system timezone.
227 */
228 private TimeZone timezone = null;
229
230
231 /**
232 * The time zone offset relative to GMT in text form when daylight saving
233 * is not in operation.
234 */
235 private String timeZoneNoDST = null;
236
237
238 /**
239 * The time zone offset relative to GMT in text form when daylight saving
240 * is in operation.
241 */
242 private String timeZoneDST = null;
243
244
245 /**
246 * The current log file we are writing to. Helpful when checkExists
247 * is true.
248 */
249 protected File currentLogFile = null;
250 private static class AccessDateStruct {
251 private Date currentDate = new Date();
252 private String currentDateString = null;
253 private SimpleDateFormat dayFormatter = new SimpleDateFormat("dd");
254 private SimpleDateFormat monthFormatter = new SimpleDateFormat("MM");
255 private SimpleDateFormat yearFormatter = new SimpleDateFormat("yyyy");
256 private SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm:ss");
257 public AccessDateStruct() {
258 TimeZone tz = TimeZone.getDefault();
259 dayFormatter.setTimeZone(tz);
260 monthFormatter.setTimeZone(tz);
261 yearFormatter.setTimeZone(tz);
262 timeFormatter.setTimeZone(tz);
263 }
264 }
265
266 /**
267 * The system time when we last updated the Date that this valve
268 * uses for log lines.
269 */
270 private static final ThreadLocal<AccessDateStruct> currentDateStruct =
271 new ThreadLocal<AccessDateStruct>() {
272 protected AccessDateStruct initialValue() {
273 return new AccessDateStruct();
274 }
275 };
276 /**
277 * Resolve hosts.
278 */
279 private boolean resolveHosts = false;
280
281
282 /**
283 * Instant when the log daily rotation was last checked.
284 */
285 private volatile long rotationLastChecked = 0L;
286
287 /**
288 * Do we check for log file existence? Helpful if an external
289 * agent renames the log file so we can automagically recreate it.
290 */
291 private boolean checkExists = false;
292
293
294 /**
295 * Are we doing conditional logging. default false.
296 */
297 protected String condition = null;
298
299
300 /**
301 * Date format to place in log file name. Use at your own risk!
302 */
303 protected String fileDateFormat = null;
304
305 /**
306 * Array of AccessLogElement, they will be used to make log message.
307 */
308 protected AccessLogElement[] logElements = null;
309
310 // ------------------------------------------------------------- Properties
311
312 /**
313 * @return Returns the enabled.
314 */
315 public boolean getEnabled() {
316 return enabled;
317 }
318
319 /**
320 * @param enabled
321 * The enabled to set.
322 */
323 public void setEnabled(boolean enabled) {
324 this.enabled = enabled;
325 }
326
327 /**
328 * Return the directory in which we create log files.
329 */
330 public String getDirectory() {
331 return (directory);
332 }
333
334
335 /**
336 * Set the directory in which we create log files.
337 *
338 * @param directory The new log file directory
339 */
340 public void setDirectory(String directory) {
341 this.directory = directory;
342 }
343
344
345 /**
346 * Return descriptive information about this implementation.
347 */
348 public String getInfo() {
349 return (info);
350 }
351
352
353 /**
354 * Return the format pattern.
355 */
356 public String getPattern() {
357 return (this.pattern);
358 }
359
360
361 /**
362 * Set the format pattern, first translating any recognized alias.
363 *
364 * @param pattern The new pattern
365 */
366 public void setPattern(String pattern) {
367 if (pattern == null)
368 pattern = "";
369 if (pattern.equals(Constants.AccessLog.COMMON_ALIAS))
370 pattern = Constants.AccessLog.COMMON_PATTERN;
371 if (pattern.equals(Constants.AccessLog.COMBINED_ALIAS))
372 pattern = Constants.AccessLog.COMBINED_PATTERN;
373 this.pattern = pattern;
374 logElements = createLogElements();
375 }
376
377
378 /**
379 * Check for file existence before logging.
380 */
381 public boolean isCheckExists() {
382
383 return checkExists;
384
385 }
386
387
388 /**
389 * Set whether to check for log file existence before logging.
390 *
391 * @param checkExists true meaning to check for file existence.
392 */
393 public void setCheckExists(boolean checkExists) {
394
395 this.checkExists = checkExists;
396
397 }
398
399
400 /**
401 * Return the log file prefix.
402 */
403 public String getPrefix() {
404 return (prefix);
405 }
406
407
408 /**
409 * Set the log file prefix.
410 *
411 * @param prefix The new log file prefix
412 */
413 public void setPrefix(String prefix) {
414 this.prefix = prefix;
415 }
416
417
418 /**
419 * Should we rotate the logs
420 */
421 public boolean isRotatable() {
422 return rotatable;
423 }
424
425
426 /**
427 * Set the value is we should we rotate the logs
428 *
429 * @param rotatable true is we should rotate.
430 */
431 public void setRotatable(boolean rotatable) {
432 this.rotatable = rotatable;
433 }
434
435
436 /**
437 * Is the logging buffered
438 */
439 public boolean isBuffered() {
440 return buffered;
441 }
442
443
444 /**
445 * Set the value if the logging should be buffered
446 *
447 * @param buffered true if buffered.
448 */
449 public void setBuffered(boolean buffered) {
450 this.buffered = buffered;
451 }
452
453
454 /**
455 * Return the log file suffix.
456 */
457 public String getSuffix() {
458 return (suffix);
459 }
460
461
462 /**
463 * Set the log file suffix.
464 *
465 * @param suffix The new log file suffix
466 */
467 public void setSuffix(String suffix) {
468 this.suffix = suffix;
469 }
470
471
472 /**
473 * Set the resolve hosts flag.
474 *
475 * @param resolveHosts The new resolve hosts value
476 */
477 public void setResolveHosts(boolean resolveHosts) {
478 this.resolveHosts = resolveHosts;
479 }
480
481
482 /**
483 * Get the value of the resolve hosts flag.
484 */
485 public boolean isResolveHosts() {
486 return resolveHosts;
487 }
488
489
490 /**
491 * Return whether the attribute name to look for when
492 * performing conditional loggging. If null, every
493 * request is logged.
494 */
495 public String getCondition() {
496 return condition;
497 }
498
499
500 /**
501 * Set the ServletRequest.attribute to look for to perform
502 * conditional logging. Set to null to log everything.
503 *
504 * @param condition Set to null to log everything
505 */
506 public void setCondition(String condition) {
507 this.condition = condition;
508 }
509
510 /**
511 * Return the date format date based log rotation.
512 */
513 public String getFileDateFormat() {
514 return fileDateFormat;
515 }
516
517
518 /**
519 * Set the date format date based log rotation.
520 */
521 public void setFileDateFormat(String fileDateFormat) {
522 this.fileDateFormat = fileDateFormat;
523 }
524
525 // --------------------------------------------------------- Public Methods
526
527 /**
528 * Execute a periodic task, such as reloading, etc. This method will be
529 * invoked inside the classloading context of this container. Unexpected
530 * throwables will be caught and logged.
531 */
532 public void backgroundProcess() {
533 if (started && getEnabled() && writer != null && buffered) {
534 writer.flush();
535 }
536 }
537
538 /**
539 * Log a message summarizing the specified request and response, according
540 * to the format specified by the <code>pattern</code> property.
541 *
542 * @param request Request being processed
543 * @param response Response being processed
544 *
545 * @exception IOException if an input/output error has occurred
546 * @exception ServletException if a servlet error has occurred
547 */
548 public void invoke(Request request, Response response) throws IOException,
549 ServletException {
550
551 if (started && getEnabled()) {
552 // Pass this request on to the next valve in our pipeline
553 long t1 = System.currentTimeMillis();
554
555 getNext().invoke(request, response);
556
557 long t2 = System.currentTimeMillis();
558 long time = t2 - t1;
559
560 if (logElements == null || condition != null
561 && null != request.getRequest().getAttribute(condition)) {
562 return;
563 }
564
565 Date date = getDate();
566 StringBuffer result = new StringBuffer(128);
567
568 for (int i = 0; i < logElements.length; i++) {
569 logElements[i].addElement(result, date, request, response, time);
570 }
571
572 log(result.toString());
573 } else
574 getNext().invoke(request, response);
575 }
576
577
578 /**
579 * Rename the existing log file to something else. Then open the
580 * old log file name up once again. Intended to be called by a JMX
581 * agent.
582 *
583 *
584 * @param newFileName The file name to move the log file entry to
585 * @return true if a file was rotated with no error
586 */
587 public synchronized boolean rotate(String newFileName) {
588
589 if (currentLogFile != null) {
590 File holder = currentLogFile;
591 close();
592 try {
593 holder.renameTo(new File(newFileName));
594 } catch (Throwable e) {
595 log.error("rotate failed", e);
596 }
597
598 /* Make sure date is correct */
599 dateStamp = fileDateFormatter.format(
600 new Date(System.currentTimeMillis()));
601
602 open();
603 return true;
604 } else {
605 return false;
606 }
607
608 }
609
610 // -------------------------------------------------------- Private Methods
611
612
613 /**
614 * Close the currently open log file (if any)
615 */
616 private synchronized void close() {
617 if (writer == null) {
618 return;
619 }
620 writer.flush();
621 writer.close();
622 writer = null;
623 dateStamp = "";
624 currentLogFile = null;
625 }
626
627
628 /**
629 * Log the specified message to the log file, switching files if the date
630 * has changed since the previous log call.
631 *
632 * @param message Message to be logged
633 */
634 public void log(String message) {
635 if (rotatable) {
636 // Only do a logfile switch check once a second, max.
637 long systime = System.currentTimeMillis();
638 if ((systime - rotationLastChecked) > 1000) {
639 synchronized(this) {
640 if ((systime - rotationLastChecked) > 1000) {
641 rotationLastChecked = systime;
642
643 String tsDate;
644 // Check for a change of date
645 tsDate = fileDateFormatter.format(new Date(systime));
646
647 // If the date has changed, switch log files
648 if (!dateStamp.equals(tsDate)) {
649 close();
650 dateStamp = tsDate;
651 open();
652 }
653 }
654 }
655 }
656 }
657
658 /* In case something external rotated the file instead */
659 if (checkExists) {
660 synchronized (this) {
661 if (currentLogFile != null && !currentLogFile.exists()) {
662 try {
663 close();
664 } catch (Throwable e) {
665 log.info("at least this wasn't swallowed", e);
666 }
667
668 /* Make sure date is correct */
669 dateStamp = fileDateFormatter.format(
670 new Date(System.currentTimeMillis()));
671
672 open();
673 }
674 }
675 }
676
677 // Log this message
678 synchronized(this) {
679 if (writer != null) {
680 writer.println(message);
681 if (!buffered) {
682 writer.flush();
683 }
684 }
685 }
686
687 }
688
689
690 /**
691 * Return the month abbreviation for the specified month, which must
692 * be a two-digit String.
693 *
694 * @param month Month number ("01" .. "12").
695 */
696 private String lookup(String month) {
697 int index;
698 try {
699 index = Integer.parseInt(month) - 1;
700 } catch (Throwable t) {
701 index = 0; // Can not happen, in theory
702 }
703 return (months[index]);
704 }
705
706
707 /**
708 * Open the new log file for the date specified by <code>dateStamp</code>.
709 */
710 protected synchronized void open() {
711 // Create the directory if necessary
712 File dir = new File(directory);
713 if (!dir.isAbsolute())
714 dir = new File(System.getProperty("catalina.base"), directory);
715 dir.mkdirs();
716
717 // Open the current log file
718 try {
719 String pathname;
720 // If no rotate - no need for dateStamp in fileName
721 if (rotatable) {
722 pathname = dir.getAbsolutePath() + File.separator + prefix
723 + dateStamp + suffix;
724 } else {
725 pathname = dir.getAbsolutePath() + File.separator + prefix
726 + suffix;
727 }
728 writer = new PrintWriter(new BufferedWriter(new FileWriter(
729 pathname, true), 128000), false);
730
731 currentLogFile = new File(pathname);
732 } catch (IOException e) {
733 writer = null;
734 currentLogFile = null;
735 }
736 }
737
738 /**
739 * This method returns a Date object that is accurate to within one second.
740 * If a thread calls this method to get a Date and it's been less than 1
741 * second since a new Date was created, this method simply gives out the
742 * same Date again so that the system doesn't spend time creating Date
743 * objects unnecessarily.
744 *
745 * @return Date
746 */
747 private Date getDate() {
748 // Only create a new Date once per second, max.
749 long systime = System.currentTimeMillis();
750 AccessDateStruct struct = currentDateStruct.get();
751 if ((systime - struct.currentDate.getTime()) > 1000) {
752 struct.currentDate.setTime(systime);
753 struct.currentDateString = null;
754 }
755 return struct.currentDate;
756 }
757
758
759 private String getTimeZone(Date date) {
760 if (timezone.inDaylightTime(date)) {
761 return timeZoneDST;
762 } else {
763 return timeZoneNoDST;
764 }
765 }
766
767
768 private String calculateTimeZoneOffset(long offset) {
769 StringBuffer tz = new StringBuffer();
770 if ((offset < 0)) {
771 tz.append("-");
772 offset = -offset;
773 } else {
774 tz.append("+");
775 }
776
777 long hourOffset = offset / (1000 * 60 * 60);
778 long minuteOffset = (offset / (1000 * 60)) % 60;
779
780 if (hourOffset < 10)
781 tz.append("0");
782 tz.append(hourOffset);
783
784 if (minuteOffset < 10)
785 tz.append("0");
786 tz.append(minuteOffset);
787
788 return tz.toString();
789 }
790
791
792 // ------------------------------------------------------ Lifecycle Methods
793
794
795 /**
796 * Add a lifecycle event listener to this component.
797 *
798 * @param listener The listener to add
799 */
800 public void addLifecycleListener(LifecycleListener listener) {
801 lifecycle.addLifecycleListener(listener);
802 }
803
804
805 /**
806 * Get the lifecycle listeners associated with this lifecycle. If this
807 * Lifecycle has no listeners registered, a zero-length array is returned.
808 */
809 public LifecycleListener[] findLifecycleListeners() {
810 return lifecycle.findLifecycleListeners();
811 }
812
813
814 /**
815 * Remove a lifecycle event listener from this component.
816 *
817 * @param listener The listener to add
818 */
819 public void removeLifecycleListener(LifecycleListener listener) {
820 lifecycle.removeLifecycleListener(listener);
821 }
822
823
824 /**
825 * Prepare for the beginning of active use of the public methods of this
826 * component. This method should be called after <code>configure()</code>,
827 * and before any of the public methods of the component are utilized.
828 *
829 * @exception LifecycleException if this component detects a fatal error
830 * that prevents this component from being used
831 */
832 public void start() throws LifecycleException {
833
834 // Validate and update our current component state
835 if (started)
836 throw new LifecycleException(sm
837 .getString("accessLogValve.alreadyStarted"));
838 lifecycle.fireLifecycleEvent(START_EVENT, null);
839 started = true;
840
841 // Initialize the timeZone, Date formatters, and currentDate
842 timezone = TimeZone.getDefault();
843 timeZoneNoDST = calculateTimeZoneOffset(timezone.getRawOffset());
844 int offset = timezone.getDSTSavings();
845 timeZoneDST = calculateTimeZoneOffset(timezone.getRawOffset() + offset);
846
847 if (fileDateFormat == null || fileDateFormat.length() == 0)
848 fileDateFormat = "yyyy-MM-dd";
849 fileDateFormatter = new SimpleDateFormat(fileDateFormat);
850 fileDateFormatter.setTimeZone(timezone);
851 dateStamp = fileDateFormatter.format(currentDateStruct.get().currentDate);
852 open();
853 }
854
855
856 /**
857 * Gracefully terminate the active use of the public methods of this
858 * component. This method should be the last one called on a given
859 * instance of this component.
860 *
861 * @exception LifecycleException if this component detects a fatal error
862 * that needs to be reported
863 */
864 public void stop() throws LifecycleException {
865
866 // Validate and update our current component state
867 if (!started)
868 throw new LifecycleException(sm
869 .getString("accessLogValve.notStarted"));
870 lifecycle.fireLifecycleEvent(STOP_EVENT, null);
871 started = false;
872
873 close();
874 }
875
876 /**
877 * AccessLogElement writes the partial message into the buffer.
878 */
879 protected interface AccessLogElement {
880 public void addElement(StringBuffer buf, Date date, Request request,
881 Response response, long time);
882
883 }
884
885 /**
886 * write thread name - %I
887 */
888 protected class ThreadNameElement implements AccessLogElement {
889 public void addElement(StringBuffer buf, Date date, Request request,
890 Response response, long time) {
891 RequestInfo info = request.getCoyoteRequest().getRequestProcessor();
892 if(info != null) {
893 buf.append(info.getWorkerThreadName());
894 } else {
895 buf.append("-");
896 }
897 }
898 }
899
900 /**
901 * write local IP address - %A
902 */
903 protected static class LocalAddrElement implements AccessLogElement {
904
905 private static final String LOCAL_ADDR_VALUE;
906
907 static {
908 String init;
909 try {
910 init = InetAddress.getLocalHost().getHostAddress();
911 } catch (Throwable e) {
912 init = "127.0.0.1";
913 }
914 LOCAL_ADDR_VALUE = init;
915 }
916
917 public void addElement(StringBuffer buf, Date date, Request request,
918 Response response, long time) {
919 buf.append(LOCAL_ADDR_VALUE);
920 }
921 }
922
923 /**
924 * write remote IP address - %a
925 */
926 protected class RemoteAddrElement implements AccessLogElement {
927 public void addElement(StringBuffer buf, Date date, Request request,
928 Response response, long time) {
929 buf.append(request.getRemoteAddr());
930 }
931 }
932
933 /**
934 * write remote host name - %h
935 */
936 protected class HostElement implements AccessLogElement {
937 public void addElement(StringBuffer buf, Date date, Request request,
938 Response response, long time) {
939 buf.append(request.getRemoteHost());
940 }
941 }
942
943 /**
944 * write remote logical username from identd (always returns '-') - %l
945 */
946 protected class LogicalUserNameElement implements AccessLogElement {
947 public void addElement(StringBuffer buf, Date date, Request request,
948 Response response, long time) {
949 buf.append('-');
950 }
951 }
952
953 /**
954 * write request protocol - %H
955 */
956 protected class ProtocolElement implements AccessLogElement {
957 public void addElement(StringBuffer buf, Date date, Request request,
958 Response response, long time) {
959 buf.append(request.getProtocol());
960 }
961 }
962
963 /**
964 * write remote user that was authenticated (if any), else '-' - %u
965 */
966 protected class UserElement implements AccessLogElement {
967 public void addElement(StringBuffer buf, Date date, Request request,
968 Response response, long time) {
969 if (request != null) {
970 String value = request.getRemoteUser();
971 if (value != null) {
972 buf.append(value);
973 } else {
974 buf.append('-');
975 }
976 } else {
977 buf.append('-');
978 }
979 }
980 }
981
982 /**
983 * write date and time, in Common Log Format - %t
984 */
985 protected class DateAndTimeElement implements AccessLogElement {
986
987
988
989
990 public void addElement(StringBuffer buf, Date date, Request request,
991 Response response, long time) {
992 AccessDateStruct struct = currentDateStruct.get();
993 if (struct.currentDateString == null) {
994 StringBuffer current = new StringBuffer(32);
995 current.append('[');
996 current.append(struct.dayFormatter.format(date));
997 current.append('/');
998 current.append(lookup(struct.monthFormatter.format(date)));
999 current.append('/');
1000 current.append(struct.yearFormatter.format(date));
1001 current.append(':');
1002 current.append(struct.timeFormatter.format(date));
1003 current.append(' ');
1004 current.append(getTimeZone(date));
1005 current.append(']');
1006 struct.currentDateString = current.toString();
1007 }
1008 buf.append(struct.currentDateString);
1009 }
1010 }
1011
1012 /**
1013 * write first line of the request (method and request URI) - %r
1014 */
1015 protected class RequestElement implements AccessLogElement {
1016 public void addElement(StringBuffer buf, Date date, Request request,
1017 Response response, long time) {
1018 if (request != null) {
1019 buf.append(request.getMethod());
1020 buf.append(' ');
1021 buf.append(request.getRequestURI());
1022 if (request.getQueryString() != null) {
1023 buf.append('?');
1024 buf.append(request.getQueryString());
1025 }
1026 buf.append(' ');
1027 buf.append(request.getProtocol());
1028 } else {
1029 buf.append("- - ");
1030 }
1031 }
1032 }
1033
1034 /**
1035 * write HTTP status code of the response - %s
1036 */
1037 protected class HttpStatusCodeElement implements AccessLogElement {
1038 public void addElement(StringBuffer buf, Date date, Request request,
1039 Response response, long time) {
1040 if (response != null) {
1041 buf.append(response.getStatus());
1042 } else {
1043 buf.append('-');
1044 }
1045 }
1046 }
1047
1048 /**
1049 * write local port on which this request was received - %p
1050 */
1051 protected class LocalPortElement implements AccessLogElement {
1052 public void addElement(StringBuffer buf, Date date, Request request,
1053 Response response, long time) {
1054 buf.append(request.getServerPort());
1055 }
1056 }
1057
1058 /**
1059 * write bytes sent, excluding HTTP headers - %b, %B
1060 */
1061 protected class ByteSentElement implements AccessLogElement {
1062 private boolean conversion;
1063
1064 /**
1065 * if conversion is true, write '-' instead of 0 - %b
1066 */
1067 public ByteSentElement(boolean conversion) {
1068 this.conversion = conversion;
1069 }
1070
1071 public void addElement(StringBuffer buf, Date date, Request request,
1072 Response response, long time) {
1073 long length = response.getContentCountLong() ;
1074 if (length <= 0 && conversion) {
1075 buf.append('-');
1076 } else {
1077 buf.append(length);
1078 }
1079 }
1080 }
1081
1082 /**
1083 * write request method (GET, POST, etc.) - %m
1084 */
1085 protected class MethodElement implements AccessLogElement {
1086 public void addElement(StringBuffer buf, Date date, Request request,
1087 Response response, long time) {
1088 if (request != null) {
1089 buf.append(request.getMethod());
1090 }
1091 }
1092 }
1093
1094 /**
1095 * write time taken to process the request - %D, %T
1096 */
1097 protected class ElapsedTimeElement implements AccessLogElement {
1098 private boolean millis;
1099
1100 /**
1101 * if millis is true, write time in millis - %D
1102 * if millis is false, write time in seconds - %T
1103 */
1104 public ElapsedTimeElement(boolean millis) {
1105 this.millis = millis;
1106 }
1107
1108 public void addElement(StringBuffer buf, Date date, Request request,
1109 Response response, long time) {
1110 if (millis) {
1111 buf.append(time);
1112 } else {
1113 // second
1114 buf.append(time / 1000);
1115 buf.append('.');
1116 int remains = (int) (time % 1000);
1117 buf.append(remains / 100);
1118 remains = remains % 100;
1119 buf.append(remains / 10);
1120 buf.append(remains % 10);
1121 }
1122 }
1123 }
1124
1125 /**
1126 * write Query string (prepended with a '?' if it exists) - %q
1127 */
1128 protected class QueryElement implements AccessLogElement {
1129 public void addElement(StringBuffer buf, Date date, Request request,
1130 Response response, long time) {
1131 String query = null;
1132 if (request != null)
1133 query = request.getQueryString();
1134 if (query != null) {
1135 buf.append('?');
1136 buf.append(query);
1137 }
1138 }
1139 }
1140
1141 /**
1142 * write user session ID - %S
1143 */
1144 protected class SessionIdElement implements AccessLogElement {
1145 public void addElement(StringBuffer buf, Date date, Request request,
1146 Response response, long time) {
1147 if (request != null) {
1148 if (request.getSession(false) != null) {
1149 buf.append(request.getSessionInternal(false)
1150 .getIdInternal());
1151 } else {
1152 buf.append('-');
1153 }
1154 } else {
1155 buf.append('-');
1156 }
1157 }
1158 }
1159
1160 /**
1161 * write requested URL path - %U
1162 */
1163 protected class RequestURIElement implements AccessLogElement {
1164 public void addElement(StringBuffer buf, Date date, Request request,
1165 Response response, long time) {
1166 if (request != null) {
1167 buf.append(request.getRequestURI());
1168 } else {
1169 buf.append('-');
1170 }
1171 }
1172 }
1173
1174 /**
1175 * write local server name - %v
1176 */
1177 protected class LocalServerNameElement implements AccessLogElement {
1178 public void addElement(StringBuffer buf, Date date, Request request,
1179 Response response, long time) {
1180 buf.append(request.getServerName());
1181 }
1182 }
1183
1184 /**
1185 * write any string
1186 */
1187 protected class StringElement implements AccessLogElement {
1188 private String str;
1189
1190 public StringElement(String str) {
1191 this.str = str;
1192 }
1193
1194 public void addElement(StringBuffer buf, Date date, Request request,
1195 Response response, long time) {
1196 buf.append(str);
1197 }
1198 }
1199
1200 /**
1201 * write incoming headers - %{xxx}i
1202 */
1203 protected class HeaderElement implements AccessLogElement {
1204 private String header;
1205
1206 public HeaderElement(String header) {
1207 this.header = header;
1208 }
1209
1210 public void addElement(StringBuffer buf, Date date, Request request,
1211 Response response, long time) {
1212 String value = request.getHeader(header);
1213 if (value == null) {
1214 buf.append('-');
1215 } else {
1216 buf.append(value);
1217 }
1218 }
1219 }
1220
1221 /**
1222 * write a specific cookie - %{xxx}c
1223 */
1224 protected class CookieElement implements AccessLogElement {
1225 private String header;
1226
1227 public CookieElement(String header) {
1228 this.header = header;
1229 }
1230
1231 public void addElement(StringBuffer buf, Date date, Request request,
1232 Response response, long time) {
1233 String value = "-";
1234 Cookie[] c = request.getCookies();
1235 if (c != null) {
1236 for (int i = 0; i < c.length; i++) {
1237 if (header.equals(c[i].getName())) {
1238 value = c[i].getValue();
1239 break;
1240 }
1241 }
1242 }
1243 buf.append(value);
1244 }
1245 }
1246
1247 /**
1248 * write a specific response header - %{xxx}o
1249 */
1250 protected class ResponseHeaderElement implements AccessLogElement {
1251 private String header;
1252
1253 public ResponseHeaderElement(String header) {
1254 this.header = header;
1255 }
1256
1257 public void addElement(StringBuffer buf, Date date, Request request,
1258 Response response, long time) {
1259 if (null != response) {
1260 String[] values = response.getHeaderValues(header);
1261 if(values.length > 0) {
1262 for (int i = 0; i < values.length; i++) {
1263 String string = values[i];
1264 buf.append(string) ;
1265 if(i+1<values.length)
1266 buf.append(",");
1267 }
1268 return ;
1269 }
1270 }
1271 buf.append("-");
1272 }
1273 }
1274
1275 /**
1276 * write an attribute in the ServletRequest - %{xxx}r
1277 */
1278 protected class RequestAttributeElement implements AccessLogElement {
1279 private String header;
1280
1281 public RequestAttributeElement(String header) {
1282 this.header = header;
1283 }
1284
1285 public void addElement(StringBuffer buf, Date date, Request request,
1286 Response response, long time) {
1287 Object value = null;
1288 if (request != null) {
1289 value = request.getAttribute(header);
1290 } else {
1291 value = "??";
1292 }
1293 if (value != null) {
1294 if (value instanceof String) {
1295 buf.append((String) value);
1296 } else {
1297 buf.append(value.toString());
1298 }
1299 } else {
1300 buf.append('-');
1301 }
1302 }
1303 }
1304
1305 /**
1306 * write an attribute in the HttpSession - %{xxx}s
1307 */
1308 protected class SessionAttributeElement implements AccessLogElement {
1309 private String header;
1310
1311 public SessionAttributeElement(String header) {
1312 this.header = header;
1313 }
1314
1315 public void addElement(StringBuffer buf, Date date, Request request,
1316 Response response, long time) {
1317 Object value = null;
1318 if (null != request) {
1319 HttpSession sess = request.getSession(false);
1320 if (null != sess)
1321 value = sess.getAttribute(header);
1322 } else {
1323 value = "??";
1324 }
1325 if (value != null) {
1326 if (value instanceof String) {
1327 buf.append((String) value);
1328 } else {
1329 buf.append(value.toString());
1330 }
1331 } else {
1332 buf.append('-');
1333 }
1334 }
1335 }
1336
1337
1338
1339
1340 /**
1341 * parse pattern string and create the array of AccessLogElement
1342 */
1343 protected AccessLogElement[] createLogElements() {
1344 List<AccessLogElement> list = new ArrayList<AccessLogElement>();
1345 boolean replace = false;
1346 StringBuffer buf = new StringBuffer();
1347 for (int i = 0; i < pattern.length(); i++) {
1348 char ch = pattern.charAt(i);
1349 if (replace) {
1350 /*
1351 * For code that processes {, the behavior will be ... if I do
1352 * not enounter a closing } - then I ignore the {
1353 */
1354 if ('{' == ch) {
1355 StringBuffer name = new StringBuffer();
1356 int j = i + 1;
1357 for (; j < pattern.length() && '}' != pattern.charAt(j); j++) {
1358 name.append(pattern.charAt(j));
1359 }
1360 if (j + 1 < pattern.length()) {
1361 /* the +1 was to account for } which we increment now */
1362 j++;
1363 list.add(createAccessLogElement(name.toString(),
1364 pattern.charAt(j)));
1365 i = j; /* Since we walked more than one character */
1366 } else {
1367 // D'oh - end of string - pretend we never did this
1368 // and do processing the "old way"
1369 list.add(createAccessLogElement(ch));
1370 }
1371 } else {
1372 list.add(createAccessLogElement(ch));
1373 }
1374 replace = false;
1375 } else if (ch == '%') {
1376 replace = true;
1377 list.add(new StringElement(buf.toString()));
1378 buf = new StringBuffer();
1379 } else {
1380 buf.append(ch);
1381 }
1382 }
1383 if (buf.length() > 0) {
1384 list.add(new StringElement(buf.toString()));
1385 }
1386 return list.toArray(new AccessLogElement[0]);
1387 }
1388
1389 /**
1390 * create an AccessLogElement implementation which needs header string
1391 */
1392 private AccessLogElement createAccessLogElement(String header, char pattern) {
1393 switch (pattern) {
1394 case 'i':
1395 return new HeaderElement(header);
1396 case 'c':
1397 return new CookieElement(header);
1398 case 'o':
1399 return new ResponseHeaderElement(header);
1400 case 'r':
1401 return new RequestAttributeElement(header);
1402 case 's':
1403 return new SessionAttributeElement(header);
1404 default:
1405 return new StringElement("???");
1406 }
1407 }
1408
1409 /**
1410 * create an AccessLogElement implementation
1411 */
1412 private AccessLogElement createAccessLogElement(char pattern) {
1413 switch (pattern) {
1414 case 'a':
1415 return new RemoteAddrElement();
1416 case 'A':
1417 return new LocalAddrElement();
1418 case 'b':
1419 return new ByteSentElement(true);
1420 case 'B':
1421 return new ByteSentElement(false);
1422 case 'D':
1423 return new ElapsedTimeElement(true);
1424 case 'h':
1425 return new HostElement();
1426 case 'H':
1427 return new ProtocolElement();
1428 case 'l':
1429 return new LogicalUserNameElement();
1430 case 'm':
1431 return new MethodElement();
1432 case 'p':
1433 return new LocalPortElement();
1434 case 'q':
1435 return new QueryElement();
1436 case 'r':
1437 return new RequestElement();
1438 case 's':
1439 return new HttpStatusCodeElement();
1440 case 'S':
1441 return new SessionIdElement();
1442 case 't':
1443 return new DateAndTimeElement();
1444 case 'T':
1445 return new ElapsedTimeElement(false);
1446 case 'u':
1447 return new UserElement();
1448 case 'U':
1449 return new RequestURIElement();
1450 case 'v':
1451 return new LocalServerNameElement();
1452 case 'I':
1453 return new ThreadNameElement();
1454 default:
1455 return new StringElement("???" + pattern + "???");
1456 }
1457 }
1458 }