log4j.xml里实现读取变量(spring容器篇)
- 需求背景
公司日志系统不太完善,集群的服务器日志散落在不同的服务器上,要从公司层面解决这个问题不太现实,目前的项目进度也不允许,当前系统会有很多对接,有上游下游,生产环境调试看日志太麻烦了, 所以准备自己动手做一个workaround。
另一方面,公司开发、上线有好几套环境,不可能每套环境都去改一次,用变量可以把对应信息写在zookeeper里,这样就可以一劳永逸了。
2. 前期调研
之前用过mongodb,而且capped collection用来做非永久性日志非常合适,于是就从这方面入手。四处找了找下来分析下来觉得还是用log4j来把日志输入,一方面这个 org.log4mongo 包装了写mongodb的所有方面用起来方便,另一方面,一个logger.info就可以做到这样的效果对代码的入侵是很小的。
3. 说干咱就干
首先pom里引入所需要的jar包,这里注意log4mongo只适用log4j 版本1.2.15 及以上。
<!-- for MongoDbAppender start --> <dependency> <groupId>org.mongodb</groupId> <artifactId>mongo-java-driver</artifactId> <version>2.12.4</version> </dependency> <dependency> <groupId>org.log4mongo</groupId> <artifactId>log4mongo-java</artifactId> <version>0.7.4</version> </dependency> <!-- for MongoDbAppender end --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.15</version> </dependency>
log4j.xml里加入以下:
<appender name="mongoDBAppender" class="org.log4mongo.MongoDbAppender"> <param name="hostname" value="${hostName}" /> <param name="port" value="${port}" /> <param name="databaseName" value="${databaseName}" /> <param name="collectionName" value="${collectionName}" /> </appender> <appender name="asynAppender" class="org.apache.log4j.AsyncAppender"> <param name="bufferSize" value="100000" /> <appender-ref ref="mongoDBAppender" /> </appender> <logger name="com.xxx.xxx" additivity="false"> <level value="INFO" /> <appender-ref ref="CONSOLE" /> <appender-ref ref="asynAppender" /> </logger>
注意上面的log4j.xml里我用到了类似这样的 ${hostName} 变量读取。
这里,查阅了google , stackoverflow,查看源代码会看到
org.apache.log4j.helpers.OptionConverter 里
static String DELIM_START = "${"; static char DELIM_STOP = '}'; /** Very similar to <code>System.getProperty</code> except that the {@link SecurityException} is hidden. @param key The key to search for. @param def The default value to return. @return the string value of the system property, or the default value if there is no property with that key. @since 1.1 */ public static String getSystemProperty(String key, String def) { try { return System.getProperty(key, def); } catch(Throwable e) { // MS-Java throws com.ms.security.SecurityExceptionEx LogLog.debug("Was not allowed to read system property \""+key+"\"."); return def; } }
从上面的代码里可以看出,log4j的处理逻辑会去解析xml里的 ${} 变量,之后会去取System的property。
也就是说,log4j.xml里是可以用变量去取代的。OK,既然能读我们就去读。
那么问题来了。。。。。。
问题1:System的property里面是没有我们需要的hostName的
Easy,没有就塞进去呗,System.setProperty("hostName","11.11.11.11") 注意区分大小写
好了,现在系统变量里面也有了,总应该OK了吧。
问题2:因为我们用的是Spring容器,System.setProperty("hostName","11.11.11.11")放在哪里才能让log4j读到呢?(有人会说在tomcat启动参数里加,确实可行,但是,你总不能Ip变了要去改启动参数吧,扩展性不好)
Easy,因为log4j一般都会配置成由Spring listener来启动,会先于整个spring框架先启动,所以,最开始log4j启动的时候 mongoDbAppender是读不到hostName等属性的,那么我们就监听Spring容器,等容器里所有的bean都初始化好了的时间结点上去让Log4j重新读一遍配置文件,这样我的设置的System propery就可以被log4j读到。
说干就干 ,看了一下log4j的源码,启动读取log4j.xml或者log4j.properties是由
org.apache.log4j.xml.DOMConfigurator
org.apache.log4j.PropertyConfigurator
两个类来实现的,根据log4j官网的api描述
The PropertyConfigurator does not handle the advanced configuration features supported by the DOMConfigurator such as support custom ErrorHandlers, nested appenders such as the AsyncAppender, etc.
PropertyConfigurator 不能像 DOMConfigurator那样处理像 AsyncAppender 这样的,所以,一般的
log4j要实现高级一些功能还是用 log4j.xml。
https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/PropertyConfigurator.html
上代码(这里之所以要用变量,是因为公司开发有好几套环境,这样的情况下不要每次上环境都改xml,只要在zoomkeeper里配置好就可以了)
@Component public class MongoDbLoggerInitializer implements ApplicationListener { public final String LOG_LOCATIONS = "/log/log4j.xml"; public void reloadLog4j() { Logger.info(this, "load lion properties", ""); System.setProperty("hostName","11.11.11.11"); System.setProperty("port","2121"); System.setProperty("databaseName","xxx"); System.setProperty("collectionName","xxx"); URL url = this.getClass().getResource(LOG_LOCATIONS); if (url == null) { Logger.info(this, "No resources found for [%s]!", LOG_LOCATIONS); return; } String path = url.toString(); if (StringUtils.isNotEmpty(path)) { path = path.substring(path.indexOf(":") + 1, path.length()); Logger.info(this, "DOMConfigurator.configure--start path=%s", path); DOMConfigurator.configure(path); Logger.info(this, "DOMConfigurator.configure--finished", ""); } } @Override public void onApplicationEvent(ApplicationEvent event) { // 容器启动完成之后load if (event instanceof ContextRefreshedEvent) { if (((ContextRefreshedEvent) event).getApplicationContext().getParent() == null) { reloadLog4j(); } } } }
关键点 DOMConfigurator.configure(path);
到此,目的达到了,想要的功能可以了。
上个mongodb的数据
{ "_id" : ObjectId("54b4fa4de4b011c54ede7f85"), "timestamp" : ISODate("2015-01-13T10:58:21.584Z"), "level" : "INFO", "thread" : "main", "message" : "MongoDBAppenderInitialize ----------", "loggerName" : { "fullyQualifiedClassName" : "com.xx.xx.MongoDBAppenderInitialize", "package" : ["xx", "xx", "xx", "xx", "xx", "xx", "xx", "MongoDBAppenderInitialize"], "className" : "MongoDBAppenderInitialize" }, "fileName" : "?", "method" : "?", "lineNumber" : "?", "class" : { "fullyQualifiedClassName" : "?", "package" : ["?"], "className" : "?" }, "host" : { "process" : "[email protected]", "name" : "xxx", "ip" : "127.0.0.1" } }
数据里还有host相关信息,对之后的查错提供帮助。
问题3. 用mongodb来做log,如果mongodb挂了,会不会影响到整个工程
这一点,我自己在机子上测试过,当我把mongodb服务器shutdown后,tomcat这边只会抛出一个Connect timeout的exception , 后续的log不会每次都抛出这个exception。
看了一下代码原来是做了一个errorHandler只会抛出第一次的exception,可以借鉴到以后的代码里。
org.apache.log4j.helpers.OnlyOnceErrorHandler
public void error(String message, Exception e, int errorCode, LoggingEvent event) { if (e instanceof InterruptedIOException || e instanceof InterruptedException) { Thread.currentThread().interrupt(); } if(firstTime) { LogLog.error(message, e); firstTime = false; } }
MongoDbAppender还是会每次去连接
org.log4mongo.MongoDbAppender
@Override public void append(DBObject bson) { if (initialized && bson != null) { try { getCollection().insert(bson, getConcern()); } catch (MongoException e) { errorHandler.error("Failed to insert document to MongoDB", e, ErrorCode.WRITE_FAILURE); } } }
写在后面的话:
1.
调试过程中遇到了很多的坑,原来tomcat里的catalina.out 文件会输出一些你其它日志里看不到的Runtime exception信息(不会记录到logger里),对调试错误很有帮助。
比如,我去getResource(log/log4j.xml) 的时候,如果文件拿不到会有IllegalArgumentException,
javax.xml.parsers.DocumentBuilder.parse(File)
public Document parse(File f) throws SAXException, IOException { if (f == null) { throw new IllegalArgumentException("File cannot be null"); } //convert file to appropriate URI, f.toURI().toASCIIString() //converts the URI to string as per rule specified in //RFC 2396, InputSource in = new InputSource(f.toURI().toASCIIString()); return parse(in); }
2.
还有就是,调试的时候,可以把log4j的debug信息打出来
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" threshold="null" debug="true">
3.
重新认识了System下的property和env
* <p><a name="EnvironmentVSSystemProperties"><i>System * properties</i> and <i>environment variables</i></a> are both * conceptually mappings between names and values. Both * mechanisms can be used to pass user-defined information to a * Java process. Environment variables have a more global effect, * because they are visible to all descendants of the process * which defines them, not just the immediate Java subprocess. * They can have subtly different semantics, such as case * insensitivity, on different operating systems. For these * reasons, environment variables are more likely to have * unintended side effects. It is best to use system properties * where possible. Environment variables should be used when a * global effect is desired, or when an external system interface * requires an environment variable (such as <code>PATH</code>). * * <p>On UNIX systems the alphabetic case of <code>name</code> is * typically significant, while on Microsoft Windows systems it is * typically not. For example, the expression * <code>System.getenv("FOO").equals(System.getenv("foo"))</code> * is likely to be true on Microsoft Windows.
从JDK文档里写的来看,property可以看成是jvm里的参数,而env则是操作系统级别的,不同的操作系统还可以大小写不敏感。
所以,更推荐使用property.
It is best to use system properties where possible.
好了,中午没睡觉把这个写了,好困。。。
参考链接:
http://blog.sina.com.cn/s/blog_4b81125f0100fo95.html