Tomcat 7基於SocketAppender的日誌採集方案

當前系統中的日誌由各個獨立的Tomcat產生,日誌存儲的比較分散,不便於管理,而且由於採用將文件寫入NAS的方式記錄日誌,當磁盤出現故障的情況下會導致Tomcat異常。爲消除日誌實體和Tomcat程序的依賴以及解決日誌的管理問題,決定採用基於Socket的遠程日誌收集方案。

本方案需要對Tomcat的日誌系統做一些改動,具體方案如下:

一、Tomcat的改造:

Tomcat原生的日誌模塊是基於java.util.Logging改造的日誌收集器,使用和配置均比較簡單,但是不適用於一些複雜的日誌記錄需求,比如此次的遠程日誌收集方案。爲適應當前的需求,採用Log4j作爲日誌記錄器。

         首先從Tomcat網站找到JULI log4j jar和JULI adapters jar兩個鏈接(http://tomcat.apache.org/download-70.cgi,在extra分類下),下載後得到tomcat-juli.jar和tomcat-juli-adapters.jar兩個jar文件,將tomcat-juli.jar拷貝至tomcat安裝目錄的bin文件夾下覆蓋原來的文件,將tomcat-juli-adapters.jar和log4j.jar拷貝至tomcat安裝目錄的lib文件夾下,同時刪除conf文件夾下的logging.properties文件。在lib目錄下建立log4j.properties文件,內容如下:

log4j.rootLogger=INFO,Console,Server  

 

#Console Appender

log4j.appender.Console=org.apache.log4j.ConsoleAppender 

log4j.appender.Console.layout=org.apache.log4j.PatternLayout   

log4j.appender.Console.layout.ConversionPattern=%d{yy-MM-dd HH:mm:ss} %5p %c{1}:%L - %m%n  

 

#Socket Appender

log4j.appender.Server=org.apache.log4j.net.SocketAppender 

log4j.appender.Server.Port=4712

log4j.appender.Server.RemoteHost=192.168.1.200 

log4j.appender.Server.layout.ReconnectionDelay=10000

log4j.appender.Server.application=test  #這條的含義下面會說

 

log4j.logger.org.apache=INFO, Server

log4j.logger.org.apache.catalina.core=INFO, Server

log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost]=INFO, Server

log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager]=INFO, Server

log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager]=INFO, Server

log4j.logger.org.apache.catalina.session=INFO, Server

log4j.logger.accessLog=Server

 

log4j.additivity. accessLog =false

其中org.apache.log4j.net.SocketAppender就是log4j提供的基於Socket的日誌收集器,下邊三條分別指定了遠程日誌採集服務器的端口、IP和重連時間。

         還有一部分是Tomcat的訪問日誌,由於訪問日誌是獨立配置在server.xml的Valve配置節中,默認如下

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"

               prefix="localhost_access_log." suffix=".txt"

               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

我們需要提供自定義的實現用以將訪問日誌也通過Log4j輸出,代碼如下:

package com.surdoc.tomcat.extend.log;

 

import java.text.DateFormat;

import java.text.MessageFormat;

import java.text.SimpleDateFormat;

import java.util.Date;

 

import org.apache.catalina.valves.AccessLogValve;

import org.apache.log4j.Logger;

 

public class Log4jAccessLogValve extends AccessLogValve {

         private final Logger logger = Logger.getLogger("accessLog");

    protected static final String valveinfo ="com.surdoc.tomcat.extend.log.Log4JAccessLogValve"; 

 

    @Override

    public void log(String message) { 

        logger.info(message+"\n");  

    }

 

    @Override

    public String getInfo() {

        return valveinfo;

    }

 

    @Override

    protected void open() {

    }

}

將這個類編譯後打成名爲log4jaccesslogvalve.jar的jar包,放到tomcat安裝目錄的lib文件夾下,注意在這裏將Logger命名爲accessLog,與log4j.properties中的log4j.logger.accessLog這個Logger對應,然後將上面提到的server.xml中的訪問日誌配置改爲此類:

<Valve className="com.surdoc.tomcat.extend.log.Log4jAccessLogValve" directory="logs"

               prefix="localhost_access_log." suffix=".txt"

               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

至此Tomcat改造完畢。

二、應用中的日誌設置

我們當前的應用也採用了Log4j作爲日誌記錄器,所以只需要將log4j配置文件中的logger都指向SocketAppender就行,如下:

log4j.rootLogger=WARN, A1

 

         #Console Appender

log4j.appender.A1=org.apache.log4j.ConsoleAppender

log4j.appender.A1.layout=org.apache.log4j.PatternLayout

