最近接到两个很小的tickets,两个都是为了项目开发时的方便:一是将logs写入到数据库中,以方便日志的查询;一是在build时,在war包加入svn revision info。
1) logging to database
经过调查,决定采用log4j的org.apache.log4j.jdbc.JDBCAppender,于是采用:
# logging to db
log4j.logger.com.example=DEBUG, DATABASE
log4j.additivity.com.example=false
log4j.appender.DATABASE=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.DATABASE.url=jdbc:postgresql://localhost:5432/test
log4j.appender.DATABASE.driver=org.postgresql.Driver
log4j.appender.DATABASE.user=pguser
log4j.appender.DATABASE.password=post
log4j.appender.DATABASE.sql=INSERT INTO debug_log(created, logger, priority, message) VALUES (to_timestamp('%d{ISO8601}','YYYY-MM-DD HH:MI:SS.MS'),'%c.%M:%L','%p','%m')
log4j.appender.DB.layout=org.apache.log4j.PatternLayout
log4j.appender.DATABASE.layout.ConversionPattern=%d{ISO8601} %p %c.%M:%L %m
很直观,用起来还很方便,但是不久就出现了问题,tomcat抛出了exception。只好把之前fixed ticket reopen,提交新的comments:Unfortunately, org.apache.log4j.jdbc.JDBCAppender that ships with the Log4j distribution is not able to process logging messages that have characters like ' (single quote) and , (comma) in it. When logging messages contains characters like single quote or comma, the program will throw an exception.
重新google了,找到了一个plusjdbc,Looking further, I found an alternative JDBCAppender package (org.apache.log4j.jdbcplus.JDBCAppender) from http://www.dankomannhaupt.de/projects/index.html. It can solve this problem. 长叹了一下。
最后采用:
log4j.appender.DATABASE=org.apache.log4j.jdbcplus.JDBCAppender
log4j.appender.DATABASE.url=jdbc:postgresql://localhost:5432/test
log4j.appender.DATABASE.dbclass=org.postgresql.Driver
log4j.appender.DATABASE.username=pguser
log4j.appender.DATABASE.password=post
log4j.appender.DATABASE.sql=INSERT INTO debug_log(created, logger, priority, message) VALUES (to_timestamp('@LAYOUT:1@', 'YYYY-MM-DD HH:MI:SS.MS'),'@LAYOUT:3@','@LAYOUT:2@','@LAYOUT:4@')
log4j.appender.DATABASE.layout=org.apache.log4j.PatternLayout
log4j.appender.DATABASE.layout.ConversionPattern=%d{ISO8601}#%p#%c.%M:%L#%m
log4j.appender.DATABASE.layoutPartsDelimiter=#
log4j.appender.DATABASE.buffer=1
log4j.appender.DATABASE.commit=true
log4j.appender.DATABASE.quoteReplace=true
问题解决,但是中间有点小波折,在我的项目中,log4j.jar(>1.2.9)重复了,在$CATALINA_HOME/lib下有一份,在web工程下的WEB-INF/lib下也有一份,而plus-jdbc.jar放置在$CATALINA_HOME/lib下,结果启动Tomcat,出现
log4j:ERROR A "org.apache.log4j.jdbcplus.JDBCAppender" object is not assignable to a "org.apache.log4j.Appender" variable.
log4j:ERROR The class "org.apache.log4j.Appender" was loaded by
log4j:ERROR [WebappClassLoader^M
delegate: false^M
repositories:^M
----------> Parent Classloader:^M
org.apache.catalina.loader.StandardClassLoader@1ccb029^M
] whereas object of type
log4j:ERROR "org.apache.log4j.jdbcplus.JDBCAppender" was loaded by [org.apache.catalina.loader.StandardClassLoader@1ccb029].
log4j:ERROR Could not instantiate appender named "DATABASE".
原来是两个JDBCAppender实例不在同一个classlaoder里面,将WEB-INF/lib下的log4j.jar删除掉,重启就没问题了,按理,将$CATALINA_HOME/lib下的plus-jdbc.jar移到WEB-INF/lib下,应该也没问题,没有测试。
2)Add build revision info in war file and read it on tomcat startup
这个经历比较惨痛,两个问题,如何获取revision? And how to read it when tomcat startup? 第二个问题倒是没什么,采用javax.servlet.ServletContextListener就可以实现,很简单,走弯路的是第一个问题,google后发现有两种常见的实现:
As I have learned, there are totally two solutions to get svn revision info.
First, retrieve the svn revision from local file($BASE_HOME/.svn/entries). Just parsing the xml file, get the revision property and write it to a properties file.(就是该死的xml,远在乌克兰的同事,该文件却不是xml的,也只怪自己调研不充分,还得折腾了半天,后来发现,最新版的svn为了performance的考虑,采用meta data来实现entries)
Second, retrieve the svn revision from the remote repository. The solution always use a svn client to perform a demand with remote server to retrieve the revision info. Installing a snv client and using SvnAnt? are most commonly used at present. SvnAnt? is an ant task that provides an interface to Subversion revision control system and encapsulates the svn client. It uses javahl - a native (JNI) java interface for the subversion api if it can find the corresponding library. javahl is platform-dependent.
Because of needing interaction with the server(服务器在国外,更新很慢), now I employ the first solution. But I found a flaw of this method when i was going off duty. Generally, we may update our project with svn before committing. This may make a mismatch with svn revision between remote server and local file. Svn revision in local file is usually updated when we update our project. But when we take a commit after update, the svn revision in the remote server will change to a new one.
So, the case is that if we update, commit, and then build, we may get a mismatch with the newest svn revision, and build the error revision into our ROOT.war. If we update , then build ,without commit, we can get right revision info.
下面是第一版实现:
<!-- retrieve the svn revision from the remote repository
<path id="svnant.lib" >
<fileset dir="${lib.dir}">
<include name="svnant.jar"/>
<include name="svnClientAdapter.jar"/>
<include name="svnjavahl.jar"/>
</fileset>
</path>
<taskdef name="svn" classpathref="svnant.lib" classname="org.tigris.subversion.svnant.SvnTask" />
<target name="get-svn-revision">
<svn username="*******" password="******" javahl="true">
<status urlProperty="https://example.com" path="." revisionProperty="svn.revision" />
</svn>
<echo>svn revision: ${svn.revision}</echo>
</target>
-->
<!-- retrieve the svn revision from local file(.svn/entries). The file may contain several 'wc-entries.entry.revision' elements.
The property will get several values seperated by ',' when using xmlproperty task. Then the svn revison expected will be the
max one of these property values.
-->
<property name="svn.revision.file" value=".svn/entries" />
<!-- This property is used to run xmlproperty task successfully with a low version of svn client (under 1.3.1). Don't sure whether it really makes sense -->
<property name="build.id" value="foo" />
<target name="get-svn-revision">
<xmlproperty file="${svn.revision.file}" collapseAttributes="true"/>
<echo>svn revision: ${wc-entries.entry.revision}</echo>
</target>
<!--
If the file doesn't contain any 'wc-entries.entry.revision' element, the content of the property file will be: revision = ${wc-entries.entry.revision};
If contain a 'wc-entries.entry.revision' element, mark this value as $revision_value, then the content will be: revision = $revision_value;
If contain several 'wc-entries.entry.revision' elements, mark these values as $value1, $value2, ..., respectively, then the content will be: revision = $value1,$value2,..., seperated by a ',';
-->
<property name="svn.revision.propertyfile" value="${build.dir}/revision.properties" />
<target name="write-svn-revision-to-file" depends="get-svn-revision">
<delete file="${svn.revision.propertyfile}"/>
<propertyfile file="${svn.revision.propertyfile}" comment="record svn revision">
<entry key="revision" value="${wc-entries.entry.revision}"/>
</propertyfile>
</target>
结果write-svn-revision-to-file这个在我这倒是可以获取本地的svn revision,但是远方的同事可急了,build老失败,只好把这部分build注释了,还好,到周末了,可以在家好好研究一下,很快找了一个新的工具:
It's my fault. In my version of svn, the entries file is xml formatted. So i parse it using ant task - 'xmlproperty'. Now i have fix this problem by using 'svnkit' tools, a pure java svn toolkit. Now there are two ways to retrieve svn revision. One is from remote repository server. For this one, before building, you should set your own username and password for the remote repository server('remote.repository.username' and 'remote.repository.password' properties in build.xml,respectively). Another one is retrieving revision from local working copy. If using this one, you should set 'local.repository' property in build.xml to your own directory.
利用svnkit,从服务器上获取revision大概是:
repository = SVNRepositoryFactory.create(SVNURL.parseURIDecoded(urlStr));
ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager(username, password);
repository.setAuthenticationManager(authManager);
headRevision = repository.getLatestRevision();
从本地working copy获取revision:
SVNClientManager clientManager = SVNClientManager.newInstance();
SVNWCClient wcClient = clientManager.getWCClient();
SVNInfo info = wcClient.doInfo(new File(fileUrl), SVNRevision.WORKING);
headRevision = info.getRevision().getNumber();
利用ant task将获取的revision写入到一个配置文件中(如revision.properties),在tomcat启动的时候加载进来,就可以了。