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 "%r" %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/),下面日期的产生,是代码产生的:
对于其他基础的字段设置的配置与源码编写,理解起来应该不大(类似平常地解释 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);
}
}
}