log4j.appender.A1.layout.ConversionPattern=%-d{yy-MM-dd HH\:mm\:ss}  [%c\:%L]-[%p] %m%n

 

#Socket Appender

log4j.appender.Server=org.apache.log4j.net.SocketAppender 

log4j.appender.Server.Port=4712

log4j.appender.Server.RemoteHost=192.168.1.200 

log4j.appender.Server.layout.ReconnectionDelay=10000

log4j.appender.Server.application=test

 

log4j.logger.org.apache=INFO,Server

log4j.logger.org.hibernate=INFO,Server

log4j.logger.org.springframework=INFO,Server

log4j.logger.com.sursen.webdocbase=INFO,Server

 

三、日誌採集服務器

Log4j中提供了一個簡單的日誌採集器org.apache.log4j.net.SimpleSocketServer,只需要將監聽端口號和server端的配置在啓動時傳入:

java -classpath log4j-1.2.17.jar org.apache.log4j.net.SimpleSocketServer 4712 log4j-server.properties

這裏我們監聽4712端口,和tomcat的SocketAppender配置一致。

         至於log4j-server.properties這個文件,先看一下內容:

log4j.rootLogger=WARN,Console

 

log4j.appender.Console=org.apache.log4j.ConsoleAppender 

log4j.appender.Console.layout=org.apache.log4j.PatternLayout

log4j.appender.Console.layout.ConversionPattern=%d{yy-MM-dd HH:mm:ss} %5p %c{1}:%L - %m%n

 

log4j.appender.Catalina=org.apache.log4j.DailyRollingFileAppender 

log4j.appender.Catalina.File=logs/catalina.log  

log4j.appender.Catalina.layout=org.apache.log4j.PatternLayout 

log4j.appender.Catalina.layout.ConversionPattern=%d{yyyy.MM.dd HH:mm:ss} %5p %c{1}(%L):? %m%n

 

log4j.appender.Manager=org.apache.log4j.DailyRollingFileAppender 

log4j.appender.Manager.File=logs/manager.log

log4j.appender.Manager.layout=org.apache.log4j.PatternLayout 

log4j.appender.Manager.layout.ConversionPattern=%d{yyyy.MM.dd HH:mm:ss} %5p %c{1}(%L):? %m%n

 

log4j.appender.HostManager=org.apache.log4j.DailyRollingFileAppender 

log4j.appender.HostManager.File=logs/host-manager.log

log4j.appender.HostManager.layout=org.apache.log4j.PatternLayout 

log4j.appender.HostManager.layout.ConversionPattern=%d{yyyy.MM.dd HH:mm:ss} %5p %c{1}(%L):? %m%n

 

log4j.appender.AccessLog=org.apache.log4j.DailyRollingFileAppender 

log4j.appender.AccessLog.File=logs/accesslog.log

log4j.appender.AccessLog.layout=org.apache.log4j.PatternLayout 

log4j.appender.AccessLog.layout.ConversionPattern=%m%n

 

log4j.appender.R=org.apache.log4j.DailyRollingFileAppender

log4j.appender.R.File=logs/webdocbaseLog.log

log4j.appender.R.DatePattern = '.'yyyy-MM-dd

log4j.appender.R.layout=org.apache.log4j.PatternLayout

log4j.appender.R.layout.ConversionPattern=%-d{yy/MM/dd HH:mm} [%c:%L]-[%p] %m%nlog4j.appender.file.layout.ConversionPattern=%m

 


#Logger

log4j.logger.org.apache=INFO, Catalina

log4j.logger.org.apache.catalina.core=INFO, Catalina

log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost]=INFO, Catalina

log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager]=INFO, Manager

log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager]=INFO, HostManager

log4j.logger.accessLog=INFO, AccessLog

log4j.logger.com.sursen.webdocbase=INFO,R

和改造之前的log4j配置文件沒什麼區別,Tomcat和服務器端是通過logger名進行對應的,舉個例子:accessLog這個logger會將日誌記錄到服務器啓動目錄下的logs文件夾下的accesslog.log文件中,如果現在有兩個tomcat:tomcat1和tomcat2,他們都有名爲accessLog的logger,服務器端則會將由這兩個logger發出的日誌都寫到logs/accesslog.log這個文件中,這樣不便於區分不同tomcat的日誌,接下來我們要實現自定義的日誌收集器,我們可以通過log4j.appender.Server.application這個參數來標識一個特定的tomcat(甚至只表示一個appender),而這個值用來與服務器端conf目錄下的logroot.properties進行對應以確定其日誌存儲的路徑,logroot.properties內容像這樣:

