001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.log;
031:
032: import com.caucho.config.ConfigException;
033: import com.caucho.config.types.Bytes;
034: import com.caucho.config.types.Period;
035: import com.caucho.util.Alarm;
036: import com.caucho.util.L10N;
037: import com.caucho.util.QDate;
038: import com.caucho.util.ThreadPool;
039: import com.caucho.vfs.Path;
040: import com.caucho.vfs.ReadStream;
041: import com.caucho.vfs.TempStream;
042: import com.caucho.vfs.Vfs;
043: import com.caucho.vfs.WriteStream;
044:
045: import java.io.IOException;
046: import java.io.FilterOutputStream;
047: import java.io.OutputStream;
048: import java.util.ArrayList;
049: import java.util.Collections;
050: import java.util.regex.Matcher;
051: import java.util.regex.Pattern;
052: import java.util.zip.GZIPOutputStream;
053: import java.util.zip.ZipOutputStream;
054: import java.util.zip.DeflaterOutputStream;
055:
056: /**
057: * Abstract class for a log that rolls over based on size or period.
058: */
059: public class AbstractRolloverLog {
060: protected static final L10N L = new L10N(AbstractRolloverLog.class);
061:
062: // Milliseconds in an hour
063: private static final long HOUR = 3600L * 1000L;
064: // Milliseconds in a day
065: private static final long DAY = 24L * 3600L * 1000L;
066:
067: // Default maximum log size = 2G
068: private static final long DEFAULT_ROLLOVER_SIZE = Bytes.INFINITE;
069: // How often to check size
070: private static final long DEFAULT_ROLLOVER_CHECK_PERIOD = 600L * 1000L;
071:
072: private static final long ROLLOVER_OVERFLOW_MAX = 64 * 1024 * 1024;
073:
074: // prefix for the rollover
075: private String _rolloverPrefix;
076:
077: // template for the archived files
078: private String _archiveFormat;
079: // .gz or .zip
080: private String _archiveSuffix = "";
081:
082: // How often the logs are rolled over.
083: private long _rolloverPeriod = Period.INFINITE;
084:
085: // Maximum size of the log.
086: private long _rolloverSize = DEFAULT_ROLLOVER_SIZE;
087:
088: // How often the rolloverSize should be checked
089: //private long _rolloverCheckPeriod = DEFAULT_ROLLOVER_CHECK_PERIOD;
090: private long _rolloverCheckPeriod = 120 * 1000;
091:
092: // How many archives are allowed.
093: private int _rolloverCount;
094:
095: private QDate _calendar = QDate.createLocal();
096:
097: private Path _pwd = Vfs.lookup();
098:
099: protected Path _path;
100:
101: protected String _pathFormat;
102:
103: private String _format;
104:
105: // The time of the next period-based rollover
106: private long _nextPeriodEnd = -1;
107: private long _nextRolloverCheckTime = -1;
108:
109: private long _lastTime;
110:
111: private boolean _isRollingOver;
112: private Path _savedPath;
113: private TempStream _tempStream;
114: private long _tempStreamSize;
115: private ArchiveTask _archiveTask = new ArchiveTask();
116:
117: private WriteStream _os;
118: private WriteStream _zipOut;
119:
120: /**
121: * Returns the access-log's path.
122: */
123: public Path getPath() {
124: return _path;
125: }
126:
127: /**
128: * Sets the access-log's path.
129: */
130: public void setPath(Path path) {
131: _path = path;
132: }
133:
134: /**
135: * Returns the pwd for the rollover log
136: */
137: public Path getPwd() {
138: return _pwd;
139: }
140:
141: /**
142: * Returns the formatted path
143: */
144: public String getPathFormat() {
145: return _pathFormat;
146: }
147:
148: /**
149: * Sets the formatted path.
150: */
151: public void setPathFormat(String pathFormat) throws ConfigException {
152: _pathFormat = pathFormat;
153:
154: if (pathFormat.endsWith(".zip")) {
155: throw new ConfigException(
156: L
157: .l(".zip extension to path-format is not supported."));
158: }
159: }
160:
161: /**
162: * Sets the archive name format
163: */
164: public void setArchiveFormat(String format) {
165: if (format.endsWith(".gz")) {
166: _archiveFormat = format.substring(0, format.length()
167: - ".gz".length());
168: _archiveSuffix = ".gz";
169: } else if (format.endsWith(".zip")) {
170: _archiveFormat = format.substring(0, format.length()
171: - ".zip".length());
172: _archiveSuffix = ".zip";
173: } else {
174: _archiveFormat = format;
175: _archiveSuffix = "";
176: }
177: }
178:
179: /**
180: * Sets the archive name format
181: */
182: public String getArchiveFormat() {
183: if (_archiveFormat == null)
184: return _rolloverPrefix + ".%Y%m%d.%H%M";
185: else
186: return _archiveFormat;
187: }
188:
189: /**
190: * Sets the log rollover period, rounded up to the nearest hour.
191: *
192: * @param period the new rollover period in milliseconds.
193: */
194: public void setRolloverPeriod(Period period) {
195: _rolloverPeriod = period.getPeriod();
196:
197: if (_rolloverPeriod > 0) {
198: _rolloverPeriod += 3600000L - 1;
199: _rolloverPeriod -= _rolloverPeriod % 3600000L;
200: } else
201: _rolloverPeriod = Period.INFINITE;
202: }
203:
204: /**
205: * Sets the log rollover period, rounded up to the nearest hour.
206: *
207: * @return the new period in milliseconds.
208: */
209: public long getRolloverPeriod() {
210: return _rolloverPeriod;
211: }
212:
213: /**
214: * Sets the log rollover size, rounded up to the megabyte.
215: *
216: * @param bytes maximum size of the log file
217: */
218: public void setRolloverSize(Bytes bytes) {
219: long size = bytes.getBytes();
220:
221: if (size < 0)
222: _rolloverSize = Bytes.INFINITE;
223: else
224: _rolloverSize = size;
225: }
226:
227: /**
228: * Sets the log rollover size, rounded up to the megabyte.
229: *
230: * @return maximum size of the log file
231: */
232: public long getRolloverSize() {
233: return _rolloverSize;
234: }
235:
236: /**
237: * Sets how often the log rollover will be checked.
238: *
239: * @param period how often the log rollover will be checked.
240: */
241: public void setRolloverCheckPeriod(long period) {
242: if (period > 1000)
243: _rolloverCheckPeriod = period;
244: else if (period > 0)
245: _rolloverCheckPeriod = 1000;
246: }
247:
248: /**
249: * Sets how often the log rollover will be checked.
250: *
251: * @return how often the log rollover will be checked.
252: */
253: public long getRolloverCheckPeriod() {
254: return _rolloverCheckPeriod;
255: }
256:
257: /**
258: * Sets the max rollover files.
259: */
260: public void setRolloverCount(int count) {
261: _rolloverCount = count;
262: }
263:
264: public void setLastTime(long lastTime) {
265: _lastTime = lastTime;
266: }
267:
268: /**
269: * Initialize the log.
270: */
271: public void init() throws IOException {
272: long now = Alarm.getCurrentTime();
273:
274: _nextRolloverCheckTime = now + _rolloverCheckPeriod;
275:
276: Path path = getPath();
277:
278: if (path != null) {
279: path.getParent().mkdirs();
280:
281: _rolloverPrefix = path.getTail();
282:
283: long lastModified = path.getLastModified();
284: if (lastModified <= 0)
285: lastModified = now;
286:
287: _calendar.setGMTTime(lastModified);
288:
289: _nextPeriodEnd = Period.periodEnd(lastModified,
290: getRolloverPeriod());
291: } else
292: _nextPeriodEnd = Period.periodEnd(now, getRolloverPeriod());
293:
294: if (_nextPeriodEnd < _nextRolloverCheckTime
295: && _nextPeriodEnd > 0)
296: _nextRolloverCheckTime = _nextPeriodEnd;
297:
298: if (_archiveFormat != null || getRolloverPeriod() <= 0) {
299: } else if (getRolloverPeriod() % DAY == 0)
300: _archiveFormat = _rolloverPrefix + ".%Y%m%d";
301: else if (getRolloverPeriod() % HOUR == 0)
302: _archiveFormat = _rolloverPrefix + ".%Y%m%d.%H";
303: else
304: _archiveFormat = _rolloverPrefix + ".%Y%m%d.%H%M";
305:
306: rolloverLog(now);
307: }
308:
309: public long getNextRolloverCheckTime() {
310: if (_nextPeriodEnd < _nextRolloverCheckTime)
311: return _nextPeriodEnd;
312: else
313: return _nextRolloverCheckTime;
314: }
315:
316: public boolean isRollover() {
317: long now = Alarm.getCurrentTime();
318:
319: return _nextPeriodEnd <= now || _nextRolloverCheckTime <= now;
320: }
321:
322: public boolean rollover() {
323: long now = Alarm.getCurrentTime();
324:
325: if (_nextPeriodEnd <= now || _nextRolloverCheckTime <= now) {
326: rolloverLog(now);
327: return true;
328: } else
329: return false;
330: }
331:
332: /**
333: * Writes to the underlying log.
334: */
335: protected void write(byte[] buffer, int offset, int length)
336: throws IOException {
337: synchronized (this ) {
338: if (_isRollingOver
339: && ROLLOVER_OVERFLOW_MAX < _tempStreamSize) {
340: try {
341: wait();
342: } catch (Exception e) {
343: }
344: }
345:
346: if (!_isRollingOver) {
347: if (_os == null)
348: openLog();
349:
350: if (_os != null)
351: _os.write(buffer, offset, length);
352: } else {
353: if (_tempStream == null) {
354: _tempStream = new TempStream();
355: _tempStreamSize = 0;
356: }
357:
358: _tempStreamSize += length;
359: _tempStream.write(buffer, offset, length, false);
360: }
361: }
362: }
363:
364: /**
365: * Writes to the underlying log.
366: */
367: protected void flush() throws IOException {
368: synchronized (this ) {
369: if (_os != null)
370: _os.flush();
371:
372: if (_zipOut != null)
373: _zipOut.flush();
374: }
375: }
376:
377: /**
378: * Check to see if we need to rollover the log.
379: *
380: * @param now current time in milliseconds.
381: */
382: protected void rolloverLog(long now) {
383: boolean isRollingOver = false;
384:
385: try {
386: Path savedPath = null;
387:
388: synchronized (this ) {
389: if (_isRollingOver || now <= _nextRolloverCheckTime)
390: return;
391:
392: _isRollingOver = isRollingOver = true;
393:
394: _nextRolloverCheckTime = now + _rolloverCheckPeriod;
395:
396: long lastPeriodEnd = _nextPeriodEnd;
397: _nextPeriodEnd = Period.periodEnd(now,
398: getRolloverPeriod());
399:
400: Path path = getPath();
401:
402: if (lastPeriodEnd < now) {
403: closeLogStream();
404:
405: if (getPathFormat() == null) {
406: savedPath = getArchivePath(lastPeriodEnd - 1);
407: }
408:
409: /*
410: if (log.isLoggable(Level.FINE))
411: log.fine(getPath() + ": next rollover at " +
412: QDate.formatLocal(_nextPeriodEnd));
413: */
414: } else if (path != null
415: && getRolloverSize() <= path.getLength()) {
416: closeLogStream();
417:
418: if (getPathFormat() == null) {
419: savedPath = getArchivePath(now);
420: }
421: }
422:
423: long nextPeriodEnd = _nextPeriodEnd;
424: if (_nextPeriodEnd < _nextRolloverCheckTime
425: && _nextPeriodEnd > 0)
426: _nextRolloverCheckTime = _nextPeriodEnd;
427: }
428:
429: // archiving of path is outside of the synchronized block to
430: // avoid freezing during archive
431: if (savedPath != null) {
432: _savedPath = savedPath;
433: isRollingOver = false;
434: ThreadPool.getThreadPool().startPriority(_archiveTask);
435: Thread.yield();
436: }
437: } finally {
438: synchronized (this ) {
439: if (isRollingOver) {
440: _isRollingOver = false;
441: flushTempStream();
442: }
443: }
444: }
445: }
446:
447: /**
448: * Tries to open the log.
449: */
450: private void openLog() {
451: closeLogStream();
452:
453: try {
454: WriteStream os = _os;
455: _os = null;
456:
457: if (os != null)
458: os.close();
459: } catch (Throwable e) {
460: // can't log in log routines
461: }
462:
463: Path path = getPath();
464:
465: if (path == null) {
466: path = getPath(Alarm.getCurrentTime());
467: }
468:
469: try {
470: if (!path.getParent().isDirectory())
471: path.getParent().mkdirs();
472: } catch (Throwable e) {
473: logWarning(L.l("Can't create log directory {0}", path
474: .getParent()), e);
475: }
476:
477: Exception exn = null;
478:
479: for (int i = 0; i < 3 && _os == null; i++) {
480: try {
481: _os = path.openAppend();
482: } catch (IOException e) {
483: exn = e;
484: }
485: }
486:
487: String pathName = path.getPath();
488:
489: try {
490: if (pathName.endsWith(".gz")) {
491: _zipOut = _os;
492: _os = Vfs.openWrite(new GZIPOutputStream(_zipOut));
493: } else if (pathName.endsWith(".zip")) {
494: throw new ConfigException(
495: "Can't support .zip in path-format");
496: }
497: } catch (Exception e) {
498: if (exn == null)
499: exn = e;
500: }
501:
502: if (exn != null)
503: logWarning(L.l("Can't create log directory {0}", path), exn);
504: }
505:
506: private void movePathToArchive(Path savedPath) {
507: if (savedPath == null)
508: return;
509:
510: closeLogStream();
511:
512: Path path = getPath();
513:
514: String savedName = savedPath.getTail();
515:
516: try {
517: if (!savedPath.getParent().isDirectory())
518: savedPath.getParent().mkdirs();
519: } catch (Throwable e) {
520: logWarning(L.l("Can't open archive directory {0}",
521: savedPath.getParent()), e);
522: }
523:
524: try {
525: if (path.exists()) {
526: WriteStream os = savedPath.openWrite();
527: OutputStream out;
528:
529: if (savedName.endsWith(".gz"))
530: out = new GZIPOutputStream(os);
531: else if (savedName.endsWith(".zip"))
532: out = new ZipOutputStream(os);
533: else
534: out = os;
535:
536: try {
537: path.writeToStream(out);
538: } finally {
539: try {
540: out.close();
541: } catch (Throwable e) {
542: // can't log in log rotation routines
543: }
544:
545: try {
546: if (out != os)
547: os.close();
548: } catch (Throwable e) {
549: // can't log in log rotation routines
550: }
551: }
552: }
553: } catch (Throwable e) {
554: logWarning(L.l("Error rotating logs"), e);
555: }
556:
557: try {
558: try {
559: if (!path.truncate())
560: path.remove();
561: } catch (IOException e) {
562: path.remove();
563:
564: throw e;
565: }
566: } catch (Exception e) {
567: logWarning(L.l("Error truncating logs"), e);
568: }
569:
570: if (_rolloverCount > 0)
571: removeOldLogs();
572: }
573:
574: /**
575: * Removes logs passing the rollover count.
576: */
577: private void removeOldLogs() {
578: try {
579: Path path = getPath();
580: Path parent = path.getParent();
581:
582: String[] list = parent.list();
583:
584: ArrayList<String> matchList = new ArrayList<String>();
585:
586: Pattern archiveRegexp = getArchiveRegexp();
587: for (int i = 0; i < list.length; i++) {
588: Matcher matcher = archiveRegexp.matcher(list[i]);
589:
590: if (matcher.matches())
591: matchList.add(list[i]);
592: }
593:
594: Collections.sort(matchList);
595:
596: if (_rolloverCount <= 0
597: || matchList.size() < _rolloverCount)
598: return;
599:
600: for (int i = 0; i + _rolloverCount < matchList.size(); i++) {
601: try {
602: parent.lookup(matchList.get(i)).remove();
603: } catch (Throwable e) {
604: }
605: }
606: } catch (Throwable e) {
607: }
608: }
609:
610: private Pattern getArchiveRegexp() {
611: StringBuilder sb = new StringBuilder();
612:
613: String archiveFormat = getArchiveFormat();
614:
615: for (int i = 0; i < archiveFormat.length(); i++) {
616: char ch = archiveFormat.charAt(i);
617:
618: switch (ch) {
619: case '.':
620: case '\\':
621: case '*':
622: case '?':
623: case '+':
624: case '(':
625: case ')':
626: case '{':
627: case '}':
628: case '|':
629: sb.append("\\");
630: sb.append(ch);
631: break;
632: case '%':
633: sb.append(".+");
634: i++;
635: break;
636: default:
637: sb.append(ch);
638: break;
639: }
640: }
641:
642: return Pattern.compile(sb.toString());
643: }
644:
645: /**
646: * Returns the path of the format file
647: *
648: * @param time the archive date
649: */
650: protected Path getPath(long time) {
651: String formatString = getPathFormat();
652:
653: if (formatString == null)
654: throw new IllegalStateException(L
655: .l("getPath requires a format path"));
656:
657: String pathString = getFormatName(formatString, time);
658:
659: return getPwd().lookup(pathString);
660: }
661:
662: /**
663: * Returns the name of the archived file
664: *
665: * @param time the archive date
666: */
667: protected Path getArchivePath(long time) {
668: Path path = getPath();
669:
670: String archiveFormat = getArchiveFormat();
671:
672: String name = getFormatName(archiveFormat + _archiveSuffix,
673: time);
674: Path newPath = path.getParent().lookup(name);
675:
676: if (newPath.exists()) {
677: if (archiveFormat.indexOf("%H") < 0)
678: archiveFormat = archiveFormat + ".%H%M";
679: else if (archiveFormat.indexOf("%M") < 0)
680: archiveFormat = archiveFormat + ".%M";
681:
682: for (int i = 0; i < 100; i++) {
683: String suffix;
684:
685: if (i == 0)
686: suffix = _archiveSuffix;
687: else
688: suffix = "." + i + _archiveSuffix;
689:
690: name = getFormatName(archiveFormat + suffix, time);
691:
692: newPath = path.getParent().lookup(name);
693:
694: if (!newPath.exists())
695: break;
696: }
697: }
698:
699: return newPath;
700: }
701:
702: /**
703: * Returns the name of the archived file
704: *
705: * @param time the archive date
706: */
707: protected String getFormatName(String format, long time) {
708: if (time <= 0)
709: time = Alarm.getCurrentTime();
710:
711: if (format != null)
712: return _calendar.formatLocal(time, format);
713: else if (getRolloverPeriod() % (24 * 3600 * 1000L) == 0)
714: return _rolloverPrefix + "."
715: + _calendar.formatLocal(time, "%Y%m%d");
716: else
717: return _rolloverPrefix + "."
718: + _calendar.formatLocal(time, "%Y%m%d.%H");
719: }
720:
721: /**
722: * error messages from the log itself
723: */
724: private void logInfo(String msg) {
725: EnvironmentStream.logStderr(msg);
726: }
727:
728: /**
729: * error messages from the log itself
730: */
731: private void logWarning(String msg, Throwable e) {
732: EnvironmentStream.logStderr(msg, e);
733: }
734:
735: /**
736: * Closes the log, flushing the results.
737: */
738: public synchronized void close() throws IOException {
739: closeLogStream();
740: }
741:
742: /**
743: * Tries to close the log.
744: */
745: private void closeLogStream() {
746: try {
747: WriteStream os = _os;
748: _os = null;
749:
750: if (os != null)
751: os.close();
752: } catch (Throwable e) {
753: // can't log in log routines
754: }
755:
756: try {
757: WriteStream zipOut = _zipOut;
758: _zipOut = null;
759:
760: if (zipOut != null)
761: zipOut.close();
762: } catch (Throwable e) {
763: // can't log in log routines
764: }
765: }
766:
767: private void flushTempStream() {
768: TempStream ts = _tempStream;
769: _tempStream = null;
770: _tempStreamSize = 0;
771:
772: try {
773: if (ts != null) {
774: if (_os == null)
775: openLog();
776:
777: try {
778: ReadStream is = ts.openRead();
779:
780: try {
781: is.writeToStream(_os);
782: } finally {
783: is.close();
784: }
785: } catch (IOException e) {
786: e.printStackTrace();
787: }
788: }
789: } finally {
790: notifyAll();
791: }
792: }
793:
794: class ArchiveTask implements Runnable {
795: private boolean _isArchiving;
796:
797: public void run() {
798: try {
799: synchronized (this ) {
800: Path savedPath = _savedPath;
801:
802: if (savedPath != null)
803: movePathToArchive(savedPath);
804:
805: _savedPath = null;
806: }
807: } finally {
808: // Write any new data from the temp stream to the log.
809: synchronized (AbstractRolloverLog.this ) {
810: _isRollingOver = false;
811:
812: flushTempStream();
813: }
814: }
815: }
816: }
817: }
|