czwartek, 15 lipca 2010

Archiwizacja logów

Ostatnio zaistniała u mnie potrzeba archiwizowania starych logów z jbossa, ponieważ ich rozmiar szybko narastał. Dodatkowo z archiwizowane starsze były potrzebne tylko do paru dni wstecz, zatem niepotrzebne stare logi powinny się same usuwać.

Pierwszym pomysłem było napisanie klasy rozszerzającej klasę DailyRollingFileAppender z org.apache.log4j. Pomysł okazał się dobry, ale nie możliwy do zrealizowania w prosty sposób, ponieważ metodę rollover(), którą chciałem przeciążyć nie miała określonego dostępu public ani protected.

W sieci można znaleźć źródła klasy DailyRollingFileAppender, kopiujemy kod i tworzymy tą klasę w swoim pakiecie. Następnie tworzymy swoją klasę rozszerzającą skopiowaną klasę, przeciążamy w niej metodę rollover i gotowe.

Dla niecierpliwych źródła: ZipFileAppender.zip

Oto rozwiązanie:

Klasa ZipFileAppender

package pl.pkarpik.log4j;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * @author pkarpik
 *
 */
public class ZipFileAppender extends DailyRollingFileAppender{

 private String compressBackups = "false";
 private String maxNumberOfBackups = "14";


 @Override
 void rollOver() throws IOException {
  super.rollOver();
  buckupsManagment();
 }

 public void buckupsManagment() {
  final File file = new File(fileName);
  int maxBackups=14;
  try{
   maxBackups=Integer.parseInt(getMaxNumberOfBackups());
  }catch (Exception e) {
  }


  //zipowanie logow
  File[] files = file.getParentFile().listFiles(getFileFilter(file));
  for (int i = 0; i < files.length; i++) {
   if (getCompressBackups().equalsIgnoreCase("YES") || getCompressBackups().equalsIgnoreCase("TRUE")) {
    zipFile(files[i]);
   }
  }

  //sortowanie logow
  files = file.getParentFile().listFiles(getFileFilter(file));
  Arrays.sort(files, new Comparator() {
   @Override
   public int compare(File o1, File o2) {
    return o1.getName().compareToIgnoreCase(o2.getName());
   }
  });
  //usuwanie plikow w przypadku przekroczenia maksymalnej liczby logow
  for (int i = 0; i < files.length; i++) {
   if (i < files.length - maxBackups) {
    files[i].delete();
   }
  }

 }

protected void zipFile(File file) {
  if (!file.getName().endsWith(".zip")) {
   try {
    File zipFile = new File(file.getParent(), file.getName() + ".zip");
    FileInputStream fis;
    fis = new FileInputStream(file);
    FileOutputStream fos = new FileOutputStream(zipFile);
    ZipOutputStream zos = new ZipOutputStream(fos);
    ZipEntry zipEntry = new ZipEntry(file.getName());
    zos.putNextEntry(zipEntry);

    byte[] buffer = new byte[4096];
    while (true) {
     int bytesRead = fis.read(buffer);
     if (bytesRead == -1)
      break;
     else {
      zos.write(buffer, 0, bytesRead);
     }
    }
    zos.closeEntry();
    fis.close();
    zos.close();
    file.delete();
   } catch (FileNotFoundException e) {
   } catch (IOException e) {
   }
  }
 }

 protected FileFilter getFileFilter(final File file){
  return new FileFilter() {
   @Override
   public boolean accept(File pathname) {
    if(pathname.isDirectory() || pathname.getName().toUpperCase().equals(file.getName().toUpperCase()))
     return false;
    return pathname.getName().toUpperCase().startsWith(file.getName().toUpperCase());
   }
  };
 }



 public String getCompressBackups() {
  return compressBackups;
 }
 public void setCompressBackups(String compressBackups) {
  this.compressBackups = compressBackups;
 }
 public String getMaxNumberOfBackups() {
  return maxNumberOfBackups;
 }
 public void setMaxNumberOfBackups(String maxNumberOfBackups) {
  this.maxNumberOfBackups = maxNumberOfBackups;
 }

}
Klasa DailyRollingFileAppender - skopiowane zródła
package pl.pkarpik.log4j;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;