test=logs/test

         test就是log4j.appender.Server.application對應的值,這樣凡是標識了test的日誌都會記錄到logs/test這個文件夾下,對於不同的log4j.appender.Server.application需要有不同的服務器端配置,這些配置均放在服務器啓動路徑下的config/client文件夾下,其名稱與log4j.appender.Server.application這個值也是對應的,比如對於test這個配置名就是test.properties。

         接下來看一下服務器端代碼,我們在Log4j提供的SimpleSocketServer基礎上進行修改,在Eclipse裏新建Java項目,先在項目中加入log4j的源碼,因爲源碼也要做一些修改:

package com.surdoc.log4j.extend.server;

 

import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

import java.io.InputStream;

import java.net.InetAddress;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.Hashtable;

import java.util.Properties;

import java.util.Set;

 

import org.apache.log4j.Hierarchy;

import org.apache.log4j.Level;

import org.apache.log4j.Logger;

import org.apache.log4j.PropertyConfigurator;

import org.apache.log4j.spi.RootLogger;

 

public class DispatchSocketServer {

    static String CLIENT_DIR = "client"; //必須是client文件夾

    static String CONFIG_FILE_EXT = ".properties";//配置文件後綴

    static Logger cat = Logger.getLogger(DispatchSocketServer.class);

 

    static DispatchSocketServer server;

static int port;

// key=application, value=hierarchy

    Hashtable<String, Hierarchy> hierarchyMap;

    String dir;

 

    public static void main(String argv[]) {

            if (argv.length == 2)

                    init(argv[0], argv[1]);

            else

                    usage("Wrong number of arguments.");

            try {

                    cat.info("Listening on port " + port);

                    ServerSocket serverSocket = new ServerSocket(port);

                    while (true) {

                            cat.info("Waiting to accept a new client.");

                            Socket socket = serverSocket.accept();

                            InetAddress inetAddress = socket.getInetAddress();

                            cat.info("Connected to client at " + inetAddress);

                            cat.info("Starting new socket node.");

                            new Thread(new SocketNode(socket, server.hierarchyMap)).start();

                    }

            } catch (Exception e) {

                    e.printStackTrace();

            }

    }

 

    static void usage(String msg) {

            System.err.println(msg);

            System.err.println("Usage: java " + DispatchSocketServer.class.getName() + " port configFile directory");

            System.exit(1);

    }

   

    static void initLogFilePath(String configDir){

            String configpath = configDir + File.separator + "logroot.properties";//路徑配置文件

           Properties prop = new Properties();

            try {

               InputStream in = new FileInputStream(configpath);

               prop.load(in);

               in.close();

           } catch (IOException e) {

               e.printStackTrace();

           }

//logroot.properties中的值存入系統變量

            Set keys = prop.keySet();

            for(Object key:keys){

               String k = (String)key;

               String v = (String)prop.getProperty(k);

               System.setProperty(k, v);

           }

    }

 

    static void init(String srvPort, String configDir) {

            initLogFilePath(configDir);

            try {

                    port = Integer.parseInt(srvPort);

            } catch (java.lang.NumberFormatException e) {

                    e.printStackTrace();

                    usage("Could not interpret port number [" + srvPort + "].");

            }

 

            PropertyConfigurator.configure(configDir + File.separator + "socketserver.properties");//

            server = new DispatchSocketServer(configDir);

    }

 

    public DispatchSocketServer(String configDir) {

            this.dir = configDir;

            hierarchyMap = new Hashtable<String, Hierarchy>(11);

            configureHierarchy();

    }

 

 

 

    // This method assumes that there is no hiearchy for inetAddress

 

    // yet. It will configure one and return it.

 

    void configureHierarchy() {

            File configFile = new File(dir + File.separator + CLIENT_DIR);

            if (configFile.exists() && configFile.isDirectory()) {

                    String[] clients = configFile.list();

                    for (int i = 0; i < clients.length; i++) {

                            File client = new File(dir + File.separator + CLIENT_DIR + File.separator + clients[i]);

                            if (client.isFile()) {

                                    Hierarchy h = new Hierarchy(new RootLogger(Level.DEBUG));

                                    String application = clients[i].substring(0, clients[i].indexOf("."));

                                    cat.info("Locating configuration file for " + application);

                                    hierarchyMap.put(application, h);

                              //這個方法需要修改源碼

                                    new PropertyConfigurator().doConfigure(client.getAbsolutePath(), h, clients[i].substring(0, clients[i].lastIndexOf(".")));

                            }

                    }

            }

    }

}

SocketNode這個類負責處理特定終端發來的日誌

package com.surdoc.log4j.extend.server;

 

