使用定制的 Web Service Appender for Log4j 將日志信息發送到某一集中位置。Log4j Appender 使您可以在面向服務架構 (SOA) 解決方案中調試和跟蹤任意問題。
引言
你
可以使用 Web Service Appender 將日志集中到某一位置,同時,Web Service Appender
允許管理者監控、開發者調試面向服務架構(SOA)環境里可能存在的任何問題。Web Service Appender 是一種擴展 JAVA
類,它由 Log4j 的 Appender 類擴展而來。
從定義上看,SOA
是一種彼此可以互相通信的服務集合,但這些服務的內容是各自獨立的,每一類服務均不受其它服務內容或服務狀態的影響,并且這些服務都工作在分布式的系統架
構里。在 SOA 中,Web 服務通常被用來在給定事務中處理請求,這些請求可以是遺留代碼、企業級 Java Beans(EJBs)
的封裝,也可以是 Java
類的封裝,使用一種可以將日志信息聚集在中心位置里的日志紀錄方法,能幫助您隔離缺陷和問題,并能讓你更好的理解邏輯流的處理。
將特定模塊或服務的日志消息紀錄到一個中心位置的機制,可以把可能潛在的問題和缺陷降低到最小。
本文對 Log4j 的功能進行了大體的概述,并介紹了如何編寫自定義的 Log4j Appender,這類特殊的 Appender 將日志消息編到一種特定的 Web 服務。
Log4j 快速入門
Log4j
是一種開放源代碼的日志庫,它已被發展為 Apache Software Foundation 日志服務項目的子項目。該庫是以 IBM 在 90
年代末開發的日志庫為基礎的,第一版發布于 1999 年。現在它在開放源代碼團體得到了廣泛使用,它的體系是圍繞以下三個主要概念構建起來的:
這些概念可以讓您根據消息類型、消息優先級來紀錄消息,您可以控制消息在何處結束及消息如何格式化。
Logger 是應用程序首先調用以初始化消息紀錄的對象。當把某一消息傳遞給日志時,logger 會生成 LoggingEvent
,對消息進行封裝。之后,Logger 對象將 LoggingEvent
傳遞給與之關聯的 Appender。
Appender 將 LoggingEvent
所包含的消息發送給指定的目標輸出文件。所謂指定的文件,大多數情況下,是 Log4 屬性文件。一些 Appender 存在于 Log4j 中。您也可以擴展 Appender,使之支持其它的目標文件,比如 XML 文件、控制臺等等。
在 Log4j 里, LoggingEvent
被賦予某一級別,以表明它們的優先級。缺省的級別包括如下幾種:
- OFF:可能是最高的級別,它是用來關閉日志紀錄的
- FATAL:指出現了非常嚴重的錯誤事件,這些錯誤可能會導致應用程序異常中止
- ERROR:指雖有錯誤,但仍允許應用程序繼續運行
- WARN:指運行環境潛藏著危害
- INFO:指報告信息,這些信息在粗粒度級別上突出顯示應用程序的進程
- DEBUG:指細粒度信息事件,細粒度信息事件對于應用程序的調試是最有用的
- ALL:可能是最低的級別,其目的是打開所有日志記錄
Logger 和 Appender 也被賦予上述的某一級別,并且僅執行等于或高于它們自身的級別的日志請求。比如,如果一個 Appender 屬于 INFO 級別,而日志請求屬于 DEBUG,那么 Appender 將不會為給定的日志事件寫消息。
客戶端組件
客戶端 log4j.properties 文件
客戶端 log4j.properties 文件是一種標準文件,它包含服務或模塊使用的所有 Appender。Web Service Appender 要求有一個端點(endpoint) 屬性以指定所使用的日志服務。
清單 1 描述了使用 WebServiceAppender
所必需的 Web 服務客戶端 Log4j 屬性。 黑體顯示的文本指明了將訪問 WebServiceAppender
服務器端的 Appender。屬性文件是使用 Log4j 的基本需求,它可以讓您配置應用程序以使用多個 Appender 以及 logging severity。一旦應用程序進入運行狀態或潛在的問題得到解決,您就可以輕松地修改屬性文件。
清單 1:客戶端 Log4j 的屬性文件 #set the level of the root logger log4j.rootLogger = INFO, CONSOLE #set own logger log4j.logger.com.carmelouria.logging.test=CONSOLE log4j.appender.CONSOLE=com.carmelouria.logging.WebServiceAppender log4j.appender.CONSOLE.endpoint= http://localhost:9080/log4j/services/LogAppenderService log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern=%p [%t] %c{2} (%M:%L) :: %m%n
|
服務器的 Log4j.properties 文件
服
務器 Log4j.properties 文件被用來關聯客戶端 Log4j 屬性文件,它指定了日志的級別及服務器將如何輸出消息。對于支持
Log4j 的應用程序,您可以定義多個 appender。當然,這些 appender 既可以用于客戶端服務,也可以用于服務模塊。
清單 2 描述了一份典型的 Log4j 屬性文件,服務器端的 WebServiceAppender
使用缺省的 Log4j Appenders。服務器端的 Appender 可以潛在的調用另一個 WebServiceAppender
,并將日志信息鏈接起來:
清單 2:服務器端的 Log4j 屬性文件 #set the level of the root logger log4j.rootLogger = INFO, FILE #set own logger log4j.appender.FILE=org.apache.log4j.RollingFileAppender log4j.appender.FILE.file=c:/temp/log4j/server/server.log log4j.appender.FILE.layout=org.apache.log4j.PatternLayout log4j.appender.FILE.layout.ConversionPattern=%p [%t] %c{2} (%M:%L) :: %m%n
|
客戶端程序測試示例:
這個客戶端程序示例是無格式普通 Java 對象(POJO),它記錄了一條消息,并被配置為使用 Web Service Appender 來處理消息。清單 3 顯示了這個示例:
清單 3:客戶端應用程序使用 WebServiceAppender 的示例 package com.carmelouria.logging.test; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PropertyConfigurator; /** * @author Carmelo Uria * */ public class LoggingSample { private static Logger logger = Logger.getLogger(LoggingSample.class.getName()); /** * */ public LoggingSample() { super(); PropertyConfigurator.configure("c:/temp/log4j.properties"); logger.log(Level.INFO, "LoggingSample instantiation..."); System.out.println("finished..."); } public static void main(String[] args) { LoggingSample sample = new LoggingSample(); } }
|
WebServiceAppender
WebServiceAppender
是必需的,它可以將消息發送到指定的 Web 服務。WebServiceAppender
繼承了 org.log4j.Appender,它允許使用 log4.properties,并成為有效的 Log4j Appender。
WebServiceAppender
使用基于 XML 的遠程過程調用 (JAX-RPC) 的 Java API,來將消息發送到服務器。JAX-RPC 是一種規范,它描述使用
RPC 和 XML 構建 Web 服務和 Web 服務客戶端的應用編程接口 (API) 和約定。JAX-RPC 又被稱為 JSR 101。
LoggingEvent
通過 SOAPElement
被分割并表示為 XML。javax.xml.soap.SOAPElement 接口意味著服務端點接口將包含一個參數,或返回 javax.xml.soap.SOAPElement 類型的值,以對應于 schema 中每個使用<xsd:any/>
的地方。從本質上看,它是 XML 參數的封裝,且沒有相應的序列化/反序列化 JAVA 類。例如,一旦客戶請求記錄一個消息,就會創建一個 LoggEvent
對象,然后傳送給 Appender。在這種情況下,Appender 就是 WebServiceAppender
。Appender 檢索事件,并在解析事件中的信息。一些額外的信息會被加入,如主機名稱,這樣您就知道這些消息來自哪個系統。同時,append 方法也將消息轉換為 SOAPElement
,這樣就可以通過 executeWebService
方法將消息傳遞給 Web 服務。使用 SOAPElement
充分考慮了 WebServiceAppender
未來版本的可擴展性問題。
清單4:執行 WebServiceAppender 服務的 Append 方法 protected void append(LoggingEvent event) { // create Web Service client using endpoint if (endpoint == null) { System.out.println("no endpoint set. Check configuration file"); System.out.println("[" + hostname + "] " + this.layout.format(event)); return; } executeWebService(event); } private void executeWebService(LoggingEvent event) { SoapClient client = new SoapClient(); URL endPoint = null; try { endPoint = new URL(getendpoint()); } catch (MalformedURLException e1) { e1.printStackTrace(); } String nameSpace = "http://ejb.logging.carmelouria.com"; QName serviceName = new QName(nameSpace, "LogAppenderServiceService"); QName operation = new QName(nameSpace, "log"); QName port = new QName(nameSpace, "LogAppenderService"); Parameter message = new Parameter("log", Constants.XSD_ANY, SOAPElement.class, ParameterMode.IN); try { /** *create SOAPElement from LoggingEvent need hostname */ Level level = event.getLevel(); String sysLog = "<syslog>" + new Integer(level.getSyslogEquivalent()).toString() + "</syslog>"; String startTime = new Long(LoggingEvent.getStartTime()).toString(); String timeTag = "<start_time>" + startTime + "</start_time>"; String hostName = "<hostname>" + InetAddress.getLocalHost() + "</hostname>"; String threadName = "<thread_name>" + event.getThreadName() +"</thread_name>"; String logger = "<logger>" + event.getLoggerName() + "</logger>"; String eventMessage = "<message>" + event.getRenderedMessage() + "</message>"; String log = hostName + threadName + logger + timeTag + sysLog + eventMessage; String throwableInformation[] = event.getThrowableStrRep(); if (throwableInformation != null) { for (int i = 0; i < throwableInformation.length; i++) { String throwable = "<throwable_information>" + throwableInformation[i] + "</throwable_information>"; log += throwable; } } String ndcString = event.getNDC(); if (throwableInformation != null) { String throwable = <ndc>" + ndcString + </ndc>"; log += throwable; } message.setValue(SOAPElementFactory.create(<log>" + log + </log>")); } catch (UnknownHostException unknownHostException) { unknownHostException.printStackTrace(); } catch (SOAPException e2) { e2.printStackTrace(); } Parameter resultType = newParameter("logResponse", Constants.WEBSERVICES_VOID, Object.class, ParameterMode.OUT); Parameter[] parameters = { message }; try { // execute client Object result = client.execute(endPoint, serviceName, operation, "wrapped", null, port, resultType, parameters); if ((result != null) && (result instanceof String)) System.out.println((String) result); } catch (ClientException e) { e.printStackTrace(); } }
|
Hostname
不幸的是,Log4j 的 LoggingEvent
沒有包含 Hostname,而 Hostname 是 Web Service Appender 眾多需求之一。在創建 SOAPElement
以前,您可以用下面的語句將 Hostname 添加到 XML 文件里:
String hostName = "<hostname>" + InetAddress.getLocalHost() + "</hostname>";
SoapElementFactory
SoapElementFactory
是主要用于創建 SOAPElement
的類。它同時支持創建 IBM 和 Java 的 SOAPElement
實現,如清單 5 所示:
清單 5:使用 SoapElementFactory 類的創建方法 public static javax.xml.soap.SOAPElement create(String xml) throws SOAPException { com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory factory = (com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory) com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory .newInstance(); SOAPElement element = (javax.xml.soap.SOAPElement)factory.createElementFromXMLString(xml); return(element); } public static SOAPElement create(String arg0, String arg1, String arg2, boolean ibmSoapElement) throws SOAPException { if (ibmSoapElement) { SOAPFactory soapFactory = (com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory) com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory.newInstance(); return (soapFactory.createSOAPElement(arg0, arg1)); } javax.xml.soap.SOAPFactory soapFactory = javax.xml.soap.SOAPFactory.newInstance(); return (soapFactory.createElement(arg0, arg1, arg2)); }
|
SoapClient
SoapClient
類封裝了 Call
接口的 JAX-RPC 實現,javax.xml.rpc.Call 接口提供了對服務端點動態調用的支持。javax.xml.rpc.Service 接口就好象是創建 Call
實例的工廠。
清單 6 說明了客戶端如何動態調用服務。這允許對服務進行變更,而無需生成客戶端代理來訪問遠程服務。
清單 6:使用 SoapClient 類的調用方法 private Object call(SoapService service, QName operation, QName portType, String operationStyleProperty, String encodingURIProperty, Parameter returnType, Parameter[] parameters) throws ClientException { QName portName; String response = null; Object results = null; Call call = null; try { // check to see if Service object exists if (service == null) throw new ClientException("Invalid Service object. It maybe null."); // retrieve call from Service object call = service.createCall(); call.setOperationName(operation); call.setPortTypeName(portType); // check call object if (call == null) throw new ClientException("invalid operation. Call object is null."); // set default values if (operationStyleProperty == null) call.setProperty(Call.OPERATION_STYLE_PROPERTY, OPERATION_STYLE_DOCUMENT_TYPE); else call.setProperty(Call.OPERATION_STYLE_PROPERTY, operationStyleProperty); if (encodingURIProperty == null) call.setProperty(Call.ENCODINGSTYLE_URI_PROPERTY, ENCODING_LITERAL); else call.setProperty(Call.ENCODINGSTYLE_URI_PROPERTY, encodingURIProperty); call.setTargetEndpointAddress(service.getServiceEndPoint()); //create Parameter class for SoapClient for (int i = 0; i < parameters.length; i++) { Class classObject = parameters[i].getClassObject(); if (classObject != null) call.addParameter(parameters[i].getName(), parameters[i].getXmlType(), parameters[i].getClassObject(), parameters[i].getMode()); else call.addParameter(parameters[i].getName(), parameters[i].getXmlType(), parameters[i].getMode()); } // pass parameter as ReturnType if (returnType != null) { if (returnType.getClassObject() != null) call.setReturnType(returnType.getXmlType(), returnType.getClassObject()); else call.setReturnType(returnType.getXmlType()); } Object[] request = new Object[parameters.length]; // add parameter values for (int i = 0; i < request.length; i++) { request[i] = parameters[i].getValue(); } results = call.invoke(request); } catch (SOAPFaultException e) { System.out.println(e.getFaultString()); e.getStackTrace(); throw new ClientException(e.getLocalizedMessage(), e); } catch (ServiceException serviceException) { serviceException.getStackTrace(); throw new ClientException(serviceException.getLocalizedMessage(), serviceException); } catch (RemoteException exception) { exception.printStackTrace(); throw new ClientException(exception.getLocalizedMessage(), exception); } return (results); }
|

 |

|
服務組件
Log4j.server.properties
Log4j.server.properties 文件包含了一個基本的 Log4j 配置文件,該文件可以讓您指定把哪些日志發送給 Web 服務系統。
清單 7:Log4j.server.properties 文件 #set the level of the root logger log4j.rootLogger = INFO, FILE #set own logger log4j.appender.FILE=org.apache.log4j.RollingFileAppender log4j.appender.FILE.file=c:/temp/log4j/server/server.log log4j.appender.FILE.layout=org.apache.log4j.PatternLayout log4j.appender.FILE.layout.ConversionPattern=%p [%t] %c{2} (%M:%L) :: %m%n
|
LogAppenderBean.java
LogAppenderBean.java 是 Web Service Appender 服務所要使用的 EJB。該服務啟動 LogAppenderBean
以處理來自每個 Web Service Appender 客戶端的每一個請求。
清單 8 顯示了來自 WebServiceAppender
EJB 的 log 方法,該方法解析來自客戶端的消息,并將客戶端信息紀錄到服務的服務器端。
清單 8:LogAppenderBean 的 log 方法 public void log(SOAPElement message) { try { InputSource source = ((IBMSOAPElement) message).toInputSource(false); Document document = Parser.parse(source); String log = null; String hostname = document.selectSingleNode("http://hostname").getText(); String threadName = document.selectSingleNode("http://thread_name").getText(); String syslog = document.selectSingleNode("http://syslog").getText(); String startTime = new Long( document.selectSingleNode("http://start_time"). getText()).toString(); log = '[' + startTime + ':' + hostname + ':' + threadName + "] " + document.selectSingleNode( "http://message").getText(); // retrieve any throwable messages List throwableList = document.selectNodes( "http://throwable_information"); if(throwableList != null) { Iterator throwables = throwableList.iterator(); while(throwables.hasNext()) { log += '\n' + ((Node)throwables.next()).getText(); }
log += '\n'; }
logger.log(Level.toLevel(new Integer(syslog).intValue()), log); logger.log(Level.INFO,log); } catch(ParserException parseException) { parseException.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } }
|
通過 IBM SOAPElement
的 InputSource,每一個 SOAPElement
的內容都會被檢索。目前,只有 IBM WebSphere? Application Server (Application Server) 支持這些代碼(請參閱參考資料)。 然而,如果您移除 IBM SOAPElement
,那么您就可以在任何應用服務器上使用這些代碼。IBM
SOAPElement
內置的性能優化也適用于 Application Server。
每一個 SOAPElement
都使用 Dom4j 來讀取、解析和轉換。Dom4j 是一種在內存中表示 XML 樹的對象模型。Dom4j 提供了一組易于使用的
API,從而為我們提供了一整套強大的功能來處理、操作或定位 XML,使用 XPath 和 XSLT 進行工作,以及與 SAX、
JAXP、DOM 集成。
除了可以使用任意的 XML 解析器外,DOM4J 還允許使用任意的 SAX 解析器,為實現更好的性能,還允許使用所有標準的 XSLT 轉換器。
轉換被用來析取發送給 Web Service Appender 的客戶端 LoggingEvent
的元素。
如果您允許使用 SOAPElement
,那么就需要在代碼中維持最大限度的靈活性。Web Service Appender 服務可以被修改,以支持所有發送給服務的 XML。
輸出
下面的示例展示了 Web Service Appender 的可能的輸出:
INFO [WebContainer : 0] ejb.LogAppenderBean (log:?) :: [1111513482641:OO7-64BIT/9.48.114.183:main]LoggingSample instantiation...
OO7-64BIT/9.48.114.183 是機器名和 IP 地址,而 main 是日志所在處的方法名。
結束語
Web
Service Appender 是將日志集中到某一位置的基本工具。由于 Web Service Appender 是 Log4j 的
Appender 類的子集,因而配置和使用 Appender 都非常簡單易懂。您可以修改 Log4j 的屬性文件,這樣,使用 Log4j
的現有應用程序和服務就可以馬上使用 Web Service Appender。
下載
描述 | 名字 | 大小 | 下載方法 |
---|
Foundation Class Library | foundation.zip | 47 KB | HTTP |
---|
Logging Web Service J2EE Application | LoggingWebService.ear | 1976 KB | HTTP |
---|
Unit Test Sample Code | SoapClientTest.java | 5 KB | HTTP |
---|