import org.apache.log4j.FileAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;

/**
 * 
 */
//Referenced classes of package org.apache.log4j:
//FileAppender, RollingCalendar, AppenderSkeleton, WriterAppender, 
//Layout
/*
DECOMPILATION REPORT

Decompiled from: C:\Users\pkarpik\.m2\repository\log4j\log4j\1.2.13\log4j-1.2.13.jar
Total time: 758 ms
Jad reported messages/errors:
Exit status: 0
Caught exceptions:
 */
public class DailyRollingFileAppender extends FileAppender
{

 public DailyRollingFileAppender()
 {
  datePattern = "'.'yyyy-MM-dd";
  nextCheck = System.currentTimeMillis() - 1L;
  now = new Date();
  rc = new RollingCalendar();
  checkPeriod = -1;
 }

 public DailyRollingFileAppender(Layout layout, String filename, String datePattern)
 throws IOException
 {
  super(layout, filename, true);
  this.datePattern = "'.'yyyy-MM-dd";
  nextCheck = System.currentTimeMillis() - 1L;
  now = new Date();
  rc = new RollingCalendar();
  checkPeriod = -1;
  this.datePattern = datePattern;
  activateOptions();
 }

 public void setDatePattern(String pattern)
 {
  datePattern = pattern;
 }

 public String getDatePattern()
 {
  return datePattern;
 }

 @Override
 public void activateOptions()
 {
  super.activateOptions();
  if(datePattern != null && super.fileName != null)
  {
   now.setTime(System.currentTimeMillis());
   sdf = new SimpleDateFormat(datePattern);
   int type = computeCheckPeriod();
   printPeriodicity(type);
   rc.setType(type);
   File file = new File(super.fileName);
   scheduledFilename = super.fileName + sdf.format(new Date(file.lastModified()));
  } else
  {
   LogLog.error("Either File or DatePattern options are not set for appender [" + super.name + "].");
  }
 }

 void printPeriodicity(int type)
 {
  switch(type)
  {
  case 0: // '\0'
   LogLog.debug("Appender [" + super.name + "] to be rolled every minute.");
   break;

  case 1: // '\001'
   LogLog.debug("Appender [" + super.name + "] to be rolled on top of every hour.");
   break;

  case 2: // '\002'
   LogLog.debug("Appender [" + super.name + "] to be rolled at midday and midnight.");
   break;

  case 3: // '\003'
   LogLog.debug("Appender [" + super.name + "] to be rolled at midnight.");
   break;

  case 4: // '\004'
   LogLog.debug("Appender [" + super.name + "] to be rolled at start of week.");
   break;

  case 5: // '\005'
   LogLog.debug("Appender [" + super.name + "] to be rolled at start of every month.");
   break;

  default:
   LogLog.warn("Unknown periodicity for appender [" + super.name + "].");
   break;
  }
 }