import java.io.BufferedInputStream;

import java.io.File;

import java.io.IOException;

import java.io.InputStream;

import java.io.ObjectInputStream;

import java.net.Socket;

import java.util.Hashtable;

import java.util.Properties;

 

import org.apache.log4j.Hierarchy;

import org.apache.log4j.Logger;

import org.apache.log4j.spi.LoggingEvent;

 

public class SocketNode implements Runnable{

    Socket socket;

    ObjectInputStream ois;

    Hashtable<String, Hierarchy> hashtable;

    static Logger logger = Logger.getLogger(SocketNode.class);

 

    public SocketNode(Socket socket, Hashtable<String, Hierarchy> hashtable) {

            this.socket = socket;

            this.hashtable = hashtable;

            try {

                    ois = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));

            } catch (Exception e) {

                    logger.error("Could not open ObjectInputStream to " + socket, e);

            }

    }

 

    public void run() {

            LoggingEvent event;

            Logger remoteLogger;

            try {

                    if (ois != null) {

                            while (true) {

                                    // read an event from the wire

                                    event = (LoggingEvent) ois.readObject();

                                    Object application = event.getMDC("application");

                                    if (application != null) {

                                            // get a logger from the hierarchy. The name of the

                                            // logger

                                            // is taken to be the name contained in the event.

                                            remoteLogger = hashtable.get(application).getLogger(event.getLoggerName());

                                            // logger.info(remoteLogger.getAppender(application.toString()));

                                            // event.logger = remoteLogger;

                                            // apply the logger-level filter

                                            if (remoteLogger != null && event.getLevel().isGreaterOrEqual(remoteLogger.getEffectiveLevel())) {

                                                    // finally log the event as if was generated locally

                                                    remoteLogger.callAppenders(event);

                                            }

                                    }

                            }

                    }

            } catch (java.io.EOFException e) {

                    logger.info("Caught java.io.EOFException closing conneciton.");

            } catch (java.net.SocketException e) {

                    logger.info("Caught java.net.SocketException closing conneciton.");

            } catch (IOException e) {

                    logger.info("Caught java.io.IOException: " + e);

                    logger.info("Closing connection.");

            } catch (Exception e) {

                    logger.error("Unexpected exception. Closing conneciton.", e);

            } finally {

                    if (ois != null) {

                            try {

                                    ois.close();

                            } catch (Exception e) {

                                    logger.info("Could not close connection.", e);

                            }

                    }

                    if (socket != null) {

                            try {

                                    socket.close();

                            } catch (IOException ex) {

                            }

                    }

            }

    }

}

找到org.apache.log4j. PropertyConfigurator這個類,在其中找到下邊的方法:

Public void doConfigure(String configFileName, LoggerRepository hierarchy) {

    Properties props = new Properties();

    FileInputStream istream = null;

    try {

      istream = new FileInputStream(configFileName);

      props.load(istream);

      istream.close();

    }

    catch (Exception e) {

      if (e instanceof InterruptedIOException || e instanceof InterruptedException) {

          Thread.currentThread().interrupt();

      }

      LogLog.error("Could not read configuration file ["+configFileName+"].", e);

      LogLog.error("Ignoring configuration file [" + configFileName+"].");

      return;

    } finally {

        if(istream != null) {

            try {

                istream.close();

            } catch(InterruptedIOException ignore) {

                Thread.currentThread().interrupt();

            } catch(Throwable ignore) {

            }

 

        }

    }

    // If we reach here, then the config file is alright.

    doConfigure(props, hierarchy);

  }

注意在DispatchSocketServer中如下的調用:

new PropertyConfigurator().doConfigure(client.getAbsolutePath(), h, clients[i].substring(0, clients[i].lastIndexOf(".")));

有三個參數,而上邊的方法只有兩個,下面我們就要添加這個方法:

