001: // CommonLogger.java
002: // $Id: CommonLogger.java,v 1.50 2002/09/18 15:22:12 ylafon Exp $
003: // (c) COPYRIGHT MIT and INRIA, 1996.
004: // Please first read the full copyright statement in file COPYRIGHT.html
005:
006: package org.w3c.jigsaw.http;
007:
008: import java.io.File;
009: import java.io.IOException;
010: import java.io.RandomAccessFile;
011:
012: import java.net.URL;
013:
014: import java.util.Calendar;
015: import java.util.Date;
016: import java.util.TimeZone;
017:
018: import org.w3c.jigsaw.auth.AuthFilter;
019:
020: import org.w3c.util.ObservableProperties;
021: import org.w3c.util.PropertyMonitoring;
022:
023: /**
024: * The CommonLogger class implements the abstract Logger class.
025: * The resulting log will conform to the
026: * <a href="http://www.w3.org/Daemon/User/Config/Logging.html#common-logfile-format">common log format</a>).
027: * @see org.w3c.jigsaw.http.Logger
028: */
029:
030: public class CommonLogger extends Logger implements PropertyMonitoring {
031: protected static final String monthnames[] = { "Jan", "Feb", "Mar",
032: "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
033: "Dec" };
034: protected static String noUrl = "*NOURL*";
035:
036: /**
037: * Name of the property indicating the log file.
038: * This property indicates the name of the log file to use.
039: * <p>This property defaults to the <code>log</code> file in the server
040: * log directory.
041: */
042: public static final String LOGNAME_P = "org.w3c.jigsaw.logger.logname";
043: /**
044: * Name of the property indicating the error log file.
045: * This property indicates the name of the error log file to use.
046: * <p>This property defaults to the <code>errlog</code> file in the
047: * server log directory.
048: */
049: public static final String ERRLOGNAME_P = "org.w3c.jigsaw.logger.errlogname";
050: /**
051: * Name of the property indicating the server trace file.
052: * This property indicates the name of the trace file to use.
053: * <p>This property defaults to the <code>trace</code> file in the
054: * server log directory.
055: */
056: public static final String LOGDIRNAME_P = "org.w3c.jigsaw.logger.logdirname";
057: /**
058: * Name of the property indicating the server log directory.
059: * <p>This property defaults to the <code>logs</code> directory in the
060: * server main directory.
061: */
062: public static final String TRACELOGNAME_P = "org.w3c.jigsaw.logger.tracelogname";
063: /**
064: * Name of the property indicating the buffer size for the logger.
065: * This buffer size applies only the the log file, not to the error
066: * log file, or the trace log file. It can be set to zero if you want
067: * no buffering.
068: * <p>This property default to <strong>4096</strong>.
069: */
070: public static final String BUFSIZE_P = "org.w3c.jigsaw.logger.bufferSize";
071: /**
072: * Name of the property indicating the buffer size for the logger.
073: * This buffer size applies only the the log file, not to the error
074: * log file, or the trace log file. It can be set to zero if you want
075: * no buffering.
076: * <p>This property default to <strong>4096</strong>.
077: */
078: public static final String ROTATE_LEVEL_P = "org.w3c.jigsaw.logger.rotateLevel";
079:
080: private byte msgbuf[] = null;
081: protected RandomAccessFile log = null;
082: protected RandomAccessFile errlog = null;
083: protected RandomAccessFile trace = null;
084: protected httpd server = null;
085: protected ObservableProperties props = null;
086: protected int bufsize = 8192;
087: protected int bufptr = 0;
088: protected int rotateLevel = 0;
089: protected byte buffer[] = null;
090: protected int year = -1;
091: protected int month = -1;
092: protected int day = -1;
093: protected int hour = -1;
094: private Calendar cal = null;
095: private long datestamp = -1;
096: private char datecache[] = { 'D', 'D', '/', 'M', 'M', 'M', '/',
097: 'Y', 'Y', 'Y', 'Y', ':', 'H', 'H', ':', 'M', 'M', ':', 'S',
098: 'S', ' ', '+', '0', '0', '0', '0' };
099:
100: /**
101: * Property monitoring for the logger.
102: * The logger allows you to dynamically (typically through the property
103: * setter) change the names of the file to which it logs error, access
104: * and traces.
105: * @param name The name of the property that has changed.
106: * @return A boolean, <strong>true</strong> if the change was made,
107: * <strong>false</strong> otherwise.
108: */
109:
110: public boolean propertyChanged(String name) {
111: if (name.equals(LOGNAME_P)) {
112: try {
113: openLogFile();
114: } catch (Exception e) {
115: e.printStackTrace();
116: return false;
117: }
118: return true;
119: } else if (name.equals(ERRLOGNAME_P)) {
120: try {
121: openErrorLogFile();
122: } catch (Exception e) {
123: e.printStackTrace();
124: return false;
125: }
126: return true;
127: } else if (name.equals(TRACELOGNAME_P)) {
128: try {
129: openTraceFile();
130: } catch (Exception e) {
131: e.printStackTrace();
132: return false;
133: }
134: return true;
135: } else if (name.equals(LOGDIRNAME_P)) {
136: try {
137: openLogFile();
138: openErrorLogFile();
139: openTraceFile();
140: } catch (Exception e) {
141: e.printStackTrace();
142: return false;
143: }
144: return true;
145: } else if (name.equals(BUFSIZE_P)) {
146: synchronized (this ) {
147: bufsize = props.getInteger(name, bufsize);
148: // Reset buffer before resizing:
149: if (bufptr > 0) {
150: try {
151: log.write(buffer, 0, bufptr);
152: bufptr = 0;
153: } catch (IOException ex) {
154: }
155: }
156: // Set new buffer:
157: buffer = (bufsize > 0) ? new byte[bufsize] : null;
158: return true;
159: }
160: } else if (name.equals(ROTATE_LEVEL_P)) {
161: int newLevel = props.getInteger(name, rotateLevel);
162: if (newLevel != rotateLevel) {
163: synchronized (this ) {
164: sync();
165: rotateLevel = newLevel;
166: openLogFile();
167: }
168: }
169: return true;
170: } else {
171: return true;
172: }
173: }
174:
175: /**
176: * Output the given message to the given RandomAccessFile.
177: * This method makes its best effort to avoid one byte writes (which you
178: * get when writing the string as a whole). It first copies the string
179: * bytes into a private byte array, and than, write them all at once.
180: * @param f The RandomAccessFile to write to, which should be one of
181: * log, errlog or trace.
182: * @param msg The message to be written.
183: * @exception IOException If writing to the output failed.
184: */
185:
186: protected synchronized void output(RandomAccessFile f, String msg)
187: throws IOException {
188: int len = msg.length();
189: if (len > msgbuf.length)
190: msgbuf = new byte[len];
191: msg.getBytes(0, len, msgbuf, 0);
192: f.write(msgbuf, 0, len);
193: }
194:
195: protected synchronized void appendLogBuffer(String msg)
196: throws IOException {
197: int msglen = msg.length();
198: if (bufptr + msglen > buffer.length) {
199: // Flush the buffer:
200: log.write(buffer, 0, bufptr);
201: bufptr = 0;
202: // Check for messages greater then buffer:
203: if (msglen > buffer.length) {
204: byte huge[] = new byte[msglen];
205: msg.getBytes(0, msglen, huge, 0);
206: log.write(huge, 0, msglen);
207: return;
208: }
209: }
210: // Append that message to buffer:
211: msg.getBytes(0, msglen, buffer, bufptr);
212: bufptr += msglen;
213: }
214:
215: protected void logmsg(String msg) {
216: if (log != null) {
217: try {
218: if (buffer == null) {
219: output(log, msg);
220: } else {
221: appendLogBuffer(msg);
222: }
223: } catch (IOException e) {
224: throw new HTTPRuntimeException(this , "logmsg", e
225: .getMessage());
226: }
227: }
228: }
229:
230: protected void errlogmsg(String msg) {
231: if (errlog != null) {
232: try {
233: output(errlog, msg);
234: } catch (IOException e) {
235: throw new HTTPRuntimeException(this , "errlogmsg", e
236: .getMessage());
237: }
238: }
239: }
240:
241: protected void tracemsg(String msg) {
242: if (trace != null) {
243: try {
244: output(trace, msg);
245: } catch (IOException e) {
246: throw new HTTPRuntimeException(this , "tracemsg", e
247: .getMessage());
248: }
249: }
250: }
251:
252: protected synchronized void checkLogFile(Date now) {
253: if (cal == null) {
254: TimeZone tz = TimeZone.getTimeZone("UTC");
255: cal = Calendar.getInstance(tz);
256: }
257: cal.setTime(now);
258: int nowYear = cal.get(Calendar.YEAR);
259: if (rotateLevel == 1) {
260: // rotate every year
261: if (nowYear != year) {
262: if (log != null) {
263: sync();
264: }
265: this .year = nowYear;
266: openLogFile(year);
267: }
268: } else {
269: int nowMonth = cal.get(Calendar.MONTH);
270: if (rotateLevel == 2) {
271: // rotate every month
272: if ((nowYear != year) || (nowMonth != month)) {
273: if (log != null) {
274: sync();
275: }
276: this .year = nowYear;
277: this .month = nowMonth;
278: openLogFile(year, month);
279: }
280: } else {
281: int nowDay = cal.get(Calendar.DAY_OF_MONTH);
282: if (rotateLevel == 3) {
283: // rotate every day
284: if ((nowYear != year) || (nowMonth != month)
285: || (nowDay != day)) {
286: if (log != null) {
287: sync();
288: }
289: this .year = nowYear;
290: this .month = nowMonth;
291: this .day = nowDay;
292: openLogFile(year, month, day);
293: }
294: }
295: }
296: }
297: }
298:
299: protected void openLogFile(int year, int month, int day) {
300: this .year = year;
301: this .month = month;
302: this .day = day;
303:
304: String ext = null;
305: if (month < 9) {
306: if (day < 10) {
307: ext = "_" + year + "_0" + (month + 1) + "_0" + day;
308: } else {
309: ext = "_" + year + "_0" + (month + 1) + "_" + day;
310: }
311: } else {
312: if (day < 10) {
313: ext = "_" + year + "_" + (month + 1) + "_0" + day;
314: } else {
315: ext = "_" + year + "_" + (month + 1) + "_" + day;
316: }
317: }
318: String logname = getFilename(LOGNAME_P, "log") + ext;
319: try {
320: RandomAccessFile old = log;
321: log = new RandomAccessFile(logname, "rw");
322: log.seek(log.length());
323: if (old != null)
324: old.close();
325: } catch (IOException e) {
326: throw new HTTPRuntimeException(this .getClass().getName(),
327: "openLogFile", "unable to open " + logname);
328: }
329: }
330:
331: protected void openLogFile(int year, int month) {
332: this .year = year;
333: this .month = month;
334:
335: String ext = null;
336: if (month < 9)
337: ext = "_" + year + "_0" + (month + 1);
338: else
339: ext = "_" + year + "_" + (month + 1);
340:
341: String logname = getFilename(LOGNAME_P, "log") + ext;
342: try {
343: RandomAccessFile old = log;
344: log = new RandomAccessFile(logname, "rw");
345: log.seek(log.length());
346: if (old != null)
347: old.close();
348: } catch (IOException e) {
349: throw new HTTPRuntimeException(this .getClass().getName(),
350: "openLogFile", "unable to open " + logname);
351: }
352: }
353:
354: protected void openLogFile(int year) {
355: this .year = year;
356:
357: String logname = getFilename(LOGNAME_P, "log") + "_" + year;
358: try {
359: RandomAccessFile old = log;
360: log = new RandomAccessFile(logname, "rw");
361: log.seek(log.length());
362: if (old != null)
363: old.close();
364: } catch (IOException e) {
365: throw new HTTPRuntimeException(this .getClass().getName(),
366: "openLogFile", "unable to open " + logname);
367: }
368: }
369:
370: /**
371: * It actually does multiple things, check when to rotate log files
372: * and also dumps the formatted date string to a stringbuffer
373: * it is dirty but hopefully faster than the previous version of the logger
374: */
375: protected synchronized void dateCache(long date, StringBuffer sb) {
376: if (cal == null) {
377: TimeZone tz = TimeZone.getTimeZone("UTC");
378: cal = Calendar.getInstance(tz);
379: }
380: long ldate;
381: // should we use the request date or just log the
382: // end of the request?
383: if (date < 0) {
384: ldate = System.currentTimeMillis();
385: } else {
386: ldate = date;
387: }
388: Date now = new Date(ldate);
389: cal.setTime(now);
390: if ((ldate > datestamp + 3600000) || (datestamp == -1)) {
391: datestamp = ldate % 3600000;
392: if (hour == -1) {
393: hour = cal.get(Calendar.HOUR_OF_DAY);
394: } else {
395: int nhour = cal.get(Calendar.HOUR_OF_DAY);
396: if (nhour != hour) {
397: hour = nhour;
398: TimeZone tz = TimeZone.getTimeZone("UTC");
399: cal = Calendar.getInstance(tz);
400: cal.setTime(now);
401: }
402: }
403: if (rotateLevel > 0) {
404: checkLogFile(now);
405: }
406: int day = cal.get(Calendar.DAY_OF_MONTH);
407:
408: if (day < 10) {
409: datecache[0] = '0';
410: datecache[1] = (char) ('0' + day);
411: } else {
412: datecache[0] = (char) ('0' + day / 10);
413: datecache[1] = (char) ('0' + day % 10);
414: }
415: monthnames[cal.get(Calendar.MONTH)].getChars(0, 3,
416: datecache, 3);
417: int year = cal.get(Calendar.YEAR);
418: datecache[10] = (char) ('0' + year % 10);
419: year = year / 10;
420: datecache[9] = (char) ('0' + year % 10);
421: year = year / 10;
422: datecache[8] = (char) ('0' + year % 10);
423: year = year / 10;
424: datecache[7] = (char) ('0' + year);
425: if (hour < 10) {
426: datecache[12] = '0';
427: datecache[13] = (char) ('0' + hour);
428: } else {
429: datecache[12] = (char) ('0' + hour / 10);
430: datecache[13] = (char) ('0' + hour % 10);
431: }
432: }
433: int minutes = cal.get(Calendar.MINUTE);
434: if (minutes < 10) {
435: datecache[15] = '0';
436: datecache[16] = (char) ('0' + minutes);
437: } else {
438: datecache[15] = (char) ('0' + minutes / 10);
439: datecache[16] = (char) ('0' + minutes % 10);
440: }
441: int seconds = cal.get(Calendar.SECOND);
442: if (seconds < 10) {
443: datecache[18] = '0';
444: datecache[19] = (char) ('0' + seconds);
445: } else {
446: datecache[18] = (char) ('0' + seconds / 10);
447: datecache[19] = (char) ('0' + seconds % 10);
448: }
449: sb.append(datecache);
450: }
451:
452: /**
453: * Log the given HTTP transaction.
454: * This is shamelessly slow.
455: */
456: public void log(Request request, Reply reply, int nbytes,
457: long duration) {
458: Client client = request.getClient();
459: long date = reply.getDate();
460:
461: String user = (String) request
462: .getState(AuthFilter.STATE_AUTHUSER);
463: URL urlst = (URL) request.getState(Request.ORIG_URL_STATE);
464: String requrl;
465: if (urlst == null) {
466: URL u = request.getURL();
467: if (u == null) {
468: requrl = noUrl;
469: } else {
470: requrl = u.toExternalForm();
471: }
472: } else {
473: requrl = urlst.toExternalForm();
474: }
475: StringBuffer sb = new StringBuffer(512);
476: String logs;
477: int status = reply.getStatus();
478: if ((status > 999) || (status < 0)) {
479: status = 999; // means unknown
480: }
481: synchronized (sb) {
482: byte ib[] = client.getInetAddress().getAddress();
483: if (ib.length == 4) {
484: boolean doit;
485: for (int i = 0; i < 4; i++) {
486: doit = false;
487: int b = ib[i];
488: if (b < 0) {
489: b += 256;
490: }
491: if (b > 99) {
492: sb.append((char) ('0' + (b / 100)));
493: b = b % 100;
494: doit = true;
495: }
496: if (doit || (b > 9)) {
497: sb.append((char) ('0' + (b / 10)));
498: b = b % 10;
499: }
500: sb.append((char) ('0' + b));
501: if (i < 3) {
502: sb.append('.');
503: }
504: }
505: } else { // ipv6, let's be safe :)
506: sb.append(client.getInetAddress().getHostAddress());
507: }
508: sb.append(" - ");
509: if (user == null) {
510: sb.append("- [");
511: } else {
512: sb.append(user);
513: sb.append(" [");
514: }
515: dateCache(date, sb);
516: sb.append("] \"");
517: sb.append(request.getMethod());
518: sb.append(' ');
519: sb.append(requrl);
520: sb.append(' ');
521: sb.append(request.getVersion());
522: sb.append("\" ");
523: sb.append((char) ('0' + status / 100));
524: status = status % 100;
525: sb.append((char) ('0' + status / 10));
526: status = status % 10;
527: sb.append((char) ('0' + status));
528: sb.append(' ');
529: if (nbytes < 0) {
530: sb.append('-');
531: } else {
532: sb.append(nbytes);
533: }
534: sb.append('\n');
535: logs = sb.toString();
536: }
537: logmsg(logs);
538: }
539:
540: public void log(String msg) {
541: logmsg(msg);
542: }
543:
544: public void errlog(Client client, String msg) {
545: errlogmsg(client + ": " + msg + "\n");
546: }
547:
548: public void errlog(String msg) {
549: errlogmsg(msg + "\n");
550: }
551:
552: public void trace(Client client, String msg) {
553: tracemsg(client + ": " + msg + "\n");
554: }
555:
556: public void trace(String msg) {
557: tracemsg(msg + "\n");
558: }
559:
560: /**
561: * Get the name for the file indicated by the provided property.
562: * This method first looks for a property value. If none is found, it
563: * than constructs a default filename from the server root, by
564: * using the provided default name.
565: * <p>This method shall either succeed in getting a filename, or throw
566: * a runtime exception.
567: * @param propname The name of the property.
568: * @param def The default file name to use.
569: * @exception HTTPRuntimeException If no file name could be deduced from
570: * the provided set of properties.
571: */
572:
573: protected String getFilename(String propname, String def) {
574: String filename = props.getString(propname, null);
575: File flogdir = null;
576: if (filename == null) {
577: String logdirname = props.getString(LOGDIRNAME_P, null);
578: if (logdirname == null) {
579: File root_dir = server.getRootDirectory();
580: if (root_dir == null) {
581: String msg = "unable to build a default value for the \""
582: + propname + "\" value.";
583: throw new HTTPRuntimeException(this .getClass()
584: .getName(), "getFilename", msg);
585: }
586: flogdir = new File(root_dir, "logs");
587: } else {
588: try {
589: flogdir = new File(logdirname);
590: } catch (RuntimeException ex) {
591: String msg = "unable to access log directory "
592: + logdirname;
593: throw new HTTPRuntimeException(this .getClass()
594: .getName(), "getFilename", msg);
595: }
596: }
597: return (new File(flogdir, def)).getAbsolutePath();
598: } else {
599: String logdirname = props.getString(LOGDIRNAME_P, null);
600: if (logdirname == null)
601: return filename;
602: try {
603: flogdir = new File(logdirname);
604: } catch (RuntimeException ex) {
605: String msg = "unable to access log directory "
606: + logdirname;
607: throw new HTTPRuntimeException(this .getClass()
608: .getName(), "getFilename", msg);
609: }
610: return (new File(flogdir, filename)).getAbsolutePath();
611: }
612: }
613:
614: /**
615: * Open this logger log file.
616: */
617:
618: protected void openLogFile() {
619: if (rotateLevel > 0) {
620: Date now = new Date();
621: this .year = -1;
622: checkLogFile(now);
623: } else {
624: String logname = getFilename(LOGNAME_P, "log");
625: try {
626: RandomAccessFile old = log;
627: log = new RandomAccessFile(logname, "rw");
628: log.seek(log.length());
629: if (old != null)
630: old.close();
631: } catch (IOException e) {
632: throw new HTTPRuntimeException(this .getClass()
633: .getName(), "openLogFile", "unable to open "
634: + logname);
635: }
636: }
637: }
638:
639: /**
640: * Open this logger error log file.
641: */
642:
643: protected void openErrorLogFile() {
644: String errlogname = getFilename(ERRLOGNAME_P, "errlog");
645: try {
646: RandomAccessFile old = errlog;
647: errlog = new RandomAccessFile(errlogname, "rw");
648: errlog.seek(errlog.length());
649: if (old != null)
650: old.close();
651: } catch (IOException e) {
652: throw new HTTPRuntimeException(this .getClass().getName(),
653: "openErrorLogFile", "unable to open " + errlogname);
654: }
655: }
656:
657: /**
658: * Open this logger trace file.
659: */
660:
661: protected void openTraceFile() {
662: String tracename = getFilename(TRACELOGNAME_P, "traces");
663: try {
664: RandomAccessFile old = trace;
665: trace = new RandomAccessFile(tracename, "rw");
666: trace.seek(trace.length());
667: if (old != null)
668: old.close();
669: } catch (IOException e) {
670: throw new HTTPRuntimeException(this .getClass().getName(),
671: "openTraceFile", "unable to open " + tracename);
672: }
673: }
674:
675: /**
676: * Save all pending data to stable storage.
677: */
678:
679: public synchronized void sync() {
680: try {
681: if ((buffer != null) && (bufptr > 0)) {
682: log.write(buffer, 0, bufptr);
683: bufptr = 0;
684: }
685: } catch (IOException ex) {
686: server.errlog(getClass().getName()
687: + ": IO exception in method sync \""
688: + ex.getMessage() + "\".");
689: }
690: }
691:
692: /**
693: * Shutdown this logger.
694: */
695:
696: public synchronized void shutdown() {
697: server.getProperties().unregisterObserver(this );
698: try {
699: // Flush any pending output:
700: if ((buffer != null) && (bufptr > 0)) {
701: log.write(buffer, 0, bufptr);
702: bufptr = 0;
703: }
704: log.close();
705: log = null;
706: errlog.close();
707: errlog = null;
708: trace.close();
709: trace = null;
710: } catch (IOException ex) {
711: server.errlog(getClass().getName()
712: + ": IO exception in method shutdown \""
713: + ex.getMessage() + "\".");
714: }
715: }
716:
717: /**
718: * Initialize this logger for the given server.
719: * This method gets the server properties describe above to
720: * initialize its various log files.
721: * @param server The server to which thiss logger should initialize.
722: */
723:
724: public void initialize(httpd server) {
725: this .server = server;
726: this .props = server.getProperties();
727: // Register for property changes:
728: props.registerObserver(this );
729: // init the rotation level
730: rotateLevel = props.getInteger(ROTATE_LEVEL_P, 0);
731: // Open the various logs:
732: openLogFile();
733: openErrorLogFile();
734: openTraceFile();
735: // Setup the log buffer is possible:
736: if ((bufsize = props.getInteger(BUFSIZE_P, bufsize)) > 0)
737: buffer = new byte[bufsize];
738: return;
739: }
740:
741: /**
742: * Construct a new Logger instance.
743: */
744:
745: CommonLogger() {
746: this .msgbuf = new byte[128];
747: }
748: }
|