记录Logstash从入门到上线
背景
现有一需求,需要做埋点数据,记录每一次请求时间、耗时等数据。根据设计要以记日志的形式使用ELK记录并分析埋点数据。由于之前并没有接触过ELK,因此本次将学习到上线的思路记录下来。
V0
思路
ELK是啥,Logstash是啥,完全不清楚。稍微百度了一下可以简单理解为ES作为仓库,Logstash作为中转和过滤数据的服务,Kibina作为数据分析工具。
目前项目里使用Logback+slf4j作为日志工具。记录日志的方式为
log.info("xxxxx");
xxxx为日志自定义内容,输出到控制台或者日志文件中的不仅只有xxxx,还有线程号,时间等详细信息,由logback配置而成。
详细信息+自定义内容=全部内容
首先需要对Logstash有一个全面了解,以下两篇好文可能会帮助到你
https://blog.csdn.net/hushukang/article/details/84423184
https://www.cnblogs.com/FengGeBlog/p/10305318.html
经过以上两篇文章的学习,可以知道Logstash有输入模块(input)、过滤模块(filter)、输出模块(output)
对于我而言,我的目的很明确:将日志从springboot经过logstash发送到es。
因此
-
输入模块
logback发送数据到logstash
-
过滤模块
logstash:把我的日志的全部内容(string)过滤成结构化数据(json)
-
输出模块
指向es地址
由于过滤模块一时半会玩不明白,因此简单点,先把数据试下如何从Logback发送到Logstash再存入ES。
配置Logstash
input{
tcp {
mode => "server"
#发布服务的端口
port => 4560
codec => json_lines
}
}
output{
stdout {
codec=>rubydebug
}
#es的地址和索引(数据库)
elasticsearch{
hosts => ["esip:port"]
index => "track"
}
}
配置Logback
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<!--logstash地址,ip:port -->
<destination>${logstash.server-addr}</destination>
<!-- encoder必须配置,有多种可选 -->
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder">
<!--在生成的json中会加这个字段-->
<!--<customFields>{"appname":"${spring.application.name}"}</customFields>-->
</encoder>
<!--logback根据这里的条件过滤,发送匹配的日志到logstash-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${log.level}</level>
</filter>
</appender>
参考链接:https://blog.csdn.net/DPnice/article/details/94650402
我的配置:
<springProperty scope="context" name="LOGSTASH_ADDR" source="logging.logstash.address" defaultValue=""/>
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOGSTASH_ADDR}</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder">
</encoder>
</appender>
<root level="info">
...
<appender-ref ref="LOGSTASH"/>
</root>
小结
以上代码将V0完成,并没有实现我的需求,但是ELk如何运作可以基本上可以理清楚。
原以为logback仅会将全部日志信息(包含时间、线程号、日志信息的string)全部发送到logback,正愁如何将信息拆分出来。最后惊喜地发现原来logback已经将不同的信息作为不同的字段,以json的形式发送到logstash,这样一来我们就可以只解析自定义的日志内容,不用处理其他数据。
{
"opTime" => 1628849263688,
"host" => "1.1.1.1",
"userId" => "214edd60-3b31-4835-96b8-a37c3d344dd5",
"appName" => "elkDemoApp",
"port" => "3636",
"costTime" => 1381,
"@timestamp" => 2021-08-13T10:07:45.069Z,
"@version" => "1",
"opType" => "008_01_00",
"opStatus" => 1,
"caseNo" => "zt1111"
}
V1
思路
现在我们已经可以在logstash中获取我们自定义的内容:一个名为message的string。即log.info("xxxxx");中的xxxxx
现在需要在logstash中将这个string转成多个普通字段,这样才能作为字段存入es。
之前只是略读上面两篇文章,这次需要详细学习Logstash的过滤模块(filter).
…学习中…
通过一下午的学习,大概了解到filter可以做的事
| 插件名称 | 说明 |
|---|---|
| date | 日期解析 |
| grok | 正则匹配解析 |
| dissect | 分隔符解析 |
| mutate | 对字段作处理,比如重命名、删除、替换 |
| json | 按照json解析字段内容到指定字段中 |
| geoip | 增加地理位置数据 |
| ruby | 利用ruby代码来动态修改Logstash Event |
初步想法:日志中按照顺序记录字段,如log.info("张三,16,10000,武汉"),然后在logstash中用分隔符分成多个字段
filter{
#截取header,只取日志中埋点的日志
mutate{
split => ["message",","]
add_field => ["name","%{[message][0]}"]
add_field => ["age","%{[message][1]}"]
...
}
}
这样的话很明显的两个问题,1是字段在logstash配置文件中写死,不好修改。2是顺序固定,如果字段为空不好调换位置。
V2
因此继续学习,看有没有其他方案,于是发现下面一段代码
从日志message中获取字段和值
https://www.cnblogs.com/yulinlewis/p/10854424.html
filter {
#从日志中解析字段
ruby {
code => "
array1 = event.get('msgtemp').split(';')
array1.each do |temp1|
if temp1.nil? then
next
end
array2 = temp1.split('=')
key = array2[0]
value = array2[1]
if key.nil? then
next
end
event.set(key, value)
end
"
}
}
这段配置可以以kv的方式,记录字段,很好的避免了上面V1中的两个问题。
logger.info("name={};age={};salary={};add={}","张三",16,10000,"武汉");
Field[] declaredFields = TrackInfo.class.getDeclaredFields();
for (Field declaredField : declaredFields) {
//带有Transient注解的不写入日志
if(declaredField.getAnnotation(javax.persistence.Transient.class)!=null){
continue;
}
declaredField.setAccessible(true);
Object value = declaredField.get(info);
if(value != null && !"".equals(value)){
logBuilder.append(declaredField.getName()).append("=").append(value.toString());
logBuilder.append(";");
}
}
V3
以上代码已经基本实现了我的需求,不过还是不太完美,需要再做一点点优化
问题
1、字段发送到logstash后,都变成了字符串,不能以数字形势发送到es,对统计可能有影响
2、所有的日志都会通过Logback发送到Logstash,再发送到es,比较浪费资源
解决方式:
1、埋点相关日志以track:开头,Logback中添加配置,只发送包含track字符串的日志到Logstash。
2、Logstash中添加配置,只发送track:开头的日志到ES。
3、Logstash的filter中添加配置,将指定类型的字段转换成数字类型,便于统计
Logback:
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOGSTASH_ADDR}</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder">
</encoder>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<expression>message.contains("track")</expression>
</evaluator>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
后端:
数字类型的字段如age,在记录日志时再跟上后缀@int,代表需要在Logstash中转换成数字类型
log.info("name=张三;age=16@int")
StringBuilder logBuilder = new StringBuilder("track:");
Field[] declaredFields = TrackInfo.class.getDeclaredFields();
for (Field declaredField : declaredFields) {
//带有Transient注解的不写入日志
if(declaredField.getAnnotation(javax.persistence.Transient.class)!=null){
continue;
}
declaredField.setAccessible(true);
Object value = declaredField.get(info);
if(value != null && !"".equals(value)){
logBuilder.append(declaredField.getName()).append("=").append(value.toString());
if(value instanceof Number){
logBuilder.append("@int");
}
logBuilder.append(";");
}
}
logBuilder.deleteCharAt(logBuilder.lastIndexOf(";"));
//log.info("track:name={};age={}@int;addr={}","张三",16,"武汉");
log.info(logBuilder.toString());
Logstash:
filter{
#截取header,只取日志中埋点的日志
mutate{
split => ["message",":"]
add_field => ["header","%{[message][0]}"]
add_field => ["msgtemp","%{[message][1]}"]
}
if [header] != "track" {
drop {}
}
#从日志中解析字段 key=value;key=value@type 如果不带type,则直接设置value;如果带type则类型转换(暂时支持整型)。
ruby {
code => "
array1 = event.get('msgtemp').split(';')
array1.each do |temp1|
if temp1.nil? then
next
end
array2 = temp1.split('=')
key = array2[0]
valueandtype = array2[1]
if key.nil? then
next
end
array3 = valueandtype.split('@')
value=array3[0]
type=array3[1]
if type==='int' then
event.set(key, Integer(value))
else
event.set(key, value)
end
end
"
}
#删除多余字段
mutate{
remove_field => ["header","appname","logger_name","level","msgtemp","message","ROLLINGFILE_MAXHISTORY","ROLLINGFILE_TOTALSIZECAP","ROLLINGFILE_MAXFILESIZE","level_value","thread_name","LOGSTASH_ADDR"]
}
}
最终实现了一个可复用性较高的方法。虽然说方法有点笨,就是截取字符串实现的。没有用到Logstash各种其他高大上的功能,但还是在短时间内满足本次需求。后续可能还会有其他bug或者需要优化的地方,继续完善。
默认pattern
https://github.com/logstash-plugins/logstash-patterns-core/blob/master/patterns/ecs-v1/java
logstash中过滤日志
https://www.oschina.net/question/2895820_2233507
logback中过滤日志
https://www.cnblogs.com/yongze103/archive/2012/05/05/2484753.html
本文记录了使用ELK Stack(Elasticsearch、Logstash、Kibana)处理日志的过程,从Logback日志到Logstash过滤再到Elasticsearch存储。介绍了Logstash的输入、过滤、输出模块,通过Logstash的filter模块将非结构化的日志转换为结构化数据,包括日期解析、正则匹配、字段转换等操作,以满足日志分析需求。
293

被折叠的 条评论
为什么被折叠?



