应用容器化部署已经成为一个趋势,依托容器云自动调度平台(如k8s)能够快速实现应用的扩容和发布,本文简要介绍了在Kubernetes平台上,SpringBoot应用日志的一种解决方案。方案依托平台优势,优雅、简介、快速的实现应用日志的采集和分析。同时,对生产环境下日志的输出,详细介绍了生产环境下采用JSON格式输出日志配置全过程。
一、目标
- 依托Kubernetes平台日志采集管理能力(Loki + Promtail的云原生日志收集方案),将应用日志也纳入综合管理。
- 生产环境采用JSON输出简化日志解析,使得日志的后续处理、分析或查询变得方便高效,开发测试环境仍然扁平化输出
- 自定义JSON日志输出内容,微服务环境下日志包含链路信息。
二、应用日志架构设计
2.1 概要
本设计方案是在Kubernetes环境下,通过集成日志工具Loki+Promtail,使得容器云环境能够自动化采集集群内各Pod日志。Grafana作为可视化终端,通过链接Loki数据源,能够对采集的日志进行搜索和分析。其中:
- Promtail: 日志收集工具,类比ELK中的Logstash
- Loki: 日志聚合工具,类似ELK中Elasticsearch
- Grafana:可视化工具,类比ELK中Kibana
应用通过输出日志到控制台,Promtail实时采集应用输出到控制台的日志,并发送至Loki。
这种方案
2.2 方案优势
轻量化
与ELK相比,大大减少了硬件资源的使用。适合中小集群监控。
与k8s原生结合
日志搜索可以通过k8s中资源label标签进行筛选。
三、实施
3.1 前置条件
3.1.1 环境准备
- Kubernetes集群环境
- Loki+Promtail+Grafana已集成到Kubernetes,并且能够采集到Pod日志
- Spring Boot应用已部署到Kubernetes
3.2 日志JSON处理
**Logstash Logback Encoder **开源项目提供了Logback JSON encoder 和 appenders,这个类库最新详细用法参考项目文档介绍
Format | Protocol | Function | LoggingEvent | AccessEvent |
Logstash JSON | Syslog/UDP | Appender | ||
Logstash JSON | TCP | Appender | ||
any | any | Appender | ||
Logstash JSON | any | Encoder | ||
Logstash JSON | any | Layout | ||
General JSON | any | Encoder | ||
General JSON | any | Layout |
根据文档说明,我们使用 LoggingEventCompositeJsonEncoder 来自定义Json Encoder。下面开始实战配置。
集成maven依赖
来源文档 https://github.com/logfellow/logstash-logback-encoder#including-it-in-your-project
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.2</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
配置logback
在资源文件夹中创建logback-spring.xml
文件,默认情况下将所有日志从控制台输出。
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="5 seconds">
<springProperty scope="context" name="appName" source="spring.application.name" defaultValue="unknown" />
<!-- ConsoleAppender:把日志输出到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<!--关键输出配置到这里-->
...
</encoder>
</appender>
<!-- 控制台输出日志级别 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
自定义LoggingEventCompositeJsonEncoder
配置说明:https://github.com/logfellow/logstash-logback-encoder#composite-encoderlayout
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC+8</timeZone>
</timestamp>
<pattern>
<omitEmptyFields>true</omitEmptyFields>
<pattern>
{
"timestamp": "%date{ISO8601}",
"service": "${appName}",
"level": "%level",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{60}",
"method": "%method",
"line": "%line",
"message": "#tryJson{%message}"
}
</pattern>
</pattern>
<stackTrace>
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>100</maxDepthPerThrowable>
<maxLength>20480</maxLength>
<rootCauseFirst>true</rootCauseFirst>
</throwableConverter>
</stackTrace>
</providers>
</encoder>
示例说明:
- omitEmptyFields:在日志中省略空的字符,详细官方配置说明
- pattern:json日志输出模版,详细官方配置说明
- stackTrace:异常堆栈信息输出配置,详细官方配置说明
- #tryJson{%message}: 对于message可以进行json转译,则输出json,否则该字段输出文本串,详细官方配置说明
配置完成后,从控制台打印的日志
3.3 自定义日志信息
除了默认的日志输出内容外,在web应用场景下,我们希望将用户请求时来源IP和请求编号记录到日志中。
Mapped Diagnostic Context (MDC)
是Slf4j提供的一个API,主要功能就是在多线程环境下进行日志调用链路跟踪,使用起来也简单。
3.3.1 实现思路
- 通过在SpringBoot中定义拦截器,获取web请求的IP,初始化请求编号
- 在logback中定义日志输出,打印mdc附带的信息
3.3.2 代码实现
拦截器配置MDC
在Spring中定义拦截器的过程较为简单
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor {
private final static String REQUEST_ID = "requestId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String xForwardedForHeader = request.getHeader("X-Forwarded-For");
String remoteIp = request.getRemoteAddr();
String uuid = UUID.randomUUID().toString();
log.info("put requestId ({}) to logger", uuid);
log.info("request id:{}, client ip:{}, X-Forwarded-For:{}", uuid, remoteIp, xForwardedForHeader);
MDC.put(REQUEST_ID, uuid);
MDC.put("remoteIp", remoteIp);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
String uuid = MDC.get(REQUEST_ID);
log.info("remove requestId ({}) from logger", uuid);
MDC.remove(REQUEST_ID);
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
在preHandle
方法中,获取remoteIp并放入MDC中,同时初始化了请求ID,这里使用的是uuid。
注册拦截器到Spring
SpringMvc注册拦截器,不多解释,主要代码如下:
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
}
}
配置MDC输出到日志
修改logback-spring.xml
配置输出模版, 通过添加输出项"requestId": "%mdc{requestId}"
和 "remoteIP": "%mdc{remoteIp}"
到模版
<pattern>
{
"timestamp": "%date{ISO8601}",
...
"requestId": "%mdc{requestId}",
"remoteIP": "%mdc{remoteIp}",
...
"message": "#tryJson{%message}"
}
</pattern>
重新部署应用,观察日志输出:
可以看到,日志输出中,已经包含了我们在mdc中自定义的属性。
3.4 日志多环境配置
使用多环境配置,在当前解决方案下,主要用来实现,生产环境日志输出JSON格式,开发环境日志输出采用默认的行日志,多环境配置比较简单,通过定义springProfile
标签,name属性为环境名称,配置如下:
<springProfile name="default">
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</springProfile>
<springProfile name="kubernetes">
<root level="INFO">
<appender-ref ref="PROD-STDOUT"/>
</root>
</springProfile>
3.5 链路信息输出
在分布式环境下,应用之间的调用链路信息,我们希望也集成到JSON日志输出中,例如是使用Spring-Cloud-Sleuth
,需要新增额外的链路信息到模版中,需要注意的是在Sleuth 3.0
中,属性名称已经发生了一些变化。参考文档: https://github.com/spring-cloud/spring-cloud-sleuth/wiki/Spring-Cloud-Sleuth-3.0-Migration-Guide#x-b3–mdc-fields-names-are-no-longer-set
3.6 日志采集监控
上图是Grafana集成Loki后日志查询的搜索页面,支持JSON格式化输出。
3.7 日志搜索与分析
LogQL是Grafana Loki的promql启发的查询语言。https://grafana.com/docs/loki/latest/logql/
它提供了2种查询能力:
- 查询返回的日志行
- 对查询结果进行统计计算
四、完整配置
本方案 logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="5 seconds">
<springProperty scope="context" name="appName" source="spring.application.name" defaultValue="unknown"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %green(%-5level) %blue(%property{PID}) --- [%thread] %cyan(%-50logger{50}) : %msg%n</pattern>
</encoder>
</appender>
<appender name="PROD-STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC+8</timeZone>
</timestamp>
<pattern>
<omitEmptyFields>true</omitEmptyFields>
<pattern>
{
"timestamp": "%date{ISO8601}",
"requestId": "%mdc{requestId}",
"remoteIP": "%mdc{remoteIp}",
"service": "${appName}",
"level": "%level",
"pid": "${PID:-}",
"trace": "%X{X-B3-TraceId:-}",
"span": "%X{X-B3-SpanId:-}",
"parent": "%X{X-B3-ParentSpanId:-}",
"thread": "%thread",
"class": "%logger{60}",
"method": "%method",
"line": "%line",
"message": "#tryJson{%message}"
}
</pattern>
</pattern>
<stackTrace>
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>100</maxDepthPerThrowable>
<maxLength>20480</maxLength>
<rootCauseFirst>true</rootCauseFirst>
</throwableConverter>
</stackTrace>
</providers>
</encoder>
</appender>
<springProfile name="default">
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</springProfile>
<springProfile name="kubernetes">
<root level="INFO">
<appender-ref ref="PROD-STDOUT"/>
</root>
</springProfile>
</configuration>
参考