tomcat 訪問日誌源碼分析與應用

    tomcat 日誌可以分爲兩類:

1、訪問日誌,記錄訪問的時間、來源、資料等相關信息(ServletRequest 可以獲取的信息,都可以記錄);

2、運行日誌,記錄tomcat 運行、異常、錯誤信息。

    tomcat 的日誌記錄常會被 log4j 或 slf4j 取代,不過這裏不討論另外日誌組件,很純粹地說一下tomcat 原生的訪問日誌。關於運行日誌的分析,有機會再另寫一篇。對於訪問日誌,tomcat 定義了以下接口:

public interface AccessLog {

    // 記錄訪問日誌
    public void log(Request request, Response response, long time);

    // ip
    public static final String REMOTE_ADDR_ATTRIBUTE =
        "org.apache.catalina.AccessLog.RemoteAddr";
    // 主機名
    public static final String REMOTE_HOST_ATTRIBUTE =
        "org.apache.catalina.AccessLog.RemoteHost";
    // 訪問協議
    public static final String PROTOCOL_ATTRIBUTE =
        "org.apache.catalina.AccessLog.Protocol";
    // 端口號
    public static final String SERVER_PORT_ATTRIBUTE =
        "org.apache.catalina.AccessLog.ServerPort";

    // 設置是否記錄ip,主機名,協議,端口號
    public void setRequestAttributesEnabled(boolean requestAttributesEnabled);
    public boolean getRequestAttributesEnabled();
}


    一個默認的實現是 AccessLogValue(在 server.xml 配置的)。先看一下,如何配置和使用 AccessLogValue,在 $tomcat_home%/conf/server.xml 裏,有一下代碼:

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log." suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />


    參數的含義如下:

className:訪問日誌的實現類(implements AccessLog)

directory: 日誌的位置

prefix:日誌名稱的前綴

suffix:日誌名稱的後綴

pattern:日誌模式的參數,(模式參數的設置可以參考附錄)

更多參數的設置可以查看 AccessLogValue 的參數。

    對於 pattern ,tomcat 提供了兩種便捷的 pattern 簡寫:common:%h %l %u %t "%r" %s %b;combined - %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"

    因爲上述配置的方式,所以我們常看到日誌記錄文件如下(在 $tomcat_home$/logs/),下面日期的產生,是代碼產生的:

image

    對於其他基礎的字段設置的配置與源碼編寫,理解起來應該不大(類似平常地解釋 xml 文件),下面重點講一下的是,如果根據 patten 來寫日誌(建議先閱讀以下附錄):

    pattern 寫法有兩種 %XXX 或 %{XXX}XX,使用代碼分析分析 pattern,再根據 pattern 獲取對應的信息,將信息寫到一個 StringBuilder 即可。對 pattern 的分析如下:對於各種配置的參數a,A等,都應該屬於一種 XXXElenment,另外對於空格或其他字符,增加一個 StringElement,那在分析 pattern 時,每遇到一個特殊的字符,就創建一個指定的 element,反之,創建一個 StringElement,對pattern 的分析如下:

List<AccessLogElement> list = new ArrayList<AccessLogElement>();
boolean replace = false;
StringBuilder buf = new StringBuilder();
for (int i = 0; i < pattern.length(); i++) {
char ch = pattern.charAt(i);
if (replace) {
/*
* 用來處理 '{',如果在之後沒有遇上 '}',將這個 '{'忽略,不處理。
* 處理一下三種情況:
* %{xxx}i 頭字段信息
* %{xxx}c cookie 信息
* %{xxx}r ServletRequest 的某個 attribute
* %{xxx}s HttpSession 的某個 attribut
*/
if ('{' == ch) {
StringBuilder name = new StringBuilder();
int j = i + 1;
for (; j < pattern.length() && '}' != pattern.charAt(j); j++) {
name.append(pattern.charAt(j));
}
if (j + 1 < pattern.length()) {
// j+1,跳過字符 '}x'
j++;
list.add(createAccessLogElement(name.toString(),
pattern.charAt(j)));
i = j; // 跳過 %{xxx}x
} else {
// 單個字符,如 a,直接創建對應的 Element
list.add(createAccessLogElement(ch));
}
} else {
list.add(createAccessLogElement(ch));
}
replace = false;
} else if (ch == '%') {
replace = true;
list.add(new StringElement(buf.toString()));
buf = new StringBuilder();
} else {
buf.append(ch);
}
}
if (buf.length() > 0) {
list.add(new StringElement(buf.toString()));
}

    通過上面的分析,我們就可以根據 pattern 得到需要的信息(存儲在 list 裏),對於各種 element 的創建如:

