在运维项目的时候,一旦出了什么问题就非常的头痛,无论是查找日志还是分析日志错误,都是令人烦躁的。
告别传统的服务器日志打捞和日志分析,搭建自动化日志收集、分析工具ELK。
elasticsearch
Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。作为 Elastic Stack 的核心,Elasticsearch 会集中存储您的数据,让您飞快完成搜索,微调相关性,进行强大的分析,并轻松缩放规模。
Elasticsearch:官方分布式搜索和分析引擎 | Elastic
Docker安装elasticsearch
docker-compose.yml
version: '3'
services:
elasticsearch:
image: elasticsearch:7.14.2 #镜像
container_name: elasticsearch #定义容器名称
restart: always #开机启动,失败也会一直重启
environment:
- "cluster.name=elasticsearch" #设置集群名称为elasticsearch
- "discovery.type=single-node" #以单一节点模式启动
- "ES_JAVA_OPTS=-Xms512m -Xmx1024m" #设置使用jvm内存大小
volumes:
- ./plugins:/usr/share/elasticsearch/plugins #插件文件挂载
- es_data:/usr/share/elasticsearch/data #数据文件挂载
- docker.elastic.co/elasticsearch/elasticsearch:7.14.2 #数据文件挂载
ports:
- 9200:9200
在当前yml文件夹使用docker-compose命令启动es,访问ip:9200,看到输出json即可
logstash
Logstash 是免费且开放的服务器端数据处理管道,能够从多个来源采集数据,转换数据,然后将数据发送到您最喜欢的“存储库”中。
Docker安装logstash
mkdir /home/mycontainers/logstash/config
mkdir /home/mycontainers/logstash/pipeline
以上来个文件用于挂载logstash文件 然后在config文件夹里面新建以下文件
touch logstash.yml
vi logstash.yml
logstash.yml输入以下内容
xpack.monitoring.elasticsearch.hosts: [ "http://elasticsearch:9200" ]
然后切换到pipeline文件夹
新建配置文件
touch my.conf
vi my.conf
input {
tcp {
port => 5044
}
}
filter {
date {
match => ["content","yyyy-MM-dd_HH-mm-ss"]
target => "content"
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "mylog"
}
}
把jvm.options文件放到config文件夹里面
## JVM configuration
# Xms represents the initial size of total heap space
# Xmx represents the maximum size of total heap space
-Xms1g
-Xmx1g
################################################################
## Expert settings
################################################################
##
## All settings below this section are considered
## expert settings. Don't tamper with them unless
## you understand what you are doing
##
################################################################
## GC configuration
#8-13:-XX:+UseConcMarkSweepGC
8-13:-XX:CMSInitiatingOccupancyFraction=75
8-13:-XX:+UseCMSInitiatingOccupancyOnly
## Locale
# Set the locale language
#-Duser.language=en
# Set the locale country
#-Duser.country=US
# Set the locale variant, if any
#-Duser.variant=
## basic
# set the I/O temp directory
#-Djava.io.tmpdir=$HOME
# set to headless, just in case
-Djava.awt.headless=true
# ensure UTF-8 encoding by default (e.g. filenames)
-Dfile.encoding=UTF-8
# use our provided JNA always versus the system one
#-Djna.nosys=true
# Turn on JRuby invokedynamic
-Djruby.compile.invokedynamic=true
# Force Compilation
-Djruby.jit.threshold=0
# Make sure joni regexp interruptability is enabled
-Djruby.regexp.interruptible=true
## heap dumps
# generate a heap dump when an allocation from the Java heap fails
# heap dumps are created in the working directory of the JVM
-XX:+HeapDumpOnOutOfMemoryError
# specify an alternative path for heap dumps
# ensure the directory exists and has sufficient space
#-XX:HeapDumpPath=${LOGSTASH_HOME}/heapdump.hprof
## GC logging
#-XX:+PrintGCDetails
#-XX:+PrintGCTimeStamps
#-XX:+PrintGCDateStamps
#-XX:+PrintClassHistogram
#-XX:+PrintTenuringDistribution
#-XX:+PrintGCApplicationStoppedTime
# log GC status to a file with time stamps
# ensure the directory exists
#-Xloggc:${LS_GC_LOG_FILE}
# Entropy source for randomness
-Djava.security.egd=file:/dev/urandom
# Copy the logging context from parent threads to children
-Dlog4j2.isThreadContextMapInheritable=true
创建pipeline.yml文件并输入
# This file is where you define your pipelines. You can define multiple.
# For more information on multiple pipelines, see the documentation:
# https://www.elastic.co/guide/en/logstash/current/multiple-pipelines.html
- pipeline.id: main
path.config: "/usr/share/logstash/pipeline"
mylogstash.yml
version: '3'
services:
logstash:
container_name: logstash
network_mode: mynetwork
image: docker.elastic.co/logstash/logstash:7.12.0 # 替换为所需的 Logstash 版本
volumes:
- /home/mycontainers/logstash/config:/usr/share/logstash/config # 挂载 Logstash 配置文件
- /home/mycontainers/logstash/pipeline:/usr/share/logstash/pipeline # 挂载 Logstash Pipeline 配置文件
ports:
- 5044:5044 # 映射 Logstash 输入端口
然后使用docker-compose命令启动即可
如果启动过程中遇到问题,可以尝试不使用docker-compose启动,使用docker命令启动,然后把容器里面的config文件夹和pipeline文件夹copy到宿主机中,然后再使用docker-compose进行挂载启动。
如果遇到jvm问题,打开jvm.options文件把以下配置注释掉
# 8-13:-XX:+UseConcMarkSweepGC
kibana
使用 Kibana 针对大规模数据快速运行数据分析,以实现可观测性、安全和搜索。对来自任何来源的任何数据进行全面透彻的分析,从威胁情报到搜索分析,从日志到应用程序监测,不一而足。
Docker安装kibana
docker-compose.yml
version: '3'
services:
kibana:
image: kibana:7.14.2
network_mode: mynetwork
container_name: kibana
restart: always
volumes:
- ./kibana.yml:/usr/share/kibana/config/kibana.yml
ports:
- 5601:5601
privileged: true #环境变量
kibana.yml
server.name: kibana
# kibana的主机地址 0.0.0.0可表示监听所有IP
server.host: "0.0.0.0"
#
# 这边设置自己es的地址,
elasticsearch.hosts: http://elasticsearch:9200
elasticsearch.username: 'kibana'
elasticsearch.password: '123456'
# # 显示登陆页面
xpack.monitoring.ui.container.elasticsearch.enabled: true
# 开启中文模式
i18n.locale: "zh-CN"
执行docker-compose命令启动,控制台不报错即可。访问ip:5601,看到kibana首页即证明成功启动
Spring Boot
日志配置
logging:
file:
path: ./logs/mylog-${server.port}.logs
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引入SpringBoot的默认配置文件defaults.xml -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>-->
<!-- 引入SpringBoot中内置的控制台输出配置文件console-appender.xml -->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!-- 引入自定义的文件输出配置文件logback-spring-file-level.xml -->
<include resource="logback-spring-file-level.xml"/>
<!-- 设置root logger的级别为INFO,并将控制台输出和文件输出中的appender都添加到root logger下 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
<appender-ref ref="LOGSTASH"/>
</root>
<!-- jmx可以动态管理logback配置-->
<jmxConfigurator/>
</configuration>
如果想保留控制台输出日志,请保留<appender-ref ref="CONSOLE"/>
,如果不想保留就注释掉
logback-spring-file-level.xml
<?xml version="1.0" encoding="UTF-8"?>
<included>
<!--只保留 WARN Level的日志-->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_FILE}.WARNLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大10MB, 保存3天的历史日志, 所有日志文件最大50MB -->
<maxFileSize>10MB</maxFileSize>
<maxHistory>3</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!-- <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %X{request_id} - %msg%n</pattern>-->
<pattern>%d{"yyyy-MM-dd HH:mm:ss.SSS"} %-5level [%logger - %M] - %msg%n</pattern>
</encoder>
</appender>
<!--保留ERROR Level的日志-->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_FILE}.ERRORLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大10MB, 保存3天的历史日志, 所有日志文件最大50MB -->
<maxHistory>3</maxHistory>
<maxFileSize>10MB</maxFileSize>
<totalSizeCap>50MB</totalSizeCap>
<!-- <cleanHistoryOnStart>true</cleanHistoryOnStart>-->
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} %X{request_id} - %msg%n</pattern>
</encoder>
</appender>
<!-- 这向logstash发送并且收集信息 -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>你的ip:5044</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
</included>
测试接口
@GetMapping("/checkLog")
public String checkToken111() {
log.error("出错了");
return userService.sayHello();
}
测试
启动项目后就能在kibana看到mylog索引了,创建索引模式后,可以在可视化界面看到相关日志信息,如下图
里面都是项目启动的info信息
然后我们测试一下接口,看一下error是否顺利收集
ApiFox发送请求
kibana可视化
可以看到,error日志也是可以收集到了
kibana开发工具
进入kibana开发工具,使用控制台进行数据查询
GET mylog/_search
{
"fields": [
"message"
],
"query": {
"match": {
"message": "出错了"
}
}
}
具体查询语法请看 搜索接口 |弹性搜索指南 [7.14] |弹性的 (elastic.co)
总结
难点
难点总体来说就是配置日志文件比较麻烦,按照以上配置进行配置,改一下对应端口应该不会有什么问题。
还有一点就是logstash,初次搞logstash的话可能会比较麻烦,毕竟是第一次,笔者也是踩了很多坑。遇到不懂的问题可以去问一下GPT或者根据控制台反应的信息进行排查和搜索资料。
如有什么疑点和建议,评论区留言,我也会这两天深入学习一下ELK,进行一个拓展或者补充。
更新
当我们没有做任何格式化的时候,日志数据格式如下
{
"@timestamp":"2023-06-14T10:57:35.968+08:00",
"@version":"1",
"message":"|测试格式化|com.example.authcenter.controller.AuthController|checkToken111|1686711455968",
"logger_name":"com.example.authcenter.controller.AuthController",
"thread_name":"http-nio-8101-exec-4",
"level":"ERROR",
"level_value":40000,
"traceId":"64892c9fa0fac0b1bb28774647874f4d",
"spanId":"f214206f5585d771",
"spanExportable":"true","X-Span-Export":"true",
"X-B3-SpanId":"f214206f5585d771",
"X-B3-ParentSpanId":"bb28774647874f4d",
"X-B3-TraceId":"64892c9fa0fac0b1bb28774647874f4d",
"parentId":"bb28774647874f4d"}
这是经过笔者格式化的json串,而在kibana是这样的
看着这密密麻麻的数据,真让人头痛。
那有没有哪一种方式能让我们的数据变得突出重点呢?
有!使用logstash过滤器
我们将上述的logstash配置文件修改一下
input {
tcp {
port => 5044
}
}
filter {
grok {
match => { "message" => "\|%{GREEDYDATA:title}\|%{GREEDYDATA:className}\|%{GREEDYDATA:methodName}\|%{GREEDYDATA:content}" }
}
date {
match => ["content","yyyy-MM-dd_HH-mm-ss"]
target => "content"
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "mylog"
}
}
然后在代码里面把日志输出也修改一下
@GetMapping("/checkLog")
public String checkToken111() {
log.error("出错了");
log.error("|测试格式化|"+this.getClass().getName()+"|checkToken111|" + System.currentTimeMillis());
return userService.sayHello();
}
grok里面是对日志输出的字段进行匹配,写入es数据库里面,并且会新建title,className,methodName和content字段。
若日志输出格式不匹配,则放弃匹配字段。 以下是GPT对这段配置的解析
然后启动项目,访问接口再去kibana查看我们的接口数据
可以看到,我们成功匹配到字段后,库里面的相关字段不为空,匹配不到字段则字段为空。这样我们只需要关注匹配到的几个字段即可,无需对一整串json串进行解析抽取。
所以在日志输出的时候注意一下日志的输出格式会对我们的排查有很大的帮助。