mmnormalize 是 Rsyslog 内置的一种数据解析的方案,甚至有自己的官网:http://www.liblognorm.com可以阅读相关用法细节。它既不像 Rsyslog 的 rainerscript 那样采用 ERE 类型的简单正则,也不像 Logstash的 Grok 那样采用 PCRE 类型的复杂正则(一度通过添加 regex parser 引入过 PCRE,后来又删了),而是自己设计了一套方式,其最核心的匹配语法就是 %char-to: 这种“向后匹配直到*为止”。下面是一段解析 nginx 访问日志的 mmnormalize 配置,相信大家第一眼看上去都会晕:

rule=:"%client_ip:char-to:"%" %tcp_peer_ip:ipv4% - [%req_time:char-to:]%] "%verb:word% %url:word% %protocol:char-to:"%" %status:interpret:int:number% %latency:interpret:float:word% %bytes_sent:interpret:int:number% "%referrer:char-to:"%" "%user_agent:char-to:"%" %upstream_addrs:tokenized:, :tokenized: \x3a :regex:[^ ,]+% %upstream_response_times:tokenized:, :tokenized: \x3a :interpret:float:regex:[^ ,]+% %pipe:word% \t %host:word% cache_%cache:word%

不过上个月 liblognorm 做了一次重大版本更新,新的 v2 语法,添加了一个 user-defined types 的设计,这就有点类似 Grok 的预定义正则的意思啦。

所以,本文来详细说说,从 v1.1.0 开始,新增的一些 liblognorm 的 type 给我们处理 Rsyslog 数据带来的便利。

normalize 的匹配规则叫做 rulebase,所以可以看到有些 rsyslog 介绍中,mmnormallize 配置文件的后缀名是 *.rb,可不要以为是用 Ruby 解析啊。

version=2
type=@TIMESTAMP:%date:date-iso% %time:time-24hr%Z
type=@TIMESTAMP:%datetime:date-rfc5424%
type=@TIME:%resptime:float%
type=@TIME:-
rule=:%timestamp:@TIMESTAMP% %clientip:ipv4% %resptime:@TIME% %urlpath:string% %reqbody:json% %referer:quoted-string%

这行 rule 在 v2 中还可以写的更美观一些:

rule=:%[ {"type":"@TIMESTAMP", "name":"timestamp"},
         {"type":"literal", "text:" "},
         {"type":"ipv4", "name":"clientip"},
         {"type":"literal", "text:" "},
         {"type":"@TIME", "name":"resptime"},
         {"type":"literal", "text:" "},
         {"type":"string", "name":"urlpath"},
         {"type":"literal", "text:" "},
         {"type":"quoted-string", "name":"referer"},
         {"type":"literal", "text:" "},
         {"type":"json", "name":"reqbody"}
       ]%

Rsyslog 的 mmjsonparse 模块只能解析 CEE 格式,如果 msg 本身是纯 JSON 的,反而不能解析,这时候就可以用上 mmnormalize 的 json parser 了。

和 json 一样也是 v1.1 以后才加入的,还有 char-seprestchar-sepchar-to 的区别是前者是0到多个,后者是1到多个;rest 则用来收集当前位置到本行结尾的全部数据。

也就是说:%capturename:char-sep:\x20 等于 %capturename:char-sep: % 等于 %{"type":"char-sep","name":"capturename","extradata":" "}% 等于 %capturename:char-sep{"extradata":" "}。相当于 Grok 里的 [^ ]*?。其他类似。

如果觉得上面那种预定义太麻烦,毕竟响应时间无非就是数值或者横杆而已,那么这行还可以这么写:

         {"type":"alternative",
          "parser": [
            {"name":"resptime", "type":"float"},
            {"type":"literal", "text":"-"}
          ]
         }

还有类似 logstash-filter-kv 插件的功能,比如把

a:2,b:4, c:6, d:8

这段数据做切割处理的配置:

%{"name":"obj", "type":"repeat",
  "parser":[
    {"type":"string", "name":"key"},
    {"type":"literal", "text":":"},
    {"type":"number", "name":"val"}
  ],
  "while": {
    "type":"alternative", "parser": [
      {"type":"literal", "text":", "},
      {"type":"literal", "text":","}
    ]
  } 
}%

会解析得到下面这样的 JSON 结果:

{ "obj": [
    { "val": "2", "key": "a" },
    { "val": "4", "key": "b" },
    { "val": "6", "key": "c" },
    { "val": "8", "key": "d" }
  ]
}

看起来似乎不是很 kv 的样子,不过对于写入 Elasticsearch 来说,却刚刚好符合 nested object 的设计!

不过目前,还有些匹配模式在 v2 中不支持的,还得继续使用 v1 模式:

rule=:%filesize:interpert:float:number%

有时候,明明你这个数据中是整形,但是因为 ES 的 mapping 问题或者其他原因,需要强制转换成浮点型。Rsyslog 本身的 rainerscript 只提供了 cnum() 函数,没有 cfloat(),那么我们只能在 mmnormalize 里做 interpert 转换了。而这个操作目前在 v2 版本中还不支持。

目前 liblognorm 所支持的所有匹配格式说明,见https://github.com/rsyslog/liblognorm/blob/master/doc/configuration.rst