·  /*
* 根據 pattern,創建以下六種類型的信息之一:
* %{xxx}i 獲取header 的某個 attribute
* %{xxx}c 獲取cookie 的某個 attribute
* %{xxx}o 獲取response 的某個 attribute
* %{xxx}r 獲取request 的某個 attribute
* %{xxx}s 獲取session 的某個 attribute
* %{xxx}t 獲取dateAndTime 的某個 attribute
*/
protected AccessLogElement createAccessLogElement(String attribute, char pattern) {
switch (pattern) {
case 'i':
return new HeaderElement(attribute);
case 'c':
return new CookieElement(attribute);
case 'o':
return new ResponseHeaderElement(attribute);
case 'r':
return new RequestAttributeElement(attribute);
case 's':
return new SessionAttributeElement(attribute);
case 't':
return new DateAndTimeElement(attribute);
default:
return new StringElement("???");
}
}
    常規 element 的創建:
protected AccessLogElement createAccessLogElement(char pattern) {
switch (pattern) {
case 'a':
return new RemoteAddrElement();
case 'A':
return new LocalAddrElement();
case 'b':
return new ByteSentElement(true);
case 'B':
return new ByteSentElement(false);
case 'D':
return new ElapsedTimeElement(true);
case 'F':
return new FirstByteTimeElement();
case 'h':
return new HostElement();
case 'H':
return new ProtocolElement();
case 'l':
return new LogicalUserNameElement();
case 'm':
return new MethodElement();
case 'p':
return new LocalPortElement();
case 'q':
return new QueryElement();
case 'r':
return new RequestElement();
case 's':
return new HttpStatusCodeElement();
case 'S':
return new SessionIdElement();
case 't':
return new DateAndTimeElement();
case 'T':
return new ElapsedTimeElement(false);
case 'u':
return new UserElement();
case 'U':
return new RequestURIElement();
case 'v':
return new LocalServerNameElement();
case 'I':
return new ThreadNameElement();
default:
return new StringElement("???" + pattern + "???");
}
}

    對於各種 element,這裏只給出其中幾個,其他的類似:
//accessElement 接口
protected interface AccessLogElement {
public void addElement(StringBuilder buf, Date date, Request request,
Response response, long time);
}
//sessionElement %{xxx}s
protected static class SessionAttributeElement implements AccessLogElement {
private final String header;

public SessionAttributeElement(String header) {
this.header = header;
}

@Override
public void addElement(StringBuilder buf, Date date, Request request,
Response response, long time) {
Object value = null;
if (null != request) {
HttpSession sess = request.getSession(false);
if (null != sess) {
value = sess.getAttribute(header);
}
} else {
value = "??";
}
if (value != null) {
if (value instanceof String) {
buf.append((String) value);
} else {
buf.append(value.toString());
}
} else {
buf.append('-');
}
}
}

// queryElement %q
protected static class QueryElement implements AccessLogElement {
@Override
public void addElement(StringBuilder buf, Date date, Request request,
Response response, long time) {
String query = null;
if (request != null) {
query = request.getQueryString();
}
if (query != null) {
buf.append('?');
buf.append(query);
}
}
}

    知道訪問日誌的基本實現之後,下面來看一下如何去應用,讓訪問日誌可以給實際項目帶來效益,在這裏就要求我們重新來看一下 pattern 到底可以爲我們記錄那些信息了,如何利用這些信息,獲取一些對項目有用的信息:這裏大概列舉一些基本有用的信息:遠程 ip%a,遠程主機名%h,請求的協議%s,請求uri %U,查詢參數 %q,響應的狀態碼,請求時間%t,響應時間%D,請求的用戶%u,根據需要,我們還可以藉助 request,session,response 獲取一些額外的信息,如 user-Agent 等。拿到這些信息後,可以幹些什麼呢??
