实战笔记

app.yml

解析的 yml 配置文件如下:

inputs:
  - Stdin:
      codec: plain
      hostname: true # if add hostname to event; default false
      type: stdin1

filters:
  - Filters:
      id: 'f1'
      if:
        - '<#if message??>true</#if>'  # 要求必须有信息
        - '<#if message?contains("mylogging=>")>true<#elseif message?contains("mylogging")>true</#if>' #要求必须包含 mylogging 关键词


      filters:
        - Add:
            fields:
              testField1: "xxxxxxxxxxxxxxxxxx" # 添加一个测试字段

  - Grok:
      match:
        - '^%{TIMESTAMP_ISO8601:time} \[%{DATA:kvsource}\] \[%{WORD:logLevel}\] \[%{DATA:className}\] -- %{GREEDYDATA:content}$'
      remove_fields: ['message'] # 移除 message 
      tag_on_failure: 'grokFail' # do not add tags; deafult "grokfail"

outputs:
  - Stdout: { }

对应的日志测试

我们输入一条日志:

2023-11-17 10:45:09.167 [app=test&tid=1313678624044a4a&sid=881dec04b6719878&sidParent=d1bc9674dabd9a84&entryPoint=com.ryo.Main#main] [INFO] [com.ryo.Main] -- mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{}

输出:

{hostname=d, @timestamp=2023-11-17T11:29:26.693+08:00, logLevel=INFO, className=com.ryo.Main, time=2023-11-17 10:45:09.167, kvsource=app=test&tid=1313678624044a4a&sid=881dec04b6719878&sidParent=d1bc9674dabd9a84&entryPoint=com.ryo.Main#main, type=stdin1, content=mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{}}

多出的哪些属性,主要是我们在 grok 中添加的。

grok 基本处理

match 中的核心一句:

- '^%{TIMESTAMP_ISO8601:time} \[%{DATA:kvsource}\] \[%{WORD:logLevel}\] \[%{DATA:className}\] -- %{GREEDYDATA:content}$'

这个语句是我们要求实现的:

2023-11-17 10:45:09.167 [app=test&tid=1&sid=1&sidParent=1&entryPoint=com.ryo.Main#main] [INFO] [com.ryo.Main] -- mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{}

如何通过logstash 的 grok 配置,要求如下:

1. 2023-11-17 10:45:09.167 解析为时间 time
2. [app=test&tid=1313678624044a4a&sid=881dec04b6719878&sidParent=d1bc9674dabd9a84&entryPoint=com.ryo.Main#main] 解析为 kvsource
3. [INFO] 解析为对应的日志级别 logLevel
4. [com.ryo.Main] 解析为 className
5. -- mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{} 解析为 content

最后的结果换行展示如下:

hostname=d 
@timestamp=2023-11-17T11:29:26.693+08:00 
logLevel=INFO 
className=com.ryo.Main 
time=2023-11-17 10:45:09.167 
kvsource=app=test&tid=1313678624044a4a&sid=881dec04b6719878&sidParent=d1bc9674dabd9a84&entryPoint=com.ryo.Main#main 
type=stdin1 
content=mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{}}

Q: 解释一下 match 表达式

- '^%{TIMESTAMP_ISO8601:time} \[%{DATA:kvsource}\] \[%{WORD:logLevel}\] \[%{DATA:className}\] -- %{GREEDYDATA:content}$'

这个表达式具体什么意思?

这个 grok 表达式可以匹配以下格式的日志记录:

2023-11-17T13:42:24.109+08:00 [app=test, sidParent=35af473fb2fce1f2, entryPoint=com.ryo.Main#main, tid=4a523d52f312a3ed, sid=5d9d40f09f091b7a] [INFO] [com.ryo.Main] -- mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{"request":"request","response":"result"}

这个表达式首先使用 TIMESTAMP_ISO8601 正则表达式匹配时间字段,然后使用 DATA 正则表达式匹配 kvsource 字段,最后使用 GREEDYDATA 正则表达式匹配 content 字段。

1) TIMESTAMP_ISO8601 正则表达式可以匹配以下格式的日期时间字符串:

YYYY-MM-DDTHH:MM:SS.SSSZ

2) DATA 正则表达式可以匹配以下格式的字符串: [^\s]+

3)GREEDYDATA 正则表达式可以匹配以下格式的字符串:.*

因此,这个 grok 表达式可以匹配所有格式为 YYYY-MM-DDTHH:MM:SS.SSSZ [DATA] [WORD] [DATA] -- GREEDYDATA 的日志记录。

4) WORD 匹配一个单词,也就是一个由字母和数字组成的字符串。

在 logstash 中, WORD 通常用于匹配日志记录中的关键字。

例如,在上面提到的 grok 表达式中, WORD 匹配了 INFO 这个关键字。

Q1: GREEDYDATA 和 DATA 什么区别?greeydata 是匹配后面的所有的意思吗?

GREEDYDATA 匹配后面的所有内容,而 DATA 匹配一个单词。

例如,在上面提到的 grok 表达式中, GREEDYDATA 匹配了 mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{"request":"request","response":"result"} 这整个字符串,而 DATA 匹配了 mylogging 这个单词。

Q2: [%{DATA:kvsource}] 两边为什么需要 \ 符号?

\ 符号是转义符,它可以告诉 logstash 解析器,后面的字符是字符串而不是正则表达式。

例如,在上面提到的 grok 表达式中,[%{DATA:kvsource}] 表示匹配一个字符串,这个字符串的值是 kvsource 字段的值。

