阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Tomcat 访问日志源码分析与应用

143次阅读
没有评论

共计 14784 个字符,预计需要花费 37 分钟才能阅读完成。

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/),下面日期的产生,是代码产生的:

Tomcat 访问日志源码分析与应用

    对于其他基础的字段设置的配置与源码编写,理解起来应该不大(类似平常地解释 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);
        }
    }
}

更多详情见请继续阅读下一页的精彩内容 :http://www.linuxidc.com/Linux/2014-11/109650p2.htm

Linux 下 Apache 与多个 Tomcat 集群负载均衡 http://www.linuxidc.com/Linux/2012-01/51731.htm

Nginx Tomcat 集群负载均衡解决笔记 http://www.linuxidc.com/Linux/2013-07/86827.htm

实例详解 Tomcat 组件安装 +Nginx 反向代理 Tomcat+Apache 使用 mod_jk 和 mod_proxy 反向代理和负载均衡 http://www.linuxidc.com/Linux/2013-06/85290.htm

CentOS 6.5 下利用 Rsyslog+LogAnalyzer+MySQL 部署日志服务器 http://www.linuxidc.com/Linux/2014-06/103836.htm

Apache+Tomcat 环境搭建(JK 部署过程)http://www.linuxidc.com/Linux/2012-11/74474.htm

知道访问日志的基本实现之后,下面来看一下如何去应用,让访问日志可以给实际项目带来效益,在这里就要求我们重新来看一下 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 或其他图表插件,下面是我搜出来的一个应用,大家可以大概感受一下效果,具体做法可以参考本文。

Tomcat 访问日志源码分析与应用
Tomcat 访问日志源码分析与应用

附录:

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

Tomcat 的详细介绍 :请点这里
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/),下面日期的产生,是代码产生的:

Tomcat 访问日志源码分析与应用

    对于其他基础的字段设置的配置与源码编写,理解起来应该不大(类似平常地解释 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);
        }
    }
}

更多详情见请继续阅读下一页的精彩内容 :http://www.linuxidc.com/Linux/2014-11/109650p2.htm

Linux 下 Apache 与多个 Tomcat 集群负载均衡 http://www.linuxidc.com/Linux/2012-01/51731.htm

Nginx Tomcat 集群负载均衡解决笔记 http://www.linuxidc.com/Linux/2013-07/86827.htm

实例详解 Tomcat 组件安装 +Nginx 反向代理 Tomcat+Apache 使用 mod_jk 和 mod_proxy 反向代理和负载均衡 http://www.linuxidc.com/Linux/2013-06/85290.htm

CentOS 6.5 下利用 Rsyslog+LogAnalyzer+MySQL 部署日志服务器 http://www.linuxidc.com/Linux/2014-06/103836.htm

Apache+Tomcat 环境搭建(JK 部署过程)http://www.linuxidc.com/Linux/2012-11/74474.htm

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-20发表,共计14784字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中