一、架构图:
1. ELK 技术栈架构设计图:
从左往右看,
Beats
:主要是使用Filebeat
,用于收集日志,将收集后的日志数据发送给 Kafka,充当 Kafka 的生产者Kafka
:高性能消息队列,主要起缓冲层的作用Logstash
:数据采集引擎,可以从数据库采集数据到 ES 中,起筛选、过滤日志数据作用ElasticSearch
和Kibana
:展示数据
下面是具体实现流程:
Log4j2
:使用Log4j2
的原因是其性能更好,底层使用无锁并行框架,缺点是服务器的性能需要高一些app.log
:存储全量日志error.log
:存储异常日志xpack-watch
:通过触发器做一个错误日志的上报和告警功能,同过对接 api,推送到对应的负责人微信之类
2. slf4j
与 Log4j2
的区别:
(1)slf4j
:全称是 simple log facade for java
,即它是日志库的一个统一规范接口,其下的实现有很多,如:Log4j
,Log4j2
,LogBack
(2)Log4j2
:Log4j
全称是 Log for java
,即是上面接口的一个实现,Log4j2
是 Log4j
的升级版本,提高日志输出的吞吐量
大致流程:使用log4j2记录日志,其中 app.log 是全量日志,error.log 是错误日志,通过 filebeat 采集日志数据,这里 filebeat 相当于生产者,将采集到的数据以消息的形式发送到 kafka 节点,logstash 相当于消费者,获取kafka节点上的数据并过滤,过滤后的数据写入到 es, 通过Kibana进行展示,最后在使用Xpack-Watcher进行对日志的监控,并使用 trigger-shell 触发告警 到对应的应用程序或者企业微信。
选择 log4j2 ,因为其内部用到了高性能队列 disruptor,因此配置上也相对吃内存,建议使用高配置硬件,filebeat用于收集日志,Kafka在这里可以做一个应对海量数据的缓冲。
3. 服务器与 ELK 结构
ELK 组件 | 服务器 IP | 安装教程 |
log4j2所在项目、FileBeat | 192.168.31.102 | FileBeat 安装与配置 |
kafka | 192.168.31.101 | kafka安装及配置 |
logstash | 192.169.31.103 | logstash 的基础语法与使用 |
Elasticsearch | 192.168.31.104 | |
kibana | 192.168.31.105 | Kibana 的安装 |
二、log4j2结合SpringBoot实现日志输出
以下是log4j2 实现日志输出的分析图
下面使用log4j2结合SpringBoot实现日志输出
1. 添加依赖
<!-- log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.4</version>
</dependency>
2. 配置文件 application.properties
server.servlet.context-path=/
server.port=8001
spring.application.name=collector
spring.http.encoding.charset=UTF-8
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.jackson.default-property-inclusion=NON_NULL
2. 日志配置文件 log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO" schema="Log4J-V2.0.xsd" monitorInterval="600" >
<Properties>
<Property name="LOG_HOME">logs</Property> <!-- 日志文件所在的目录名 -->
<property name="FILE_NAME">collector</property> <!-- 日志文件名 -->
<!-- 日志输出的格式 -->
<property name="patternLayout">[%d{yyyy-MM-dd'T'HH:mm:ss.SSSZZ}] [%level{length=5}] [%thread-%tid] [%logger] [%X{hostName}] [%X{ip}] [%X{applicationName}] [%F,%L,%C,%M] [%m] ## '%ex'%n</property>
</Properties>
<!-- Appenders中声明了三个输出组件:控制台、app-${FILE_NAME}.log、error-${FILE_NAME}.log-->
<Appenders>
<!-- 1.输出到控制台 -->
<Console name="CONSOLE" target="SYSTEM_OUT">
<!-- 日志输出的格式 -->
<PatternLayout pattern="${patternLayout}"/>
</Console>
<!-- 2. 输出到${LOG_HOME}/app-${FILE_NAME}.log 文件中-->
<RollingRandomAccessFile name="appAppender" fileName="${LOG_HOME}/app-${FILE_NAME}.log" filePattern="${LOG_HOME}/app-${FILE_NAME}-%d{yyyy-MM-dd}-%i.log" > <!-- filePattern: 文件名命名格式-->
<PatternLayout pattern="${patternLayout}" />
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="500MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingRandomAccessFile>
<!-- 3. 错误日志输出到 ${LOG_HOME}/error-${FILE_NAME}.log 文件中 -->
<RollingRandomAccessFile name="errorAppender" fileName="${LOG_HOME}/error-${FILE_NAME}.log" filePattern="${LOG_HOME}/error-${FILE_NAME}-%d{yyyy-MM-dd}-%i.log" >
<PatternLayout pattern="${patternLayout}" />
<!-- 日志过滤条件:warn级别以上的日志才会输入到该文件中 -->
<Filters>
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="500MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<!-- 业务相关 异步logger (混合异步日志记录器的配置)-->
<AsyncLogger name="com.didiok" level="info" includeLocation="true">
<!-- 将 appAppender 再包裹一层AsyncLogger标签-->
<AppenderRef ref="appAppender"/>
</AsyncLogger>
<AsyncLogger name="com.didiok" level="info" includeLocation="true">
<!-- 将 errorAppender 再包裹一层AsyncLogger标签-->
<AppenderRef ref="errorAppender"/>
</AsyncLogger>
<!-- 在 root 标签中再声明一下上面appender标签里加的三个输出组件 -->
<Root level="info">
<Appender-Ref ref="CONSOLE"/>
<Appender-Ref ref="appAppender"/>
<AppenderRef ref="errorAppender"/>
</Root>
</Loggers>
</Configuration>
(1)如下是输出日志的格式
[%d{yyyy-MM-dd'T'HH:mm:ss.SSSZZ}] [%level{length=5}] [%thread-%tid] [%logger] [%X{hostName}] [%X{ip}] [%X{applicationName}] [%F,%L,%C,%M] [%m] ## '%ex'%n
其代表的各自意义是:
[%d{yyyy-MM-dd'T'HH:mm:ss.SSSZZ}]
:时间,UTC美国的时间,因为后期elk也是使用这个时区[%level{length=5}]
:日志级别[%thread-%tid]
:线程id[%logger]
:创建对应 logger 实例传入的 class[%X{hostName}]
:在 MDC 中自定义的字段,当前应用主机名称[%X{ip}]
:在 MDC 中自定义的字段,当前应用的 IP[%X{applicationName}]
:在 MDC 中自定义的字段,当前应用的 applicationName[%F,%L,%C,%M]
:%F
表示当前输出日志的文件名、%L
表示是日志输出所在的行数、%C
表示当前输出日志的类名、%M
表示当前输出日志所在的方法名[%m]
:日志输出的自定义内容##
:自己编的特殊约定分隔符,将普通信息和错误堆栈分隔开'%ex'
:错误信息,其中单引号是特殊约定,用于包裹异常信息%n
换行符
(2)MDC
MDC 线程变量,它的作用就是自定义变量,可以看做为一个 Map
对象,和ThreadLocal也很类似。供日志输出时使用,如上自定义输出格式的 [%X{hostName}]
,其中 %X
表示的就是自定义变量,使用 MDC 很简单,只要在调用日志输出之前,调用 MDC 的 put
方法设置变量即可,如果没有调用 put
方法,则输出日志时,该变量为空。
MDC.put("hostName", NetUtil.getLocalHostName());
MDC.put("ip", NetUtil.getLocalIp());
MDC.put("applicationName", applicationName);
3. 测试代码
@Slf4j
@RestController
public class IndexController {
/**
* [%d{yyyy-MM-dd'T'HH:mm:ss.SSSZZ}]
* [%level{length=5}]
* [%thread-%tid]
* [%logger]
* [%X{hostName}]
* [%X{ip}]
* [%X{applicationName}]
* [%F,%L,%C,%M]
* [%m] ## '%ex'%n
* -----------------------------------------------
* [2019-09-18T14:42:51.451+08:00]
* [INFO]
* [main-1]
* [org.springframework.boot.web.embedded.tomcat.TomcatWebServer]
* []
* []
* []
* [TomcatWebServer.java,90,org.springframework.boot.web.embedded.tomcat.TomcatWebServer,initialize]
* [Tomcat initialized with port(s): 8001 (http)] ## ''
*
* ["message",
* "\[%{NOTSPACE:currentDateTime}\]
* \[%{NOTSPACE:level}\]
* \[%{NOTSPACE:thread-id}\]
* \[%{NOTSPACE:class}\]
* \[%{DATA:hostName}\]
* \[%{DATA:ip}\]
* \[%{DATA:applicationName}\]
* \[%{DATA:location}\]
* \[%{DATA:messageInfo}\]
* ## (\'\'|%{QUOTEDSTRING:throwable})"]
* @return
*/
@RequestMapping(value = "/index")
public String index() {
InputMDC.putMDC();
log.info("我是一条info日志");
log.warn("我是一条warn日志");
log.error("我是一条error日志");
return "idx";
}
@RequestMapping(value = "/err")
public String err() {
InputMDC.putMDC();
try {
int a = 1/0;
} catch (Exception e) {
log.error("算术异常", e);
}
return "err";
}
}
其中,InputMDC.putMDC()方法的代码如下:
@Component
public class InputMDC implements EnvironmentAware {
private static Environment environment;
@Override
public void setEnvironment(Environment environment) {
InputMDC.environment = environment;
}
public static void putMDC() {
MDC.put("hostName", NetUtil.getLocalHostName());
MDC.put("ip", NetUtil.getLocalIp());
// 从环境变量 environment 中获取 applicationName
// 在 application.properties 配置文件中配置的东西都会加载到 environment 中
MDC.put("applicationName", environment.getProperty("spring.application.name"));
}
}
控制台输出的日志结果:
然后在 pom.xml 文件中加入 打包 时用到的配置:
<build>
<finalName>collector</finalName>
<!-- 打包时包含properties、xml -->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<!-- 是否替换资源中的属性-->
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.bfxy.collector.Application</mainClass>
</configuration>
</plugin>
</plugins>
</build>
这时候,就可以将 项目打包成 collector.jar 部署到linux服务器(服务器的ip:192.168.31.102)上,并使用命令启动该项目:
java -jar collector.jar & # &是指后台运行,即使用 ctrl+c 命令时,程序不会被中断
可以使用 jps -l 命令查看java进程状态中有没有 collector.jar 来判断是否启动成功!
然后在浏览器中访问 http://192.168.31.102:8001/index 和 http://192.168.31.102:8001/err 这两个接口,从而生成日志文件,这时候可以到 项目的 logs/ 目录下看到有两个文件: app-collector.log 和 error-collector.log 文件,
接下来就可以使用 FileBeat 采集日志文件了!
三、FileBeat安装与配置
1. 安装与配置
FileBeat的安装(所在服务器的ip:192.168.31.102):FileBeat 安装与配置
为了能够实现采集日志文件,并将采集到的数据发送到 kafka 中,这里需要将 FileBeat 的配置文件修改如下, vim /usr/local/filebeat-7.6.2/filebeat.yml ,
###################### Filebeat Configuration Example #########################
filebeat.inputs:
- input_type: log
paths:
## 定义了日志文件路径,可以采用模糊匹配模式,如*.log
- /usr/local/logs/app-collector.log
#定义写入 ES 时的 _type 值
document_type: "app-log"
multiline:
#pattern: '^\s*(\d{4}|\d{2})\-(\d{2}|[a-zA-Z]{3})\-(\d{2}|\d{4})' # 指定匹配的表达式(匹配以 2017-11-15 08:04:23:889 时间格式开头的字符串)
pattern: '^\[' # 指定匹配的表达式(匹配以 [ 开头的字符串)
negate: true # 是否需要匹配到
match: after # 不匹配的行,合并到上一行的末尾
max_lines: 2000 # 最大的行数
timeout: 2s # 如果在规定时间没有新的日志事件就不等待后面的日志
fields: ## topic 对应的消息字段或自定义增加的字段
logbiz: collector
logtopic: app-log-collector ## 按服务划分用作kafka topic,会在logstash filter 过滤数据时候作为 判断参数 [fields][logtopic]
evn: dev
- input_type: log
paths:
## 定义了日志文件路径,可以采用模糊匹配模式,如*.log
- /usr/local/logs/error-collector.log
#定义写入 ES 时的 _type 值
document_type: "error-log"
multiline:
#pattern: '^\s*(\d{4}|\d{2})\-(\d{2}|[a-zA-Z]{3})\-(\d{2}|\d{4})' # 指定匹配的表达式(匹配以 2017-11-15 08:04:23:889 时间格式开头的字符串)
pattern: '^\[' # 指定匹配的表达式(匹配以 [ 开头的字符串)
negate: true # 是否匹配到
match: after # 不匹配的行,合并到上一行的末尾
max_lines: 2000 # 最大的行数
timeout: 2s # 如果在规定时间没有新的日志事件就不等待后面的日志,直接进行推送操作
fields: ## topic 对应的消息字段或自定义增加的字段
logbiz: collector
logtopic: error-log-collector ## 按服务划分用作kafka topic
evn: dev
output.kafka: ## filebeat 支持多种输出,支持向 kafka,logstash,elasticsearch 输出数据,此处设置数据输出到 kafka。
enabled: true ## 启动这个模块
hosts: ["192.168.31.101:9092"] ## 地址
topic: '%{[fields.logtopic]}' ## 主题(使用动态变量)
partition.hash: ## kafka 分区 hash 规则
reachable_only: true
compression: gzip ## 数据压缩
max_message_bytes: 1000000 ## 最大容量
required_acks: 1 ## 是否需要 ack,有三个值可以取:0、1、-1
logging.to_files: true
四. 启动 kafka
1. kafka的安装(所在服务器的ip:192.168.31.101):
安装教程:kafka安装及配置
然后使用命令启动 kafka:(注意如果kafka依赖了zookeeper,需要先启动zookeeper)
/usr/local/kafka-3.2.1/bin/kafka-server-start.sh /usr/local/kafka-3.2.1/config/server.properties &
2. 创建两个topic:
# 创建 topic
./kafka-topics.sh --bootstrap-server 192.168.31.101:9092 --create --topic app-log-collector --partitions 2 --replication-factor 1
./kafka-topics.sh --bootstrap-server 192.168.31.101:9092 --create --topic error-log-collector --partitions 2 --replication-factor 1
查看 topic 列表:
# 查看 kafka 中topic列表
./kafka-topics.sh --bootstrap-server 192.168.31.101:9092 --list
五、启动 FileBeat
1. 启动命令:
# 启动filebeat:
cd /usr/local/filebeat-7.6.2/
./filebeat &
2. 查看kafka上有没有接收到消息
使用浏览器访问接口:http://192.168.31.102:8001/index 和 http://192.168.31.102:8001/err 这两个接口,从而生成日志文件,如果 filebeat 运行正常的话,应该会采集数据发送到 kafka 上,所以进入 /usr/local/kafka-3.2.1/kafka-logs/ 目录下,查看 app-log-collector 和 error-log-collector 对应的目录下,有没有数据生成:
接下来就是配置logstash了。
六、Logstash
1. logstash的安装与基本语法(所在服务器的ip:192.168.31.103):
1. logstash消费日志框架
3. 配置文件
在 /usr/local/logstash-7.6.2/
目录下新建一个 script
文件夹用于存放对接 Kafka 的配置文件。
以下是在 script
文件夹下创建的 logstash-script.conf
配置文件:
## multiline 插件也可以用于其他类似的堆栈式信息,比如 linux 的内核日志。
input {
# 订阅 kafka 的topic
kafka {
topics_pattern => "app-log-.*" ## kafka 主题 topic
bootstrap_servers => "192.168.31.101:9092" ## kafka 地址
codec => json ## 数据格式
consumer_threads => 2 ## 增加consumer的并行消费线程数(数值可以设置为 kafka 的分片数)
decorate_events => true
group_id => "app-log-group" ## kafka 组别
}
kafka {
topics_pattern => "error-log-.*" ## kafka 主题 topic
bootstrap_servers => "192.168.31.101:9092" ## kafka 地址
codec => json ## 数据格式
consumer_threads => 2 ## 增加consumer的并行消费线程数(数值可以设置为 kafka 的分片数)
decorate_events => true
group_id => "error-log-group" ## kafka 组别
}
}
# 过滤数据
filter {
## 时区转换,这里使用 ruby 语言,因为 logstash 本身是东八区的,这个时区比北京时间慢8小时,所以这里采用 ruby 语言设置为北京时区
ruby {
code => "event.set('index_time',event.timestamp.time.localtime.strftime('%Y.%m.%d'))"
}
## [fields][logtopic] 这个是从 FileBeat 定义传入 Kafka 的
if "app-log" in [fields][logtopic]{
grok {
## 表达式,这里对应的是Springboot输出的日志格式
match => ["message", "\[%{NOTSPACE:currentDateTime}\] \[%{NOTSPACE:level}\] \[%{NOTSPACE:thread-id}\] \[%{NOTSPACE:class}\] \[%{DATA:hostName}\] \[%{DATA:ip}\] \[%{DATA:applicationName}\] \[%{DATA:location}\] \[%{DATA:messageInfo}\] ## (\'\'|%{QUOTEDSTRING:throwable})"]
}
}
## [fields][logtopic] 这个是从 FileBeat 定义传入 Kafka 的
if "error-log" in [fields][logtopic]{
grok {
## 表达式
match => ["message", "\[%{NOTSPACE:currentDateTime}\] \[%{NOTSPACE:level}\] \[%{NOTSPACE:thread-id}\] \[%{NOTSPACE:class}\] \[%{DATA:hostName}\] \[%{DATA:ip}\] \[%{DATA:applicationName}\] \[%{DATA:location}\] \[%{DATA:messageInfo}\] ## (\'\'|%{QUOTEDSTRING:throwable})"]
}
}
}
## 测试输出到控制台:
## 命令行输入 ./logstash -f /usr/local/logstash-6.4.3/script/logstash-script.conf --verbose --debug
output {
stdout { codec => rubydebug }
}
以上是 logstash 配置信息,配置监听kafka的topic,这里暂时将数据输出到 控制台, consumer_threads 设置为和 topic的partition 数量一样。
这里使用 logstash 作为 kafka 的消费者角色,用于接收 kafka 的消息,经过过滤之后将数据传输到Elasticsearch,不过这里暂时只将数据传输到控制台。
查看一下配置文件 logstash-script.conf
中的过滤规则,这需要结合 log4j2 中定义日志输出格式一起看:
["message", "\[%{NOTSPACE:currentDateTime}\] \[%{NOTSPACE:level}\] \[%{NOTSPACE:thread-id}\] \[%{NOTSPACE:class}\] \[%{DATA:hostName}\] \[%{DATA:ip}\] \[%{DATA:applicationName}\] \[%{DATA:location}\] \[%{DATA:messageInfo}\] ## (\'\'|%{QUOTEDSTRING:throwable})"]
- "message":logstash 固定的格式,统一叫传入的数据为 message
- \[%{NOTSPACE:currentDateTime}\]:匹配 [ 为开头,] 为结尾,NOTSPACE 表示不能有空格,赋值变量名为 currentDateTime
- \[%{NOTSPACE:level}\]:匹配 [ 为开头,] 为结尾,NOTSPACE 表示不能有空格,赋值变量名为 level,日志级别
- \[%{NOTSPACE:thread-id}\]:匹配 [ 为开头,] 为结尾,NOTSPACE 表示不能有空格,赋值变量名为 thread-id,线程ID
- \[%{NOTSPACE:class}\]:匹配 [ 为开头,] 为结尾,NOTSPACE 表示不能有空格,赋值变量名为 class,创建对应 logger 实例传入的 class
- \[%{DATA:hostName}\]:匹配 [ 为开头,] 为结尾,DATA 表示数据,可为空,赋值变量名为 level,当前应用主机名称
- \[%{DATA:ip}\]:匹配 [ 为开头,] 为结尾,DATA 表示数据,可为空,赋值变量名为 level,当前应用的 IP
- \[%{DATA:applicationName}\]:匹配 [ 为开头,] 为结尾,DATA 表示数据,可为空,赋值变量名为 level,当前应用的 applicationName
- \[%{DATA:location}\]:匹配 [ 为开头,] 为结尾,DATA 表示数据,可为空,赋值变量名为 location
- \[%{DATA:messageInfo}\]:匹配 [ 为开头,] 为结尾,DATA 表示数据,可为空,赋值变量名为 messageInfo,日志输出的自定义内容
- (\'\'|%{QUOTEDSTRING:throwable}):两个 ' 单引号之间的 | 表示,之间可为空,不为空就是 throwable 异常信息
4. 启动
启动 logstash 命令:
# 测试配置文件是否正常
/usr/local/logstash-6.6.0/bin/logstash -f /usr/local/logstash-6.6.0/script/logstash-script.conf -t
# 启动
/usr/local/logstash-7.6.2/bin/logstash -f /usr/local/logstash-7.6.2/script/logstash-script.conf
# 后台启动
nohub /usr/local/logstash-6.6.0/bin/logstash -f /usr/local/logstash-6.6.0/script/logstash-script.conf &
## 如果测试时,想要控制台输出debug级别的日志,输入以下命令
/usr/local/logstash-7.6.2/bin/logstash -f /usr/local/logstash-7.6.2/script/logstash-script.conf --verbose --debug
5. 观察控制台输出
启动之后,观察控制台有没有输出“logstash消费kafka的消息”相关日志,之后,在浏览器中访问接口:http://192.168.31.102:8001/index 和 http://192.168.31.102:8001/err 这两个接口,从而生成日志文件,并观察 logstash 的控制台,有没有消费kafka消息的日志。
并在 kafka 节点服务器上输入以下命令观察对应 topic 上的消息有没有被消费掉:
# 查看消费者组 app-log-group、error-log-group的消费进度
/usr/local/kafka-3.2.1/bin/kafka-consumer-groups.sh --bootstrap-server 192.168.31.101:9092 --describe --group app-log-group
/usr/local/kafka-3.2.1/bin/kafka-consumer-groups.sh --bootstrap-server 192.168.31.101:9092 --describe --group error-log-group
七、Elasticsearch 与 Kibana
1. 安装
Elasticsearch 安装教程(所在服务器ip:192.168.31.104):Elasticsearch 安装
Elasticsearch 安装 Xpack:Elasticsearch 安装 X-pack
Kibana 安装教程(所在服务器ip:192.168.31.105):Kibana 的安装
2. 修改 logstash 的配置文件
vim /usr/local/logstash-7.6.2/script/logstash-script.conf ,增加输出到 Elasticsearch 的配置内容:
## elasticsearch:
output {
if "app-log" in [fields][logtopic]{
## es插件
elasticsearch {
# es服务地址
hosts => ["192.168.31.104:9200"]
## 索引名,%{index_time} 是由上面配置的 ruby 脚本定义的日期时间,即每天生成一个索引
index => "app-log-%{[fields][logbiz]}-%{index_time}"
# 是否嗅探集群ip:一般设置true
# 只需要知道一台 elasticsearch 的地址,就可以访问这一台对应的整个 elasticsearch 集群
sniffing => true
# logstash默认自带一个mapping模板,进行模板覆盖
template_overwrite => true
# 如果为 ES 内置账号elastic设置了密码,则需要增加以下两行,具体如何设置密码可参考 https://blog.csdn.net/Qynwang/article/details/130731688中的1.4节
user => "elastic"
password => "123456" # elastic账号对应的密码为123456
}
}
if "error-log" in [fields][logtopic]{
elasticsearch {
hosts => ["192.168.31.104:9200"]
index => "error-log-%{[fields][logbiz]}-%{index_time}"
sniffing => true
template_overwrite => true
# 如果为 ES 内置账号elastic设置了密码,则需要增加以下两行,具体如何设置密码可参考 https://blog.csdn.net/Qynwang/article/details/130731688中的1.4节
user => "elastic"
password => "123456" # elastic账号对应的密码为123456
}
}
}
完整的 logstash-script.conf 为:
## multiline 插件也可以用于其他类似的堆栈式信息,比如 linux 的内核日志。
input {
# 订阅 kafka 的topic
kafka {
topics_pattern => "app-log-.*" ## kafka 主题 topic
bootstrap_servers => "192.168.31.101:9092" ## kafka 地址
codec => json ## 数据格式
consumer_threads => 2 ## 增加consumer的并行消费线程数(数值可以设置为 kafka 的分片数)
decorate_events => true
group_id => "app-log-group" ## kafka 组别
}
kafka {
topics_pattern => "error-log-.*" ## kafka 主题 topic
bootstrap_servers => "192.168.31.101:9092" ## kafka 地址
codec => json ## 数据格式
consumer_threads => 2 ## 增加consumer的并行消费线程数(数值可以设置为 kafka 的分片数)
decorate_events => true
group_id => "error-log-group" ## kafka 组别
}
}
# 过滤数据
filter {
## 时区转换,这里使用 ruby 语言,因为 logstash 本身是东八区的,这个时区比北京时间慢8小时,所以这里采用 ruby 语言设置为北京时区
ruby {
code => "event.set('index_time',event.timestamp.time.localtime.strftime('%Y.%m.%d'))"
}
## [fields][logtopic] 这个是从 FileBeat 定义传入 Kafka 的
if "app-log" in [fields][logtopic]{
grok {
## 表达式,这里对应的是Springboot输出的日志格式
match => ["message", "\[%{NOTSPACE:currentDateTime}\] \[%{NOTSPACE:level}\] \[%{NOTSPACE:thread-id}\] \[%{NOTSPACE:class}\] \[%{DATA:hostName}\] \[%{DATA:ip}\] \[%{DATA:applicationName}\] \[%{DATA:location}\] \[%{DATA:messageInfo}\] ## (\'\'|%{QUOTEDSTRING:throwable})"]
}
}
## [fields][logtopic] 这个是从 FileBeat 定义传入 Kafka 的
if "error-log" in [fields][logtopic]{
grok {
## 表达式
match => ["message", "\[%{NOTSPACE:currentDateTime}\] \[%{NOTSPACE:level}\] \[%{NOTSPACE:thread-id}\] \[%{NOTSPACE:class}\] \[%{DATA:hostName}\] \[%{DATA:ip}\] \[%{DATA:applicationName}\] \[%{DATA:location}\] \[%{DATA:messageInfo}\] ## (\'\'|%{QUOTEDSTRING:throwable})"]
}
}
}
## 测试输出到控制台:
## 命令行输入 ./logstash -f /usr/local/logstash-6.4.3/script/logstash-script.conf --verbose --debug
output {
stdout { codec => rubydebug }
}
## elasticsearch:
output {
if "app-log" in [fields][logtopic]{
## es插件
elasticsearch {
# es服务地址
hosts => ["192.168.31.104:9200"]
## 索引名,%{index_time} 是由上面配置的 ruby 脚本定义的日期时间,即每天生成一个索引
index => "app-log-%{[fields][logbiz]}-%{index_time}"
# 是否嗅探集群ip:一般设置true
# 只需要知道一台 elasticsearch 的地址,就可以访问这一台对应的整个 elasticsearch 集群
sniffing => true
# logstash默认自带一个mapping模板,进行模板覆盖
template_overwrite => true
# 如果为 ES 内置账号elastic设置了密码,则需要增加以下两行,具体如何设置密码可参考 https://blog.csdn.net/Qynwang/article/details/130731688中的1.4节
user => "elastic"
password => "123456" # elastic账号对应的密码为123456
}
}
if "error-log" in [fields][logtopic]{
elasticsearch {
hosts => ["192.168.31.104:9200"]
index => "error-log-%{[fields][logbiz]}-%{index_time}"
sniffing => true
template_overwrite => true
# 如果为 ES 内置账号elastic设置了密码,则需要增加以下两行,具体如何设置密码可参考 https://blog.csdn.net/Qynwang/article/details/130731688中的1.4节
user => "elastic"
password => "123456" # elastic账号对应的密码为123456
}
}
}
2. 创建 Elasticsearch 的索引模板(可使用postman创建也可以使用kinbana可视化界面)
创建一个索引模板,当 error-log-* 这类的索引创建的时候就会使用这个新模版,创建这个模板的目的主要是为了设置 level 这个字段的 type 为 keyword,由于模板中用到了 ik_max_word,所以需要在 elasticsearch 中安装 IKAnalyzer 中文分词器插件 :
PUT _template/error-log-
{
"index_patterns": "error-log-*",
"order": 0,
"settings": {
"index": {
"refresh_interval": "5s"
}
},
"mappings": {
"dynamic_templates": [
{
"message_field": {
"match_mapping_type": "string",
"path_match": "message",
"mapping": {
"norms": false,
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
}
}
},
{
"throwable_field": {
"match_mapping_type": "string",
"path_match": "throwable",
"mapping": {
"norms": false,
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
}
}
},
{
"string_fields": {
"match_mapping_type": "string",
"match": "*",
"mapping": {
"norms": false,
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
],
"properties": {
"hostName": {
"type": "keyword"
},
"ip": {
"type": "ip"
},
"level": {
"type": "keyword"
},
"currentDateTime": {
"type": "date"
}
}
}
}
3. 编写 watcher 脚本
编写一个用于监控 error 日志并触发告警消息的脚本:
PUT /_watcher/watch/error_log_collector_watcher
{
"trigger": {
"schedule": {
"interval": "5s"
}
},
"input": {
"search": {
"request": {
"indices": ["<error-log-collector-{now+8h/d}>"],
"body": {
"size": 0,
"query": {
"bool": {
"must": [
{
"term": {"level": "ERROR"}
}
],
"filter": {
"range": {
"currentDateTime": {
"gt": "now-30s" , "lt": "now"
}
}
}
}
}
}
}
}
},
"condition": {
"compare": {
"ctx.payload.hits.total": {
"gt": 0
}
}
},
"transform": {
"search": {
"request": {
"indices": ["<error-log-collector-{now+8h/d}>"],
"body": {
"size": 1,
"query": {
"bool": {
"must": [
{
"term": {"level": "ERROR"}
}
],
"filter": {
"range": {
"currentDateTime": {
"gt": "now-30s" , "lt": "now"
}
}
}
}
},
"sort": [
{
"currentDateTime": {
"order": "desc"
}
}
]
}
}
}
},
"actions": {
"test_error": {
"webhook" : {
"method" : "POST",
"url" : "http://192.168.31.102:8001/accurateWatch",
"body" : "{\"title\": \"异常错误告警\", \"applicationName\": \"{{#ctx.payload.hits.hits}}{{_source.applicationName}}{{/ctx.payload.hits.hits}}\", \"level\":\"告警级别P1\", \"body\": \"{{#ctx.payload.hits.hits}}{{_source.messageInfo}}{{/ctx.payload.hits.hits}}\", \"executionTime\": \"{{#ctx.payload.hits.hits}}{{_source.currentDateTime}}{{/ctx.payload.hits.hits}}\"}"
}
}
}
}
对创建的 watcher 进行增删改查:
# 查看一个watcher
#
GET /_watcher/watch/error_log_collector_watcher
#删除一个watcher
DELETE /_watcher/watch/error_log_collector_watcher
#执行watcher
POST /_watcher/watch/error_log_collector_watcher/_execute
#查看历史执行记录
GET /.watcher-history*/_search?pretty
{
"sort" : [
{ "result.execution_time" : "desc" }
],
"query": {
"match": {
"watch_id": "error_log_collector_watcher"
}
}
}
也可以直接查看索引文件的数据记录,用于和watcher输出结果作对比,看看两边是否一致:
# 直接查看索引文件的数据记录,用于和watcher输出结果作对比,看看两边是否一致
GET error-log-collector-2023.05.19/_search?size=10
{
"query": {
"match": {
"level": "ERROR"
}
}
,
"sort": [
{
"currentDateTime": {
"order": "desc"
}
}
]
}
4. 脚本中当触发告警时,通知的接口为 http://192.168.31.102:8001/accurateWatch
下面在 collector 项目中编写这个接口 /accurateWatch:
@RestController
public class WatcherController {
@RequestMapping(value ="/accurateWatch")
public String watch(@RequestBody AccurateWatcherMessage accurateWatcherMessage) {
String ret = JSON.toJSONString(accurateWatcherMessage);
System.err.println("----告警内容----:" + ret);
return "is watched" + ret;
}
}
4. 将 collector 项目重新打包上传:
先将原来的 collector.jar 关掉:
jps -l
kill -9 30137 # 30137 是collector.jar项目运行的进程号
然后将新的 collector.jar 上传到linux,并启动
java -jar collector.jar &
5. 测试告警通知
使用浏览器访问接口:http://192.168.31.102:8001/index 和 http://192.168.31.102:8001/err 这两个接口,然后在 collector.jar 所在的控制台中查看有没有告警信息输出。
微信公众号:JavaZhiZhe,或扫描下方二维码,谢谢关注!