Spring Boot Web 输出 Tomcat 的访问日志到控制台

当我们直接使用 Tomcat  时,访问日志的配置在 $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" />

产生的日志文件在 $TOMCAT_HOME/logs 目录中,生成以日期戳进行区分的滚动日志文件,如 localhost_access_log.2022-10-25.txt 等

而在我们使用  Spring Boot Web 时, 默认的嵌入式 Web 应用服务器是 Tomcat,我们可以在 Spring 属性文件中配置是否启用 Tomcat 访问日志(默认不启用)。属性文件中的配置针对的是如何输出访问日志到文件,而我们有时候需要输出访问日志到控制台而不非文件,比如 AWS 上的 ECS 应用,控制台的输出可直接发送到 CloudWatch,这样便于分析日志。

在 Spring Web 中可以用 Interceptor 或 Filter 来记录访问日志,但在请求出错时无法准确显示出响应时间和状态码,毕竟 Tomcat 的访问日志在代码的外层,进出 Controller 方法的输入输出信息也就掌握的更清楚。

本文的任务是探索输出 Spring Boot Web 嵌入式 Tomcat 的访问日志到控制台。有两种方式

  1. 配置特殊输出设备文件 /dev/stdout 引导 Tomcat 输出访问日志到控制台
  2. 定制 Tomcat 的 AccessLogValve,获取到访问日志记录,然后自己控制输出,直接 Sytem.out.println 或由通用日志框架(slf4 等) 输出

关于访问日志的配置可在 Spring 属性文件(application.properties 或 yaml 文件) 中定制,属性名为 server.tomcat.accesslog.* 等一系列配置中。在 Spring Common Application Properties 页面中搜索 server.tomcat.accesslog. 下面列出相关的属性, 说明及默认值,按大概的关注度由强及弱排列

  1. server.tomcat.accesslog.enabled: 是否开启访问日志,默认为 false
  2. server.tomcat.accesslog.directory: 访问日志目录,可用绝对或相对目录(相对于 Tomcat Base 目录),默认为 logs
  3. server.tomcat.accesslog.prefix: 日志文件名前缀,默认为 access_log
  4. server.tomcat.accesslog.suffix: 日志文件名后缀,默认为 .log
  5. server.tomcat.accesslog.pattern: 日志记录行输出格式,默认为 common, 即 %h %l %u %t "%r" %s %b
  6. server.tomcat.accesslog.file-date-format: 插入到日志文件名中的日期格式,默认为 .yyyy-MM-dd, 所以结合 prefix, suffix, 最后的文件名就像 localhost_access_log.2022-10-25.log
  7. server.tomcat.accesslog.condition-if: 如果设置了值为 "needLog", 那么只有请求的 ServletRequest.getAttribute("needLog") 不为 null 时才记录; 默认为空,即所有的访问都被记录
  8. server.tomcat.accesslog.condition-unless: 如果设置了值为 "excludeLog" 的话,只有 ServletRequest.getAttribute("excludeLog") 为 null 时才记录; 默认为空,即所有访问都被记录
  9. server.tomcat.accesslog.rotate: 是否滚动访问日志文件,默认是 true
  10. server.tomcat.accesslog.rename-on-rotate: 日志文件名是否含有时间戳,直到被滚动时才加上时间戳。默认为 false,表示当前日志文件名含有时间戳
  11. server.tomcat.accesslog.max-day: 访问日志保留的最大天数,默认为 -1, 表示永远
  12. server.tomcat.accesslog.buffered:  在输出日志内容到文件前是否缓存, 缓存中的内容被定期刷到文件中,默认为 true。缓存到减少日志输出对正常业务代码的性能影响
  13. server.tomcat.accesslog.check-exists: 是否检查日志文件是否存在,比如在外部重命名了日志文件后会重新创建,默认为 false
  14. server.tomcat.accesslog.encoding: 日志文件的字符集,默认为系统默认字符集
  15. server.tomcat.accesslog.locale: 产生时间戳所用的区域(Locale),默认为 Java 的默认 local
  16. server.tomcat.accesslog.request-attributes-enabled: 是否把 IP 地址,Hostname, 协议,端口号附加到 Request 中去,默认为  false
  17. server.tomcat.accesslog.ipv6-canonical: 是否使用 IPv6 的规范格式,默认为 false

如果在 Spring Boot 中引入的不是 Tomcat,比如 undertow, 那么相应的访问日志配置属性就是 server.undertow.accesslog.*

上面的属性对应到 org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Accesslog, org.apache.catalina.values.AccessLogValveorg.apache.catalina.values.AbstractAccessLogValve 中的属性。

输出访问日志到  /dev/stdout(标准输出)

在进行实质性配置之前看一下只是单单启用 server.tomcat.accesslog.enabled=true ,其他属性全保持默认值时的访问日志在哪里。为此我们先在 application.properties 中加上一行

根据其他属性的默认值,我们可此访问日志文件将会是 logs/access_log.2022-10-26.log (写作时的日期), 这是一个相对路径,相对目录是 System.getProperty("catalina.home"), 比如在 Mac OS X 下在一次 Spring Boot Web 应用启动后输出的值为

/private/var/folders/hz/h515j94x4ls272nvxf3pqt800000gq/T/tomcat.8080.17520809821230678569

访问了一次 /test 后,产生的日志文件可见如下