如果没有使用 \ 符号,logstash 解析器会将 [%{DATA:kvsource}\] 解释为一个正则表达式,这会导致日志记录无法正确解析。

Q3: 中间的空格是任意数量吗?

^%{TIMESTAMP_ISO8601:time} \[%{DATA:kvsource}\]$

这种写法 time 和 kvsource 之间的空格有数量限制吗?还是可以任意空格?

在上述的 grok 表达式中,time 和 kvsource 之间的空格是可以有任意数量的。

这意味着可以匹配任意数量的空格字符,包括零个空格字符、一个空格字符、多个连续空格字符等。

因此,该表达式可以匹配各种不同数量的空格字符。

ps: 我们可以实际测试一下,把输入的日志改成随便加几个空格?

2023-11-17 10:45:09.167       [app=test&tid=1&sid=1&sidParent=1&entryPoint=com.ryo.Main#main]    [INFO]     [com.ryo.Main] --    mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{}

对应的结果:

{hostname=d, @timestamp=2023-11-17T13:59:09.558+08:00, message=2023-11-17 10:45:09.167       [app=test&tid=1&sid=1&sidParent=1&entryPoint=com.ryo.Main#main]    [INFO]     [com.ryo.Main] --    mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{}, type=stdin1, logReqJson={}, tags=[grokFail]}

发现并不行,说明上面的回答是错的。

Q4: 如何可以让每个之间的空格是任意数量?

1) 我们要知道任意空格的表达式怎么写?

要匹配任意位数的空格或制表符(tab),可以使用 \s+

这个正则表达式模式可以匹配一个或多个连续的空格或制表符。

例如,如果你想匹配任意数量的空格或制表符,可以使用 \s+ 这个模式。

\s* 和 \s+ 都是匹配空白字符(空格、制表符、换行符等)的正则表达式模式。

但是,\s* 会匹配任意数量的空白字符,而 \s+ 只会匹配一个或多个空白字符。

2) 改写一下对应的 grok match 语句:

改成下面的样子:

- '^%{TIMESTAMP_ISO8601:time}\s*\[%{DATA:kvsource}\]\s*\[%{WORD:logLevel}\]\s*\[%{DATA:className}\]\s*-- %{GREEDYDATA:content}$'

然后再次用上面加了随意空格的输入测试,已经可以正确解析。

{hostname=d, @timestamp=2023-11-17T14:06:19.002+08:00, logLevel=INFO, logReqJson={}, className=com.ryo.Main, time=2023-11-17 10:45:09.167, kv={app=test, sidParent=1, entryPoint=com.ryo.Main#main, tid=1, sid=1}, type=stdin1, content=   mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{}, requestJson=xxxxxxxxxxxxxxxxxx}

KV 内容的 split 拆分

kvsource=app=test&tid=1313678624044a4a&sid=881dec04b6719878&sidParent=d1bc9674dabd9a84&entryPoint=com.ryo.Main#main

这个是一个通过 & 连接的 kv 信息,我们如何通过 hangout 内置组件拆分呢?

配置

  - KV:
      # kvsource=app=test&tid=1313678624044a4a&sid=881dec04b6719878&sidParent=d1bc9674dabd9a84&entryPoint=com.ryo.Main#main
      source: kvsource # default message
      target: kv   # default null
      field_split: '&'  # default " "
      value_split: '='  # default "="
      trim: '\t\"'  #default null
      trimkey: '\"'  # default null
      tag_on_failure: "KVfail" # default "KVfail"
      remove_fields: [ 'kvsource' ]

测试效果

{hostname=d, testField1=xxxxxxxxxxxxxxxxxx, @timestamp=2023-11-17T11:49:33.885+08:00, logLevel=INFO, className=com.ryo.Main, time=2023-11-17 10:45:09.167, kv={app=test, sidParent=d1bc9674dabd9a84, entryPoint=com.ryo.Main#main, tid=1313678624044a4a, sid=881dec04b6719878}, type=stdin1, content=mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{}}

所有的信息都会被处理转换到 kv map 中。

split 拿到最后一个 json

针对

2023-11-17 13:41:50.620 [app=test&tid=4a523d52f312a3ed&sid=5d9d40f09f091b7a&sidParent=35af473fb2fce1f2&entryPoint=com.ryo.Main#main] [INFO] [com.ryo.Main] -- mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{"request":"request","response":"result"}

如何获取到最后的一个 json 信息?

配置

我们添加一个对应的 filters, 直接可以针对 split 然后处理:

  - Filters:
      id: 'f1'
      if:
        - '<#if message??>true</#if>'
        - '<#if message?contains("mylogging=>")>true<#elseif message?contains("mylogging")>true</#if>'
      filters:
        - Add:
            fields:
              logReqJson: '<#assign a=message?split("=>")>${a[2]}'

效果

hostname=d 
@timestamp=2023-11-17T13:42:24.109+08:00 
logLevel=INFO 
logReqJson={"request":"request","response":"result"} 
className=com.ryo.Main 
time=2023-11-17 13:41:50.620 
kv={app=test, sidParent=35af473fb2fce1f2, entryPoint=com.ryo.Main#main, tid=4a523d52f312a3ed, sid=5d9d40f09f091b7a} 
type=stdin1 
content=mylogging=>[SERVICE][cost=10 ms][com.ryo.Main#main-out]=>{"request":"request","response":"result"} 
requestJson=xxxxxxxxxxxxxxxxxx

参考资料

chat