随着项目的逐渐扩大,日志的增加也变得更快。log4j是常用的日志记录工具,在有些时候,我们可能需要将log4j的日志发送到专门用于记录日志的远程服务器,特别是对于稍微大一点的应用。这么做的优点有:
可以集中管理日志:可以把多台服务器上的日志都发送到一台日志服务器上,方便管理、查看和分析
可以减轻服务器的开销:日志不在服务器上了,因此服务器有更多可用的磁盘空间
可以提高服务器的性能:通过异步方式,记录日志时服务器只负责发送消息,不关心日志记录的时间和位置,服务器甚至不关心日志到底有没有记录成功
远程打印日志的原理:项目a需要打印日志,而a调用log4j来打印日志,log4j的jmsappender又给配置的地址(activemq地址)发送一条jms消息,此时绑定在queue上的项目b的监听器发现有消息到来,于是立即唤醒监听器的方法开始输出日志。
本文将使用两个java项目product和logging,其中product项目就是模拟线上的项目,而logging项目模拟运行在专用的日志服务器上的项目。说明:本文的例子是在windows平台下。
2. 解压后不需要任何配置,进入到bin下对应的系统架构文件夹
3. 双击activemq.bat启动,如果看到类似下面的页面,就代表activemq启动好了:
然后打开浏览器,输入地址:http://localhost:8161进入管理页面,用户名admin,密码admin:
可以点击manage activemq broker进入queue的查看界面。
我用maven来管理项目,方便维护各种依赖的jar包。先看下项目结构:
项目不复杂,主要是4个文件:pom.xml,main.java,log4j.properties和jndi.properties
pom.xml中主要是声明项目的依赖包,其余没有什么东西了:
<a href="http://my.oschina.net/itblog/blog/533730#">?</a>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<code><!-- use to call write log methods --></code>
<code><</code><code>dependency</code><code>></code>
<code> </code><code><</code><code>groupid</code><code>>log4j</</code><code>groupid</code><code>></code>
<code> </code><code><</code><code>artifactid</code><code>>log4j</</code><code>artifactid</code><code>></code>
<code> </code><code><</code><code>version</code><code>>1.2.17</</code><code>version</code><code>></code>
<code></</code><code>dependency</code><code>></code>
<code><!-- log4j uses this lib --></code>
<code> </code><code><</code><code>groupid</code><code>>org.slf4j</</code><code>groupid</code><code>></code>
<code> </code><code><</code><code>artifactid</code><code>>slf4j-log4j12</</code><code>artifactid</code><code>></code>
<code> </code><code><</code><code>version</code><code>>1.7.13</</code><code>version</code><code>></code>
<code><!-- spring jms lib --></code>
<code> </code><code><</code><code>groupid</code><code>>org.springframework</</code><code>groupid</code><code>></code>
<code> </code><code><</code><code>artifactid</code><code>>spring-jms</</code><code>artifactid</code><code>></code>
<code> </code><code><</code><code>version</code><code>>4.0.0.release</</code><code>version</code><code>></code>
<code><!-- activemq lib --></code>
<code> </code><code><</code><code>groupid</code><code>>org.apache.activemq</</code><code>groupid</code><code>></code>
<code> </code><code><</code><code>artifactid</code><code>>activemq-core</</code><code>artifactid</code><code>></code>
<code> </code><code><</code><code>version</code><code>>5.7.0</</code><code>version</code><code>></code>
main.java:
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<code>package</code> <code>com.demo.product;</code>
<code>import</code> <code>javax.jms.connection;</code>
<code>import</code> <code>javax.jms.destination;</code>
<code>import</code> <code>javax.jms.message;</code>
<code>import</code> <code>javax.jms.messageconsumer;</code>
<code>import</code> <code>javax.jms.messagelistener;</code>
<code>import</code> <code>javax.jms.session;</code>
<code>import</code> <code>org.apache.activemq.activemqconnectionfactory;</code>
<code>import</code> <code>org.apache.activemq.command.activemqobjectmessage;</code>
<code>import</code> <code>org.apache.log4j.logger;</code>
<code>import</code> <code>org.apache.log4j.spi.loggingevent;</code>
<code>public</code> <code>class</code> <code>main </code><code>implements</code> <code>messagelistener {</code>
<code> </code>
<code> </code><code>public</code> <code>main() </code><code>throws</code> <code>exception {</code>
<code> </code><code>// create consumer and listen queue</code>
<code> </code><code>activemqconnectionfactory factory = </code>
<code> </code><code>new</code> <code>activemqconnectionfactory(</code><code>"tcp://localhost:61616"</code><code>);</code>
<code> </code><code>connection connection = factory.createconnection();</code>
<code> </code><code>session session = connection.createsession(</code><code>false</code><code>, session.auto_acknowledge);</code>
<code> </code><code>connection.start();</code>
<code> </code><code>//////////////注意这里jmsappender只支持topicdestination,下面会说到////////////////</code>
<code> </code><code>destination topicdestination = session.createtopic(</code><code>"logtopic"</code><code>);</code>
<code> </code><code>messageconsumer consumer = session.createconsumer(topicdestination);</code>
<code> </code><code>consumer.setmessagelistener(</code><code>this</code><code>);</code>
<code> </code>
<code> </code><code>// log a message</code>
<code> </code><code>logger logger = logger.getlogger(main.</code><code>class</code><code>);</code>
<code> </code><code>logger.info(</code><code>"info log."</code><code>);</code>
<code> </code><code>logger.warn(</code><code>"warn log"</code><code>);</code>
<code> </code><code>logger.error(</code><code>"error log."</code><code>);</code>
<code> </code><code>// clean up</code>
<code> </code><code>thread.sleep(</code><code>1000</code><code>);</code>
<code> </code><code>consumer.close();</code>
<code> </code><code>session.close();</code>
<code> </code><code>connection.close();</code>
<code> </code><code>system.exit(</code><code>1</code><code>);</code>
<code> </code><code>}</code>
<code> </code><code>public</code> <code>static</code> <code>void</code> <code>main(string[] args) </code><code>throws</code> <code>exception {</code>
<code> </code><code>new</code> <code>main();</code>
<code> </code><code>public</code> <code>void</code> <code>onmessage(message message) {</code>
<code> </code><code>try</code> <code>{</code>
<code> </code><code>// receive log event in your consumer</code>
<code> </code><code>loggingevent event = (loggingevent)((activemqobjectmessage)message).getobject();</code>
<code> </code><code>system.out.println(</code><code>"received log ["</code> <code>+ event.getlevel() + </code><code>"]: "</code><code>+ event.getmessage());</code>
<code> </code><code>} </code><code>catch</code> <code>(exception e) {</code>
<code> </code><code>e.printstacktrace();</code>
<code> </code><code>}</code>
<code>}</code>
说明:然后是log4j.properties:
<code>log4j.rootlogger=info, stdout, jms</code>
<code> </code>
<code>## be sure that activemq messages are not logged to 'jms' appender</code>
<code>log4j.logger.org.apache.activemq=info, stdout</code>
<code>log4j.appender.stdout=org.apache.log4j.consoleappender</code>
<code>log4j.appender.stdout.layout=org.apache.log4j.patternlayout</code>
<code>log4j.appender.stdout.layout.conversionpattern=%d %-5p %c - %m%n</code>
<code>## configure 'jms' appender. you'll also need jndi.properties file in order to make it work</code>
<code>log4j.appender.jms=org.apache.log4j.net.jmsappender</code>
<code>log4j.appender.jms.initialcontextfactoryname=org.apache.activemq.jndi.activemqinitialcontextfactory</code>
<code>log4j.appender.jms.providerurl=tcp://localhost:61616</code>
<code>log4j.appender.jms.topicbindingname=logtopic</code>
<code>log4j.appender.jms.topicconnectionfactorybindingname=connectionfactory</code>
其实按理说只需要这么三个文件就可以了,但是这时候执行会报错:
<code>javax.naming.namenotfoundexception: logtopic</code>
<code> </code><code>at org.apache.activemq.jndi.readonlycontext.lookup(readonlycontext.java:</code><code>235</code><code>)</code>
<code> </code><code>at javax.naming.initialcontext.lookup(unknown source)</code>
<code> </code><code>at org.apache.log4j.net.jmsappender.lookup(jmsappender.java:</code><code>245</code><code>)</code>
<code> </code><code>at org.apache.log4j.net.jmsappender.activateoptions(jmsappender.java:</code><code>222</code><code>)</code>
<code> </code><code>at org.apache.log4j.config.propertysetter.activate(propertysetter.java:</code><code>307</code><code>)</code>
<code> </code><code>...</code>
<code> </code><code>at org.apache.activemq.activemqprefetchpolicy.<clinit>(activemqprefetchpolicy.java:</code><code>39</code><code>)</code>
<code> </code><code>at org.apache.activemq.activemqconnectionfactory.<init>(activemqconnectionfactory.java:</code><code>84</code><code>)</code>
<code> </code><code>at org.apache.activemq.activemqconnectionfactory.<init>(activemqconnectionfactory.java:</code><code>137</code><code>)</code>
<code> </code><code>at com.demo.product.main.<init>(main.java:</code><code>20</code><code>)</code>
<code> </code><code>at com.demo.product.main.main(main.java:</code><code>43</code><code>)</code>
为什么会报错呢?来看看jmsappender的javadoc文档,它是这么描述的:
大意是说,jmsappender需要一个jndi配置来初始化一个jndi上下文(context)。因为有了这个上下文才能管理jms topic和topic的连接。于是为项目配置一个叫jndi.properties的文件,其内容为:
<code>topic.logtopic=logtopic</code>
然后再运行就不会报错了。我们先来看看activemq(注意切换到topic标签页下):
可以看到,主题为logtopic的消息,有3条进queue,这3条也出queue了。而出queue的消息,已经被我们的监听器收到并打印出来了:
需要注意的是,本例只是一个很简单的例子,目的是阐明远程打印日志的原理。实际项目中,一般日志服务器上运行着的,不是项目,而是专用的日志记录器。下面,我们就把这个项目拆分成两个项目,并用spring来管理这些用到的bean
修改后的product的项目结构并没有改变,改变的只是main类:
<code>public</code> <code>class</code> <code>main{</code>
<code> </code><code>private</code> <code>static</code> <code>final</code> <code>logger logger = logger.getlogger(main.</code><code>class</code><code>);</code>
<code> </code><code>// just log a message</code>
<code> </code><code>system.exit(</code><code>0</code><code>);</code>
这个main类和普通的logger调用一样,仅仅负责打印日志。有没有觉得太简单了呢?
来看看项目结构图:
为了让监听器一直活着,我把logging写成了一个web项目,跑在tomcat上。index.jsp就是个hello world字符串而已,用来验证logging活着。注意,在logging项目中,已没有product项目中的log4j.properties和jndi.properties两个文件。
来看看另外几个文件:
pom.xml(每个包的目的都写在注释里了):
<code><!-- use to cast object to logevent when received a log --></code>
<code><!-- use to receive jms message --></code>
<code><!-- use to load spring.xml --></code>
<code> </code><code><</code><code>artifactid</code><code>>spring-web</</code><code>artifactid</code><code>></code>
web.xml
<code><!</code><code>doctype</code> <code>web-app public</code>
<code> </code><code>"-//sun microsystems, inc.//dtd web application 2.3//en"</code>
<code> </code><code>"http://java.sun.com/dtd/web-app_2_3.dtd" ></code>
<code><</code><code>web-app</code><code>></code>
<code> </code><code><</code><code>context-param</code><code>></code>
<code> </code><code><</code><code>param-name</code><code>>contextconfiglocation</</code><code>param-name</code><code>></code>
<code> </code><code><</code><code>param-value</code><code>>classpath:spring.xml</</code><code>param-value</code><code>></code>
<code> </code><code></</code><code>context-param</code><code>></code>
<code> </code><code><!-- use to load spring.xml --></code>
<code> </code><code><</code><code>listener</code><code>></code>
<code> </code><code><</code><code>listener-class</code><code>></code>
<code> </code><code>org.springframework.web.context.contextloaderlistener</code>
<code> </code><code></</code><code>listener-class</code><code>></code>
<code> </code><code></</code><code>listener</code><code>></code>
<code> </code><code><</code><code>welcome-file-list</code><code>></code>
<code> </code><code><</code><code>welcome-file</code><code>>index.jsp</</code><code>welcome-file</code><code>></code>
<code> </code><code></</code><code>welcome-file-list</code><code>></code>
<code></</code><code>web-app</code><code>></code>
spring.xml
<code><?</code><code>xml</code> <code>version</code><code>=</code><code>"1.0"</code> <code>encoding</code><code>=</code><code>"utf-8"</code><code>?></code>
<code><</code><code>beans</code> <code>xmlns</code><code>=</code><code>"http://www.springframework.org/schema/beans"</code>
<code> </code><code>xmlns:xsi</code><code>=</code><code>"http://www.w3.org/2001/xmlschema-instance"</code>
<code> </code><code>xsi:schemalocation="</code>
<code> </code><code>http://www.springframework.org/schema/beans </code>
<code> </code><code>http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"></code>
<code> </code><code><</code><code>bean</code> <code>id</code><code>=</code><code>"jmstemplate"</code> <code>class</code><code>=</code><code>"org.springframework.jms.core.jmstemplate"</code><code>></code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"connectionfactory"</code> <code>ref</code><code>=</code><code>"connectionfactory"</code><code>/></code>
<code> </code><code></</code><code>bean</code><code>></code>
<code> </code><code><</code><code>bean</code> <code>id</code><code>=</code><code>"connectionfactory"</code> <code>class</code><code>=</code><code>"org.springframework.jms.connection.singleconnectionfactory"</code><code>></code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"targetconnectionfactory"</code> <code>ref</code><code>=</code><code>"targetconnectionfactory"</code><code>/></code>
<code> </code><code><</code><code>bean</code> <code>id</code><code>=</code><code>"targetconnectionfactory"</code> <code>class</code><code>=</code><code>"org.apache.activemq.activemqconnectionfactory"</code><code>></code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"brokerurl"</code> <code>value</code><code>=</code><code>"tcp://localhost:61616"</code><code>/></code>
<code><!-- as jmsappender only support the topic way to send messages, </code>
<code> </code><code>thus queuedestination here is useless.</code>
<code> </code><code><bean id="queuedestination" class="org.apache.activemq.command.activemqqueue"></code>
<code> </code><code><constructor-arg name="name" value="queue" /></code>
<code> </code><code></bean></code>
<code> </code><code>--></code>
<code> </code><code><</code><code>bean</code> <code>id</code><code>=</code><code>"topicdestination"</code> <code>class</code><code>=</code><code>"org.apache.activemq.command.activemqtopic"</code><code>></code>
<code> </code><code><</code><code>constructor-arg</code> <code>name</code><code>=</code><code>"name"</code> <code>value</code><code>=</code><code>"logtopic"</code> <code>/></code>
<code> </code><code><</code><code>bean</code> <code>id</code><code>=</code><code>"jmscontainer"</code> <code>class</code><code>=</code><code>"org.springframework.jms.listener.defaultmessagelistenercontainer"</code><code>></code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"connectionfactory"</code> <code>ref</code><code>=</code><code>"connectionfactory"</code> <code>/></code>
<code> </code><code><!-- <property name="destination" ref="queuedestination" /> --></code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"destination"</code> <code>ref</code><code>=</code><code>"topicdestination"</code> <code>/></code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"messagelistener"</code> <code>ref</code><code>=</code><code>"logmessagelistener"</code> <code>/></code>
<code> </code><code><</code><code>bean</code> <code>id</code><code>=</code><code>"logmessagelistener"</code> <code>class</code><code>=</code><code>"com.demo.logging.logmessagelistener"</code><code>/></code>
<code></</code><code>beans</code><code>></code>
logmessagelistener指向我们自己实现的日志消息处理逻辑类,topicdestination则关注topic为“logtopic”的消息,而jmscontainer把这两个对象绑在一起,这样就能接收并处理消息了。
最后就是伟大的监听器了logmessagelistener了:
<code>package</code> <code>com.demo.logging;</code>
<code>public</code> <code>class</code> <code>logmessagelistener </code><code>implements</code> <code>messagelistener {</code>
<code> </code><code>system.out.println(</code><code>"logging project: ["</code> <code>+ event.getlevel() + </code><code>"]: "</code><code>+ event.getmessage());</code>
哈哈,说伟大,其实太简单了。但是可以看到,监听器里面就是之前product项目中main类里面移除的实现了messagelistener接口中的代码。
在执行测试前,删掉activemq中所有的queue,确保测试效果。
先运行logging项目,开始queue的监听。再运行product的main类的main函数,可以先看到main类打印到控制台的日志:
接下来去看看queue中的情况:
可以看到有个叫logtopic的主题的消息,进了3条,出了3条。不用想,出queue的3条日志已经被logging项目的listener接收并打印出来了,现在去看看tomcat的控制台:
还要注意queue中的logtopic的consumer数量为1而不是0,这与开始的截图不同。我们都知道这个consumer是logging项目中的logmessagelistener对象,它一直活着,是因为tomcat一直活着;之前的consumer数量为0,是因为在main函数执行完后,queue的监听器(也是写日志的对象)就退出了。
通过把product和logging项目分别放在不同的机器上执行,在第三台机器上部署activemq(当然你可以把activemq搭建在任意可以访问的地方),再配置一下product项目的log4j.properties文件和logging项目的spring.xml文件就能用于生产环境啦。
jmsappender类将loggingevent实例序列化成objectmessage,并将其发送到jms server的一个指定topic中,因此,使用此种将日志发送到远程的方式只支持topic方式发送,不支持queue方式发送。我们再log4j.properties中配置了这一句:
这一句指定了使用的appender,打开这个appender,在里面可以看到很多setter,比如:
这些setter不是巧合,而正是对应了我们在log4j.properties中设置的其他几个选项:
<code>log4j.appender.jms.providerurl=tcp:</code><code>//localhost:61616</code>
来看看jmsappender的activeoptions方法,这个方法是用于使我们在log4j.properties中的配置生效的:
58
59
60
61
62
63
64
65
66
67
68
69
70
<code>/**</code>
<code> </code><code>* options are activated and become effective only after calling this method.</code>
<code> </code><code>*/</code>
<code>public</code> <code>void</code> <code>activateoptions() {</code>
<code> </code><code>topicconnectionfactory topicconnectionfactory;</code>
<code> </code><code>try</code> <code>{</code>
<code> </code><code>context jndi;</code>
<code> </code><code>loglog.debug(</code><code>"getting initial context."</code><code>);</code>
<code> </code><code>if</code> <code>(initialcontextfactoryname != </code><code>null</code><code>) {</code>
<code> </code><code>properties env = </code><code>new</code> <code>properties();</code>
<code> </code><code>env.put(context.initial_context_factory, initialcontextfactoryname);</code>
<code> </code><code>if</code> <code>(providerurl != </code><code>null</code><code>) {</code>
<code> </code><code>env.put(context.provider_url, providerurl);</code>
<code> </code><code>} </code><code>else</code> <code>{</code>
<code> </code><code>loglog.warn(</code><code>"you have set initialcontextfactoryname option but not the "</code>
<code> </code><code>+ </code><code>"providerurl. this is likely to cause problems."</code><code>);</code>
<code> </code><code>}</code>
<code> </code><code>if</code> <code>(urlpkgprefixes != </code><code>null</code><code>) {</code>
<code> </code><code>env.put(context.url_pkg_prefixes, urlpkgprefixes);</code>
<code> </code><code>if</code> <code>(securityprincipalname != </code><code>null</code><code>) {</code>
<code> </code><code>env.put(context.security_principal, securityprincipalname);</code>
<code> </code><code>if</code> <code>(securitycredentials != </code><code>null</code><code>) {</code>
<code> </code><code>env.put(context.security_credentials, securitycredentials);</code>
<code> </code><code>} </code><code>else</code> <code>{</code>
<code> </code><code>loglog.warn(</code><code>"you have set securityprincipalname option but not the "</code>
<code> </code><code>+ </code><code>"securitycredentials. this is likely to cause problems."</code><code>);</code>
<code> </code><code>}</code>
<code> </code><code>jndi = </code><code>new</code> <code>initialcontext(env);</code>
<code> </code><code>} </code><code>else</code> <code>{</code>
<code> </code><code>jndi = </code><code>new</code> <code>initialcontext();</code>
<code> </code><code>loglog.debug(</code><code>"looking up ["</code> <code>+ tcfbindingname + </code><code>"]"</code><code>);</code>
<code> </code><code>topicconnectionfactory = (topicconnectionfactory) lookup(jndi, tcfbindingname);</code>
<code> </code><code>loglog.debug(</code><code>"about to create topicconnection."</code><code>);</code>
<code> </code><code>///////////////////////////////注意这里只会创建topicconnection////////////////////////////</code>
<code> </code><code>if</code> <code>(username != </code><code>null</code><code>) {</code>
<code> </code><code>topicconnection = topicconnectionfactory.createtopicconnection(username, password);</code>
<code> </code><code>topicconnection = topicconnectionfactory.createtopicconnection();</code>
<code> </code><code>loglog.debug(</code><code>"creating topicsession, non-transactional, "</code> <code>+ </code><code>"in auto_acknowledge mode."</code><code>);</code>
<code> </code><code>topicsession = topicconnection.createtopicsession(</code><code>false</code><code>, session.auto_acknowledge);</code>
<code> </code><code>loglog.debug(</code><code>"looking up topic name ["</code> <code>+ topicbindingname + </code><code>"]."</code><code>);</code>
<code> </code><code>topic topic = (topic) lookup(jndi, topicbindingname);</code>
<code> </code><code>loglog.debug(</code><code>"creating topicpublisher."</code><code>);</code>
<code> </code><code>topicpublisher = topicsession.createpublisher(topic);</code>
<code> </code><code>loglog.debug(</code><code>"starting topicconnection."</code><code>);</code>
<code> </code><code>topicconnection.start();</code>
<code> </code><code>jndi.close();</code>
<code> </code><code>} </code><code>catch</code> <code>(jmsexception e) {</code>
<code> </code><code>errorhandler.error(</code><code>"error while activating options for appender named ["</code> <code>+ name + </code><code>"]."</code><code>, e,</code>
<code> </code><code>errorcode.generic_failure);</code>
<code> </code><code>} </code><code>catch</code> <code>(namingexception e) {</code>
<code> </code><code>} </code><code>catch</code> <code>(runtimeexception e) {</code>
上面初始化了一个topicconnection,一个topicsession,一个topicpublisher。咱们再来看看这个appender的append方法:
<code> </code><code>* this method called by {@link appenderskeleton#doappend} method to do most</code>
<code> </code><code>* of the real appending work.</code>
<code>public</code> <code>void</code> <code>append(loggingevent event) {</code>
<code> </code><code>if</code> <code>(!checkentryconditions()) {</code>
<code> </code><code>return</code><code>;</code>
<code> </code><code>objectmessage msg = topicsession.createobjectmessage();</code>
<code> </code><code>if</code> <code>(locationinfo) {</code>
<code> </code><code>event.getlocationinformation();</code>
<code> </code><code>msg.setobject(event);</code>
<code> </code><code>topicpublisher.publish(msg);</code><code>///////////////注意这一句//////////////</code>
<code> </code><code>errorhandler.error(</code><code>"could not publish message in jmsappender ["</code> <code>+ name + </code><code>"]."</code><code>, </code>
<code> </code><code>e, errorcode.generic_failure);</code>
这里使用topicpublisher.publish()方法,把序列化的消息发布出去。可见这也证明了jmsappender只支持以topic方式发送消息。