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 "%r" %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 "%r" %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文件夹下。