Public void doConfigure(String configFileName, LoggerRepository hierarchy, String application) { //application就是log4j.appender.Server.application對應的值

    Properties props = new Properties();

    FileInputStream istream = null;

    try {

      istream = new FileInputStream(configFileName);

      props.load(istream);

      istream.close();

      Set keys = props.keySet();

      for(Object key:keys){

      String k = (String)key;

      String v = props.getProperty(k);

// 不同的配置文件中都可以用${LOGBASEPATH}來引用對應的日誌存放路徑

if(v.indexOf("${LOGBASEPATH}")!=-1){

          String base = System.getProperty(application);

          if(base==null || base.equals(""))

              throw new RuntimeException("Base path for "+application+"is not exist!!!");

          v = v.replaceAll("\\$\\{LOGBASEPATH\\}", base);

          props.setProperty(k, v);

      }

      }

    }

    catch (Exception e) {

      if (e instanceof InterruptedIOException || e instanceof InterruptedException) {

          Thread.currentThread().interrupt();

      }

      LogLog.error("Could not read configuration file ["+configFileName+"].", e);

      LogLog.error("Ignoring configuration file [" + configFileName+"].");

      return;

    } finally {

        if(istream != null) {

            try {

                istream.close();

            } catch(InterruptedIOException ignore) {

                Thread.currentThread().interrupt();

            } catch(Throwable ignore) {

            }

 

        }

    }

    // If we reach here, then the config file is alright.

    doConfigure(props, hierarchy);

  }

 服務器端到這裏就寫完了,將整個程序編譯後打成jar包,名字可以是DispatchLogServer.jar,然後建立一個文件夾,比如logserver,將jar包放進去

然後建立logs/test文件夾,和config/test文件夾,把上邊說的logroot.properties放在config文件夾下,在這裏再建立一個配置文件socketserver.properties,負責日誌服務器的日誌輸出,內容如下:

log4j.rootCategory=INFO, STDOUT

 

log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender

log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout

log4j.appender.STDOUT.layout.ConversionPattern=[%d{yyyy-MM-dd HH\:mm\:ss}][%5p][%5t][%l] %m%n

在config/test文件夾下建立test對應的log4j配置:

log4j.rootLogger=WARN,Console

 

log4j.appender.Console=org.apache.log4j.ConsoleAppender 

log4j.appender.Console.layout=org.apache.log4j.PatternLayout

log4j.appender.Console.layout.ConversionPattern=%d{yy-MM-dd HH:mm:ss} %5p %c{1}:%L - %m%n

 

log4j.appender.Catalina=org.apache.log4j.DailyRollingFileAppender 

log4j.appender.Catalina.File=${LOGBASEPATH}/catalina.log  

log4j.appender.Catalina.layout=org.apache.log4j.PatternLayout 

log4j.appender.Catalina.layout.ConversionPattern=%d{yyyy.MM.dd HH:mm:ss} %5p %c{1}(%L):? %m%n

 

log4j.appender.Manager=org.apache.log4j.DailyRollingFileAppender 

log4j.appender.Manager.File=${LOGBASEPATH}/manager.log

log4j.appender.Manager.layout=org.apache.log4j.PatternLayout 

log4j.appender.Manager.layout.ConversionPattern=%d{yyyy.MM.dd HH:mm:ss} %5p %c{1}(%L):? %m%n

 

log4j.appender.HostManager=org.apache.log4j.DailyRollingFileAppender 

log4j.appender.HostManager.File=${LOGBASEPATH}/host-manager.log

log4j.appender.HostManager.layout=org.apache.log4j.PatternLayout 

log4j.appender.HostManager.layout.ConversionPattern=%d{yyyy.MM.dd HH:mm:ss} %5p %c{1}(%L):? %m%n

 

log4j.appender.AccessLog=org.apache.log4j.DailyRollingFileAppender 

log4j.appender.AccessLog.File=${LOGBASEPATH}/accesslog.log

log4j.appender.AccessLog.layout=org.apache.log4j.PatternLayout 

log4j.appender.AccessLog.layout.ConversionPattern=%m%n

 

log4j.appender.R=org.apache.log4j.DailyRollingFileAppender

log4j.appender.R.File=${LOGBASEPATH}/webdocbaseLog.log

log4j.appender.R.DatePattern = '.'yyyy-MM-dd

log4j.appender.R.layout=org.apache.log4j.PatternLayout

log4j.appender.R.layout.ConversionPattern=%-d{yy/MM/dd HH:mm} [%c:%L]-[%p] %m%n

 

#Logger

log4j.logger.org.apache=INFO, Catalina

log4j.logger.org.apache.catalina.core=INFO, Catalina

log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost]=INFO, Catalina

log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager]=INFO, Manager

log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager]=INFO, HostManager

log4j.logger.accessLog=INFO, AccessLog

 

log4j.logger.com.sursen.webdocbase=INFO,R

注意和前邊的log4j-server.properties的區別,我們可以在裏邊通過${LOGBASEPATH}引用test對應的日誌存儲根路徑。

爲了方便運行我們在logserver文件夾下建立一個啓動腳本startup.bat內容:

java -classpath DispatchLogServer.jar com.surdoc.log4j.extend.server.DispatchSocketServer 4712 config

啓動tomcat,可以看到tomcat下的logs文件夾裏空空如也,而日誌全都傳輸到了服務器的logs/test文件夾下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章