欢迎来嫖从零开始SpringCloud Alibaba电商系列:
前文我们介绍了如何使用AOP拦截,记录所有访问controller的用户即请求参数、请求的执行结果、执行异常的结果。
今天我们来聊一聊大名鼎鼎的ELK中的ElasticSearch(简称ES),以及如何在将ES与logback结合,存储业务日志。
Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索。”Elasticsearch是分布式的,这意味着索引可以被分成分片,每个分片可以有0个或多个副本。每个节点托管一个或多个分片,并充当协调器将操作委托给正确的分片。再平衡和路由是自动完成的。“相关数据通常存储在同一个索引中,该索引由一个或多个主分片和零个或多个复制分片组成。一旦创建了索引,就不能更改主分片的数量。 ————wiki百科
概括:
ES的安装就跳过了,推荐大家直接用docker一拉,简单配一下映射的路径,然后一run,完事儿。
GET http://127.0.0.1:9200/_cluster/health
返回结果:
{
"cluster_name": "elasticsearch", # 集群名称
"status": "yellow", # 集群状态,分为red、yellow、green。当前因为副本分片未分配所以yellow。
"timed_out": false, # 是否有搜索的超时限制
"number_of_nodes": 1, # 所有节点数
"number_of_data_nodes": 1, # data节点,即存放数据的节点数
"active_primary_shards": 7, # 可用的主分片数(所有index的主分片数之和)
"active_shards": 7, # 所有可用的分片数(包括副本分片)
"relocating_shards": 0, # 迁移分片的数量,例如上面所说新加入节点后会导致各个节点间的分片重新分配
"initializing_shards": 0,
"unassigned_shards": 7, # 未分配的分片数,因为我只有一个节点,副本分片和主分片都放在这里也无法保证数据安全,所以干脆没有副本分片,当多一个节点接入集群时,这些副本分片将被分配
"delayed_unassigned_shards": 0,
"number_of_pending_tasks": 0,
"number_of_in_flight_fetch": 0,
"task_max_waiting_in_queue_millis": 0,
"active_shards_percent_as_number": 50.0
}
http://127.0.0.1:9200/_cat
database
,在6.x及之后的版本索引更类似与mysql的table,因为一个索引中只允许一种类型
(这个类型中定义了本索引内数据所用到的所有字段。)PUT /logs
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
文档document
,即添加一条数据。这条数据的字段由上述索引
定义,如果文档
中的某个字段不存在与索引
中,那么该字段将被自动创建到索引
的元信息中。POST http://127.0.0.1:9200/logs/systemLog
{
"id": "ac6d46ad-b50d-437f-a47d-70562c08e475",
"requestMethod": "UserController下的saveUser",
"requestParam": "id:113213123",
"requestUrl": "127.0.0.2",
"result": "success",
"sourceUrl": "127.0.0.1",
"userDetails": "{\"Details:lele\"}"
"errMessage": "is error",
}
这里字段的含义解释放在下面的java类中。
索引
,即logs的类型结构,可以发现它多了很多字段并自动提供分析得到了字段类型,这些都是上一步创建文档之前生成的,之后再保存同样字段的数据将会直接 使用这些字段(这些字段也称为mappings)。GET http://127.0.0.1:9200/logs
值得一提,除了int、date等一系列基础类型外,字符串类的基础类型还有keyword和text,keyword用于精准匹配,而text类型用于建立倒排索引并提供相似度查找,下次再聊查询
图片中可以看到我这里的每个字段都被自动生成为text类型,但是下面还有一个fields是做什么的?当我们将类型定义为text,es会为这个字段建立倒排索引进行查询,这样就不能对这个字段进行精准查询了,fileds就可以解决这种问题,可以再为该字段定义一个keyword类型。
spring.data.elasticsearch.cluster-name=elasticsearch
spring.data.elasticsearch.repositories.enabled=true
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ApiModel("系统访问日志")
@Document(indexName="logs",type="systemLog")
public class SystemLog {
@Field
@ApiModelProperty("日志id,uuid生成")
private String id;
@Field
@ApiModelProperty("日志类型")
private LogConstant.LogType type;
@Field
@ApiModelProperty("日志记录时间")
private Long logTime;
@Field
@ApiModelProperty("常规日志信息")
private String messgae;
@Field
@ApiModelProperty("来源地址")
private String sourceUrl;
@Field
@ApiModelProperty("访问者信息")
private String userDetails;
@Field
@ApiModelProperty("访问节点")
private String requestUrl;
@Field
@ApiModelProperty("访问方法")
private String requestMethod;
@Field
@ApiModelProperty("请求参数")
private String requestParam;
@Field
@ApiModelProperty("处理成功返回结果:与errMessage二者有一")
private String result;
@Field
@ApiModelProperty("处理失败返回结果:与result二者有一")
private String errMessage;
}```
3. 开启ES的repository层扫描,和mybatis的@mapperscan差不多。
```java
@Configuration
@EnableElasticsearchRepositories(basePackages = "org.lele.common.repository")
public class JPAConfig {
}
@Repository
public interface SystemLogRepository extends ElasticsearchCrudRepository<SystemLog,String> {
}
public class LogConstant {
public enum LogType{
/**
* INFO级别日志
*/
INFO,
/**
* WARN级别日志
*/
WARN,
/**
* ERROR级别日志
*/
ERROR,
/**
* aop日志,在controller层方法之前。
*/
BEFORE,
/**
* aop日志,在controller层方法之后,成功返回时写入。
*/
AFTER_RETURN,
/**
* aop日志,在controller层方法之后,抛异常时写入。
*/
AFTER_EXCEPTION
}
}
@Before("logPointCut()")
public void before(JoinPoint jp) {
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
UserDTO userDTO = sessionUtils.getCurrentUser();
SystemLog log = SystemLog.builder()
.id( UUID.randomUUID().toString() )
.type(LogConstant.LogType.BEFORE )
.logTime( System.currentTimeMillis() )
.sourceUrl( request.getRemoteAddr() )
.userDetails( userDTO.toString() )
.requestUrl( HostNameUtil.getIp() )
.requestMethod( jp.getTarget().getClass().getName() + "." + jp.getSignature().getName() + "()" )
.requestParam( JSONObject.toJSONString( jp.getArgs() ) )
.build();
systemLogRepository.save(log);
}
用户访问日志解决了,其他的日志,如logback的日志如何收集到es中呢?
一般说到ES做日志处理,大家一般应该会想到ELK。
ELK是ES+LogsTash+Kibana(都是elastic公司自家的),通过前面的介绍我们已经知道ES是搜索引擎(兼nosql),kibana是配合ES的可视化界面,LogsTash则是日志收集兼数据处理过滤的地方,是ELK的起点。
LogTash的场景: 当我们的日志输出到文件或kafka等消息队列又或是其他东西里面的时候,LogsTash可以从中获取日志,然后简单处理之后放入ES中。
有了解过Hadoop生态的同学可能会意识到,LogTash和Flume的场景是很类似的。
LogTash的缺点: 无法直接同时收集多节点的日志文件,当然可以通过消息队列但是不够方便,于是elastic又推出了专门面向日志收集的工具集合——Beats,这些都是题外话。
对于我们的系统来说: ELK目前不是必须的(笔者比较穷,机器hold不住了…),所以我们直接采用另外一种方式来对接日志与ES——Logbak Appender。
日志永远是一个系统不可缺少的部分,我们一般项目中或多或少会接触到日志,最多的可能就是logback和log4j,而这两者的区别不过是logback是log4j的升级版,因为两者都是一个作者的作品。
另:slf4j不是日志框架,而是日志适配器,我们使用了slf4j就可以随意切换底层的日志组件,而不会影响到业务的代码逻辑。
Appender : Appender是logback提供的一种可以自定义数据写出的方式,可以写出的console,文件,也可以写出到ES或是其他任何可用java连通的介质中。
talk is cheap,show me the code!
感知
的意思,继承该接口可以让Holder类获取到application实例。
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
/**
* 直接获取applicationContext
* @return applicationContext
*/
public static ApplicationContext getContext() {
return context;
}
/**
* 根据名称获取Bean
*
* @param beanName bean名称
* @return Bean实例
*/
public static Object getBean(String beanName) {
if (context == null || StringUtils.isBlank(beanName)) {
return null;
}
return context.getBean(beanName);
}
/**
* 根据类型获取Bean
*
* @param className bean类型
* @param bean类型
* @return Bean实例
*/
public static <T> T getBean(Class<T> className) {
if (context == null || className == null) {
return null;
}
return context.getBean(className);
}
}
public class ElasticsearchAppender extends AppenderBase<LoggingEvent> implements java.io.Serializable {
private SystemLogRepository systemLogRepository;
@Override
public void stop() {
super.stop();
}
@Override
protected void append(LoggingEvent e) {
try {
doLogging(e);
} catch (Exception exception) {
addError("日志写入ES失败", exception);
} finally {
MDC.clear();
}
}
private void doLogging(LoggingEvent e) {
if (systemLogRepository == null) {
systemLogRepository = ApplicationContextHolder.getBean(SystemLogRepository.class);
if (systemLogRepository == null) {
addWarn("systemLogRepository is null.");
return;
}
}
String errorMessage = buildMessage(e);
Function<Level, LogConstant.LogType> getLogType = level->{
switch( level.toInt() ){
case Level.ERROR_INT:
return LogConstant.LogType.ERROR;
case Level.INFO_INT:
return LogConstant.LogType.INFO;
case Level.WARN_INT:
return LogConstant.LogType.WARN;
default:
return LogConstant.LogType.INFO;
}
};
SystemLog systemLog = SystemLog.builder()
.id( UUID.randomUUID().toString() )
.logTime( e.getTimeStamp() )
.messgae( errorMessage )
.type( getLogType.apply(e.getLevel()) )
.build();
try {
systemLogRepository.save(systemLog);
} catch (Exception ex) {
addError(ex.getMessage());
}
}
//获取完整堆栈
private String buildMessage(LoggingEvent e) {
if (e.getLevel().toInt() == Level.ERROR_INT && e.getThrowableProxy() != null) {
return e.getFormattedMessage() + CoreConstants.LINE_SEPARATOR
+ ThrowableProxyUtil.asString(e.getThrowableProxy());
}
return e.getFormattedMessage();
}
}
<?xml version="1.0" encoding="UTF-8"?>
>
>
>
>%d %5p %40.40c:%4L - %m%n >
>
>
>
>
>
-ref ref="console"/>
-ref ref="elasticAppender"/>
>
>
private Logger logger = LoggerFactory.getLogger( MUserController.class );
@GetMapping
@ApiOperation(value = "查询用户")
public R selectAll(@ApiIgnore Page<MUser> page, MUser mUser) {
logger.info( "我来查询用户了" );
return success(this.mUserService.page(page, new QueryWrapper<>(mUser)));
}
我们获取数据是有目的的,我们受够了在n个机器上反复vim查看日志的操作,所以我们要用更方便的kibana来查看日志信息。
没有环境的朋友仍然推荐docker,一pull一run十分舒坦,这里只介绍如何kibana的简单操作,如何结合我们之前存入的数据来使用。
下面是一段docker-compose的kibana配置,有兴趣的同学可以了解一下compose,极大节约部署时间成本。这里关键点就是kibana要连接上ES,也就是划红线的这部分,无论哪种方式配置,都需要这个参数配置。
kibana的使用需要先建立索引匹配集
,也就是建立一个集合,用来匹配我们的一些索引,笔者这里只是匹配了logs,这是我们存储系统日志的索引。
建好索引就可以在discover
栏目愉快的看日志了,下面是一个全日志的列表,我们可以按照需求来过滤或展示。
第二张图展示的是type(日志类型)为AFTER_RETURN的日志,且只显示result列的结果,日志看起来舒服多了。
kibana还提供了图形展示的功能Visualize
,将数据导入图形模板。
笔者这里简单做了一个图:各种类型的日志各自占比多少。
kibana的介绍就到这里,实际上这对笔者收集并分析查看日志的需求来说,已经足够使用了。
https://github.com/flyChineseBoy/lel-mall/tree/master/mall13