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" : "15703@xx.xx.xx",
"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