$ ~ ls /private/var/folders/hz/h515j94x4ls272nvxf3pqt800000gq/T/tomcat.8080.17520809821230678569
logs work
$ ~ ls /private/var/folders/hz/h515j94x4ls272nvxf3pqt800000gq/T/tomcat.8080.17520809821230678569/logs
access_log.2022-10-26.log
$ ~ cat /private/var/folders/hz/h515j94x4ls272nvxf3pqt800000gq/T/tomcat.8080.17520809821230678569/logs/access_log.2022-10-26.log
0:0:0:0:0:0:0:1 - - [26/Oct/2022:10:08:53 -0500] "GET /test HTTP/1.1" 200 12

创建文件输出流来输出内容到控制台可用下面的方式

执行后控制台输出为

hello

由此,我们可以让 Tomcat 认为输出文件是 /dev/stdout 这一绝对文件路径,为达到此目的,我们必须原本访问日志文件的中,后缀置空掉,最后的 Spring 属性配置如下

这样 Tomcat 就会乖乖的输出访问日志到  /dev/stdout,关闭缓存的目的是要立即看到访问日志输出。

如果我们对它进行追根溯源的话,可以找到线索

因为 AccessLogValve 最终是用 new File() 来创建访问日志文件的,也就是为什么我们指定输出到 /dev/stdout 便是标准输出控制台了。

从上可知,我们以 Hack 的方式用 /dev/stdout 欺骗了 Spring Boot 使得访问日志输出到了控制台,同时也为我们指明了另一个方向,那就是

用 WebServerFactoryCustomizer 定制 AccessLogValue

从前面的源代码可知 Spring 发现有 WebServerFactoryCustomizer 实例便会调用它的 customize() 方法,所以我们只要声明一个这样的 Bean 就能定制 Tomcat 的 AccessLogValue

创建类 AccessValveConfig

现在测试一下,在控制台上会输出

2022-10-26 11:52:35.301 INFO 34986 --- [nio-8080-exec-1] yanbin.blog.AccessValveConfig : remote_host=0:0:0:0:0:0:0:1, request=GET /test HTTP/1.1, status_code=200, execute_time=56, bytes_received=-, bytes_sent=12, user_agent=curl/7.64.1

日志是通过 SLFJ 输出来的,timestamp, 线程信息由 SLFJ 负责

有关于访问日志的 pattern 及定制

当我们在定制 pattern 时,了解它是如何获取各种信息的很重要。前面提到过内置的可用字段可以在 org.apache.catalina.values.AbstractAccessLogValve 中找到,如 %a, %A, %r 等,以及用 ${xxx}i, %{xxx}o, %{xxx}c, %{xxx}r, %{xxx}s 可分别从请求头(i), 响应头(o), cookie(c), Request attribute(r), Session attribute(s) 中提取属性值。它们的实现在 org.apache.Catalina.values.AccessLogElement 子类中,如 ResponseHeaderElement。

假如我们有自己需求的话,比如我们部署在 ECS Container 中应用希望能获得所在 EC2 的 IP 地址,而不是容器的 IP 地址,就可以实现一个 EC2AttributeElement,之后在 pattern 中配置 %{ip}E,为此我们需下面几步

  1. 在 Pattern 中加上 %{ip}E
  2. 在 CustomAccessLogValve 中覆盖 createAccessLogElement(String name, char pattern) 方法,并在 CustomAccessLogValve 中(因为 AccessLogElement 是一个 protected 类) 创建 Ec2AttributeElement类,相关的代码如下

    在 ECS 容器中可通过 curl http://169.254.169.254/latest/meta-data/local-ipv4 获得所在 EC2 实例的 IP 地址, 前面的代码还应把第一次成功解析出来的 IP 地址缓存起来,后续直接使用。

最后,部署到 EC2 的 Docker 容器中的 Spring Boot Web  的访问日志就会像

2022-10-26 12:43:51.253 INFO 36843 --- [nio-8080-exec-1] yanbin.blog.AccessValveConfig : remote_host=0:0:0:0:0:0:0:1, local_host=192.168.86.23, request=GET /test HTTP/1.1, status_code=200, execute_time=56, bytes_received=-, bytes_sent=12, user_agent=curl/7.64.1

备注:

无法是在 EC2 还是在 EC2 上运行的 ECS 容器中都可以通过 http://169.254.169.254/latest/meta-data/ 获得相应的 EC2 实例的信息,如

[root@xyz-01234567 ~]# curl http://169.254.169.254/latest/meta-data/
ami-id
ami-launch-index
ami-manifest-path
autoscaling/
block-device-mapping/
events/
hostname
iam/
identity-credentials/
instance-action
instance-id
instance-life-cycle
instance-type
local-hostname
local-ipv4
mac
metrics/
network/
placement/
profile
reservation-id
security-groups
services/
[root@xyz-01234567 ~]# curl http://169.254.169.254/latest/meta-data/hostname
ip-192-168-86-23.ec2.internal
[root@lin-01234567 ~]# curl http://169.254.169.254/latest/meta-data/instance-type
t3.small

其他的信息以相同的方式获得

参考:

  1. How to get host ip and port from inside ECS container
  2. How to redirect logs to stdout?

本文链接 https://yanbin.blog/spring-boot-web-tomcat-access-logs-to-console/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments