ELK 海量日志收集架构设计

 一、架构图: 

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,即它是日志库的一个统一规范接口,其下的实现有很多,如:Log4jLog4j2LogBack

(2)Log4j2Log4j 全称是 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所在项目、FileBeat192.168.31.102FileBeat 安装与配置
kafka192.168.31.101kafka安装及配置
logstash 192.169.31.103logstash 的基础语法与使用
Elasticsearch192.168.31.104

Elasticsearch 安装

Elasticsearch集群搭建

Elasticsearch 安装 X-pack

Xpack-Watchers基本语法与使用

kibana192.168.31.105Kibana 的安装

二、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):

教程:  logstash 的基础语法与使用

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,或扫描下方二维码,谢谢关注!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值