1、訪問者所使用的媒體,哪一個瀏覽器,pc 還是 移動
2、獨立 ip 請求數(更具ip分析人羣的地域分佈)
3、頁面的訪問量(那個頁面最後歡迎,響應時間如何)
4、404 的請求 uri
5、導致 500 的uri
6、指定的用戶的訪問行爲
7、訪問的流量統計等
    既然是需要統計數據,那這些數據肯定不能存在txt裏,最合適的地方莫過於數據庫。一下是一個基本的參考,根據實際情況,重構和定製自己的需求(定製自己的 MyJdbcAccessLogValue):
    tomcat 7 提供了一個 JdbcAccessLogValue,用於將數據存儲到數據庫裏,JdbcAccessLogValue,可以存儲的信息有:

remoteHostField = "remoteHost";
userField = "userName";
timestampField = "timestamp";
virtualHostField = "virtualHost";
methodField = "method";
queryField = "query";
statusField = "status";
bytesField = "bytes";
refererField = "referer";
userAgentField = "userAgent";

   使用 JdbcAccessLogValue,需要做一下配置,在$tomcat_home%/conf/server.xml 添加如下配置:

<Valve className="org.apache.catalina.valves.JDBCAccessLogValve"
driverName="your_jdbc_driver"
connectionURL="your_jdbc_url"
pattern="common" resolveHosts="false"
/>

  同樣的,需要在數據庫創建一個訪問表,官方給的例子如下(可以根據需要自己定製):
CREATE TABLE access (
id INT UNSIGNED AUTO_INCREMENT NOT NULL,
remoteHost CHAR(15) NOT NULL,
userName CHAR(15),
timestamp TIMESTAMP NOT NULL,
virtualHost VARCHAR(64) NOT NULL,
method VARCHAR(8) NOT NULL,
query VARCHAR(255) NOT NULL,
status SMALLINT UNSIGNED NOT NULL,
bytes INT UNSIGNED NOT NULL,
referer VARCHAR(128),
userAgent VARCHAR(128),
PRIMARY KEY (id),
INDEX (timestamp),
INDEX (remoteHost),
INDEX (virtualHost),
INDEX (query),
INDEX (userAgent)
);

  對於訪問日誌,因爲數據量比較大,可以分表存放,對於信息統計,可是設置一個定時器,定時在某個時刻統計當日/當週的訪問結果。得到數據裏,下面就是網頁呈現的問題,藉助一下插件,如jfreechart或其他圖表插件,下面是我搜出來的一個應用,大家可以大概感受一下效果,具體做法可以參考本文。

http://www.blogjava.net/xmatthew/archive/2008/04/14/192450.html

張貼一張其博客的圖片:

image

附錄:

pattern 模式詳細的參數設置:

%a - 遠端IP地址 
%A - 本地IP地址 
%b - 發送的字節數,不包括HTTP頭,如果爲0,使用"-" 
%B - 發送的字節數,不包括HTTP頭 
%h - 遠端主機名(如果resolveHost=false,遠端的IP地址) 
%H - 請求協議 
%l - 從identd返回的遠端邏輯用戶名(總是返回 '-') 
%m - 請求的方法(GET,POST,等) 
%p - 收到請求的本地端口號 
%q - 查詢字符串(如果存在,以 '?'開始) 
%r - 請求的第一行,包含了請求的方法和URI 
%s - 響應的狀態碼 
%S - 用戶的session ID 
%t - 日誌和時間,使用通常的Log格式
%u - 認證以後的遠端用戶(如果存在的話,否則爲'-') 
%U - 請求的URI路徑 
%v - 本地服務器的名稱 
%D - 處理請求的時間,以毫秒爲單位 
%T - 處理請求的時間,以秒爲單位 

%{xxx}i  獲取header 的某個 attribute
%{xxx}c  獲取cookie 的某個 attribute
%{xxx}o  獲取response 的某個 attribute
%{xxx}r  獲取request 的某個 attribute
%{xxx}s  獲取session 的某個 attribute
%{xxx}t  獲取dateAndTime 的某個 attribute

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章