1. 说明
ApiBoot Logging是ApiBoot提供单应用、微服务应用下的请求日志分析框架。在微服务的链路调用过程中,可以记录下每次调用的链路信息。信息可以以日志的形式保存在数据库中,或者也可以自己通过代码将日志保存在其他地方,比如kafka中。
日志的格式大致如下:
[
{
"endTime":1564368219907,
"httpStatus":200,
"parentSpanId":"d9ed5130-b72c-4282-8d18-4a6f7a08275a",
"requestBody":"",
"requestHeaders":{
"api-boot-x-trace-id":"855b0f4d-7667-4e14-ac8d-6b63fb4ef64e",
"host":"localhost:9099",
"connection":"keep-alive",
"api-boot-x-parent-span-id":"d9ed5130-b72c-4282-8d18-4a6f7a08275a",
"accept":"*/*",
"user-agent":"Java/1.8.0_201"
},
"requestIp":"127.0.0.1",
"requestMethod":"GET",
"requestUri":"/levelone",
"responseBody":"levelone",
"responseHeaders":{},
"serviceId":"ahhx_jcpt",
"serviceIp":"192.168.21.101",
"servicePort":"9099",
"spanId":"d1597b46-ec21-4a37-8f40-d39d95d533a8",
"startTime":1564368219904,
"timeConsuming":3,
"traceId":"855b0f4d-7667-4e14-ac8d-6b63fb4ef64e"
}
]
1.1 traceId(链路ID)和spanId(跨度ID)
- 如果一个请求的header信息内包含traceId(链路ID)则加入该链路,如果不存在则生成新的链路信息
- 如果一个请求的header信息内包含spanId(跨度ID),则使用该spanId作为parent spanId,对两个请求进行上下级关联。
简单的理解,通过traceId,可以分析出来,如果几个日志的traceId是一样的,那么这几个调用是属于一条链路的。然后再通过spanId和parentSpanId就可以分析出来这个几个请求的上下级关系,是哪个调用的哪个。
1.2 体系
项目结构主要分为两个部分,一个是ApiBoot Logging,另一个是ApiBoot Logging Admin。
ApiBoot Logging是日志采集端,就是集成了ApiBoot Logging的项目,这个项目中的接口被请求时,就会采集到日志。
ApiBoot Logging Admin是日志收集端,所有ApiBoot Logging的项目采集到的日志都会上报到ApiBoot Logging Admin。
2. 项目中导入ApiBoot Logging
2.1 引入依赖
<!--ApiBoot Logging-->
<dependency>
<groupId>org.minbox.framework</groupId>
<artifactId>api-boot-starter-logging</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
2.2 修改配置文件
logging:
level:
org.minbox.framework.api.boot.plugin.logging: debug
root: info
com.base.web: info
api:
boot:
logging:
admin:
server-address: 127.0.0.1:20004
# 格式化上报日志
format-console-log-json: true
number-of-request-log: 2
# 显示上报日志
show-console-report-log: true
server-address是Logging Admin的地址,Logging Admin是独立的项目,是Logging项目的日志上报地址。这里可以先不管,等到Logging Admin创建完毕,在修改为对于的ip和port
2.3 测试接口
@Controller
public class ApiBootContorller {
@ResponseBody
@GetMapping("/levelone")
public String levelone(){
return "levelone";
}
}
通过postman调用这个接口,就可以在控制台中看到打印的日志。
集成ApiBoot Logging 的步骤只有这些,但是可能会遇到一些问题
spring boot版本问题:
我的项目使用的是spring boot 2.0.1 但是ApiBoot 需要2.1.6,直接引入,启动就会报错。
10:16:58 [localhost-startStop-1] ERROR o.a.c.c.C.[Tomcat].[localhost].[/] - Exception starting filter [apiBootLoggingFilter]
java.lang.AbstractMethodError: null
at org.apache.catalina.core.ApplicationFilterConfig.initFilter(ApplicationFilterConfig.java:285)
at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:112)
at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:4598)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5241)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1421)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1411)
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
于是我将项目升级到2.1.6版本,之后还是无法启动,因为项目原先依赖的kafka无法启动,之后又去升级kafka依赖,以及修改kafka接收数据的代码。
总之,如果一个项目比较复杂,升级spring boot版本还是比较容易碰到坑的。
3. 创建Logging Admin 项目
Logging Admin 项目用来接口日志采集端发送过来的数据,而且一个Logging Admin可以接受多个数据采集端,可以理解为一对多的关系。
3.1 导入依赖
<!--ApiBoot Logging Admin-->
<dependency>
<groupId>org.minbox.framework</groupId>
<artifactId>api-boot-starter-logging-admin</artifactId>
</dependency>
ApiBoot Mybatis Enhance
<dependency>
<groupId>org.minbox.framework</groupId>
<artifactId>api-boot-starter-mybatis-enhance</artifactId>
</dependency>
MySQL驱动
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
Hikari数据源
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
如果不指定依赖的version的话,需要添加依赖管理,如果指定了依赖的具体版本,就不需要添加依赖管理了,上面的ApiBoot Logging引入时也是一样。
依赖管理:
<!--ApiBoot 版本依赖-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.minbox.framework</groupId>
<artifactId>api-boot-dependencies</artifactId>
<version>2.1.2.RELEASE</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
3.2 配置文件
server.port=20004
logging.level.org.minbox.framework.api.boot.plugin.logging=debug
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.username=root
spring.datasource.password=ahhx@123
spring.datasource.url=jdbc:mysql://192.168.220.46:3306/apiboot
# 格式化上报日志
api.boot.logging.admin.format-console-log-json=true
# 显示上报日志
api.boot.logging.admin.show-console-report-log=true
3.3 在数据库中运行sql文件
上面的配置文件中用到了数据库,需要初始化数据库,创建一些表。
表结构如下,将如下语句在对应的数据库中跑一次,即可。
SET NAMES utf8mb4 ;
--
-- Table structure for table `logging_request_logs`
--
DROP TABLE IF EXISTS `logging_request_logs`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `logging_request_logs` (
`lrl_id` varchar(36) NOT NULL COMMENT '主键,UUID',
`lrl_service_detail_id` varchar(36) DEFAULT NULL COMMENT '服务详情编号,关联logging_service_details主键',
`lrl_trace_id` varchar(36) DEFAULT NULL COMMENT '链路ID',
`lrl_parent_span_id` varchar(36) DEFAULT NULL COMMENT '上级跨度ID',
`lrl_span_id` varchar(36) DEFAULT NULL COMMENT '跨度ID',
`lrl_start_time` mediumtext COMMENT '请求开始时间',
`lrl_end_time` mediumtext COMMENT '请求结束时间',
`lrl_http_status` int(11) DEFAULT NULL COMMENT '请求响应状态码',
`lrl_request_body` longtext COMMENT '请求主体内容',
`lrl_request_headers` text COMMENT '请求头信息',
`lrl_request_ip` varchar(30) DEFAULT NULL COMMENT '发起请求客户端的IP地址',
`lrl_request_method` varchar(10) DEFAULT NULL COMMENT '请求方式',
`lrl_request_uri` varchar(200) DEFAULT NULL COMMENT '请求路径',
`lrl_response_body` longtext COMMENT '响应内容',
`lrl_response_headers` text COMMENT '响应头信息',
`lrl_time_consuming` int(11) DEFAULT NULL COMMENT '请求耗时',
`lrl_create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '日志保存时间',
PRIMARY KEY (`lrl_id`),
KEY `logging_request_logs_LRL_SERVICE_DETAIL_ID_index` (`lrl_service_detail_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='请求日志信息表';
--
-- Table structure for table `logging_service_details`
--
DROP TABLE IF EXISTS `logging_service_details`;
SET character_set_client = utf8mb4 ;
CREATE TABLE `logging_service_details` (
`lsd_id` varchar(36) NOT NULL,
`lsd_service_id` varchar(200) DEFAULT NULL COMMENT '上报服务的ID,对应spring.application.name配置值',
`lsd_service_ip` varchar(50) DEFAULT NULL COMMENT '上报服务的IP地址',
`lsd_service_port` int(11) DEFAULT NULL COMMENT '上报服务的端口号',
`lsd_last_report_time` timestamp NULL DEFAULT NULL COMMENT '最后一次上报时间,每次上报更新',
`lsd_create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '首次上报时创建时间',
PRIMARY KEY (`lsd_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='上报日志的客户端服务详情';
3.4 测试
将ApiBoot Logging配置文件中的对应的server-address: 127.0.0.1:20004指定为Logging Admin的ip和port。
使用postman调用ApiBoot Logging中的接口,可以看到的结果是ApiBoot Logging本地的控制台打印出来了日志,同时Logging Admin也收到了日志打印在控制台上并且存储在了数据库中。
4. 链式调用
之前引入ApiBoot Logging 的项目叫做ahhx-jcpt
spring:
application:
name: ahhx-jcpt
再创建一个ApiBoot Logging项目叫做apiboot,项目中提供一个接口
@ResponseBody
@GetMapping(value = "/index")
public String hello(){
return getStringApi.getString();
}
在apiboot中的接口中调用ahhx-jcpt,就达成了链式调用。
需要注意的问题是,目前最新版2.1.2,只支持spring cloud openfeign调用,对于RestTemplate等方式调用,虽然也可以采集到日志,但是并不能正确得到traceId、spanId和parentSpanId。那么就无法分析出链路的调用关系。
关于spring cloud openfeign的简单使用将在后文描述。
使用postman调用apiboot的\index接口,查看ahhx-jcpt和apiboot的控制台,可以看到他们的traceId是一样的,并且ahhx-jcpt的parentSpanId就是apiboot的spanId。
5. spring cloud openfeign最简单使用
5.1 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
5.2 代码
5.2.1 创建一个接口
@FeignClient(name = "ahhx-jcpt", url = "http://localhost:9099")
public interface GetStringApi {
@RequestMapping(value = "/levelone", method = RequestMethod.GET)
String getString();
}
表示这个getString方法会调用http://localhost:9099/levelone接口
5.2.2 调用
@Controller
public class IndexController {
@Autowired
private GetStringApi getStringApi;
@ResponseBody
@GetMapping(value = "/index")
public String hello(){
return getStringApi.getString();
}
}
当/index被调用,就会调用GetStringApi 的getString,而getString就会去调用http://localhost:9099/levelone接口
6. 在Logging Admin端用kafka存储日志,并关闭自带的存储数据库操作
6.1 关闭存储数据库操作
- 删除依赖
<!--ApiBoot Mybatis Enhance-->
<!--<dependency>-->
<!--<groupId>org.minbox.framework</groupId>-->
<!--<artifactId>api-boot-starter-mybatis-enhance</artifactId>-->
<!--</dependency>-->
<!--MySQL驱动-->
<!--<dependency>-->
<!--<groupId>mysql</groupId>-->
<!--<artifactId>mysql-connector-java</artifactId>-->
<!--</dependency>-->
<!--Hikari数据源-->
<!--<dependency>-->
<!--<groupId>com.zaxxer</groupId>-->
<!--<artifactId>HikariCP</artifactId>-->
<!--</dependency>-->
- 删除配置
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.datasource.type=com.zaxxer.hikari.HikariDataSource
#spring.datasource.username=root
#spring.datasource.password=ahhx@123
#spring.datasource.url=jdbc:mysql://192.168.220.46:3306/apiboot
这样admin端,在收到数据以后就只会在控制台打印,不会存数据库
6.2 存储kafka
继承SmartApplicationListener
@Component
public class LocalNoticeSample implements SmartApplicationListener {
@Autowired
private KafkaSender kafkaSender;
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return eventType == ReportLogEvent.class;
}
@Override
public boolean supportsSourceType(Class<?> sourceType) {
return sourceType == LoggingEndpoint.class;
}
/**
* order 值越小执行越靠前
*
* @return
*/
@Override
public int getOrder() {
return 2;
}
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
System.out.println("onApplicationEvent");
ReportLogEvent reportLogEvent = (ReportLogEvent) applicationEvent;
ApiBootLogClientNotice notice = reportLogEvent.getLogClientNotice();
kafkaSender.sendMessage(notice.getLoggers().toString());
}
}
kafka发送代码:
@EnableBinding(Source.class)
@Component
public class KafkaSender {
private final Logger logger = LoggerFactory.getLogger(KafkaSender.class);
@Autowired
private Source source;
public void sendMessage(String msg){
try {
logger.info("准备发送数据到kafka->数据:【message:"+msg+"】");
source.output().send(MessageBuilder.withPayload(msg).build());
}catch (Exception e){
logger.info("准备发送数据到kafka->出错");
e.printStackTrace();
}
}
}
kafka接收代码
@Component
@EnableBinding(Sink.class)
public class KafkaReceive {
private final Logger logger = LoggerFactory.getLogger(KafkaReceive.class);
@StreamListener(Sink.INPUT)
public void process(Message<String> message) throws Exception {
logger.info("监听到kafka数据->输入参数【message:"+message+"】");
String receivedString = message.getPayload();
logger.info("received kafka message : value:"+receivedString);
}
}
7. 追诉
7.1 提取日志
在admin端想要提取日志,需要写一个类集成SmartApplicationListener 即可。
在Logging端目前无法直接提取日志(2.1.2版本),根据作者说明,在下一版本将可以直接提取日志。
并且据说下一个版本admin端将提供可视界面,以及日志上报到admin可以关闭,让日志不上报。
8. 相关链接
还有很多的功能这里并没有介绍,具体可查看如下地址:
http://apiboot.minbox.io/zh-cn/docs/api-boot-logging-admin.html
https://github.com/hengboy/api-boot
https://gitee.com/hengboy/api-boot
非常感谢作者
@恒宇少年https://github.com/hengboy提供的帮助