 int computeCheckPeriod()
 {
  RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone, Locale.ENGLISH);
  Date epoch = new Date(0L);
  if(datePattern != null)
  {
   for(int i = 0; i <= 5; i++)
   {
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
    simpleDateFormat.setTimeZone(gmtTimeZone);
    String r0 = simpleDateFormat.format(epoch);
    rollingCalendar.setType(i);
    Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
    String r1 = simpleDateFormat.format(next);
    if(r0 != null && r1 != null && !r0.equals(r1))
     return i;
   }

  }
  return -1;
 }

 void rollOver()
 throws IOException
 {
  if(datePattern == null)
  {
   super.errorHandler.error("Missing DatePattern option in rollOver().");
   return;
  }
  String datedFilename = super.fileName + sdf.format(now);
  if(scheduledFilename.equals(datedFilename))
   return;
  closeFile();
  File target = new File(scheduledFilename);
  if(target.exists())
   target.delete();
  File file = new File(super.fileName);
  boolean result = file.renameTo(target);
  if(result)
   LogLog.debug(super.fileName + " -> " + scheduledFilename);
  else
   LogLog.error("Failed to rename [" + super.fileName + "] to [" + scheduledFilename + "].");
  try
  {
   setFile(super.fileName, false, super.bufferedIO, super.bufferSize);
  }
  catch(IOException e)
  {
   super.errorHandler.error("setFile(" + super.fileName + ", false) call failed.");
  }
  scheduledFilename = datedFilename;
 }

 @Override
 protected void subAppend(LoggingEvent event)
 {
  long n = System.currentTimeMillis();
  if(n >= nextCheck)
  {
   now.setTime(n);
   nextCheck = rc.getNextCheckMillis(now);
   try
   {
    rollOver();
   }
   catch(IOException ioe)
   {
    LogLog.error("rollOver() failed.", ioe);
   }
  }
  super.subAppend(event);
 }

 static final int TOP_OF_TROUBLE = -1;
 static final int TOP_OF_MINUTE = 0;
 static final int TOP_OF_HOUR = 1;
 static final int HALF_DAY = 2;
 static final int TOP_OF_DAY = 3;
 static final int TOP_OF_WEEK = 4;
 static final int TOP_OF_MONTH = 5;
 private String datePattern;
 private String scheduledFilename;
 private long nextCheck;
 Date now;
 SimpleDateFormat sdf;
 RollingCalendar rc;
 int checkPeriod;
 static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");

 class RollingCalendar extends GregorianCalendar {
  private static final long serialVersionUID = -3560331770601814177L;

  int type = TOP_OF_TROUBLE;

  RollingCalendar() {
   super();
  }

  RollingCalendar(TimeZone tz, Locale locale) {
   super(tz, locale);
  }

  void setType(int type) {
   this.type = type;
  }

  public long getNextCheckMillis(Date now) {
   return getNextCheckDate(now).getTime();
  }

  public Date getNextCheckDate(Date now) {
   this.setTime(now);

   switch (type) {
   case TOP_OF_MINUTE:
    this.set(Calendar.SECOND, 0);
    this.set(Calendar.MILLISECOND, 0);
    this.add(Calendar.MINUTE, 1);
    break;
   case TOP_OF_HOUR:
    this.set(Calendar.MINUTE, 0);
    this.set(Calendar.SECOND, 0);
    this.set(Calendar.MILLISECOND, 0);
    this.add(Calendar.HOUR_OF_DAY, 1);
    break;
   case HALF_DAY:
    this.set(Calendar.MINUTE, 0);
    this.set(Calendar.SECOND, 0);
    this.set(Calendar.MILLISECOND, 0);
    int hour = get(Calendar.HOUR_OF_DAY);
    if (hour < 12) {
     this.set(Calendar.HOUR_OF_DAY, 12);
    } else {
     this.set(Calendar.HOUR_OF_DAY, 0);
     this.add(Calendar.DAY_OF_MONTH, 1);
    }
    break;
   case TOP_OF_DAY:
    this.set(Calendar.HOUR_OF_DAY, 0);
    this.set(Calendar.MINUTE, 0);
    this.set(Calendar.SECOND, 0);
    this.set(Calendar.MILLISECOND, 0);
    this.add(Calendar.DATE, 1);
    break;
   case TOP_OF_WEEK:
    this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
    this.set(Calendar.HOUR_OF_DAY, 0);
    this.set(Calendar.MINUTE, 0);
    this.set(Calendar.SECOND, 0);
    this.set(Calendar.MILLISECOND, 0);
    this.add(Calendar.WEEK_OF_YEAR, 1);
    break;
   case TOP_OF_MONTH:
    this.set(Calendar.DATE, 1);
    this.set(Calendar.HOUR_OF_DAY, 0);
    this.set(Calendar.MINUTE, 0);
    this.set(Calendar.SECOND, 0);
    this.set(Calendar.MILLISECOND, 0);
    this.add(Calendar.MONTH, 1);
    break;
   default:
    throw new IllegalStateException("Unknown periodicity type.");
   }
   return getTime();
  }
 }
}
Przykładowa konfiguracja w pliku jboss-log4j.xml

     ..
     .
     .
      < param name="DatePattern" value="'.'yyyy-MM-dd-HH-mm" >


      < param name="CompressBackups" value="YES" >
      < param name="MaxNumberOfBackups" value="3" >
     .
     .
     .