Nginx日志解析

Nginx原始日志

通过阿里云极简模式采集了Nginx默认日志。默认的nginx 日志format如下

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

通过使用极简模式采集Nginx日志,样例如下:

使用正则抽取基础字段

# 用于将源字段里的内容,通过正则捕获组抽取出对应的字段
e_regex("源字段", "正则或有命名捕获正则", "目标字段名或数组(可选)")
# 通用字段抽取
e_regex("content",'(?<remote_addr>[0-9:\.]*) - (?<remote_user>[a-zA-Z0-9\-_]*) \[(?<local_time>[a-zA-Z0-9\/ :\-\+]*)\] "(?<request>[^"]*)" (?<status>[0-9]*) (?<body_bytes_sent>[0-9\-]*) "(?<refer>[^"]*)" "(?<http_user_agent>[^"]*)"')

通过正则抽取以后,可以看到日志的字段增加了refer、remote_addr、remote_user、request等字段。

处理时间字段

当前提取到的localtime不易读,我们把它解析成易读的格式,会用到的以下数据加工函数:

# 用于设置字段值
e_set("字段名", "固定值或表达式函数")

# 将时间字符串解析为日期时间对象
dt_strptime('值如v("字段名")', "格式化字符串")

# 将日期时间对象按照指定格式转换为字符串
dt_strftime(日期时间表达式, "格式化字符串")

实现思路,先通过 dt_strptime 将local_time的时间转化为日期时间对象,然后再通过dt_strftime将日期时间对象转化为标准的日期时间字符串。 针对Nginx local_time的转化,使用如下数据加工语句: e_set("local_time", dt_strftime(dt_strptime(v("local_time"),"%d/%b/%Y:%H:%M:%S %z"),"%Y-%m-%d %H:%M:%S"))

e_set("local_time", dt_strftime(dt_strptime(v("local_time"),"%d/%b/%Y:%H:%M:%S %z"),"%Y-%m-%d %H:%M:%S"))

效果如下:

解析request uri

可以看到request字段由 METHOD URI VERSION组成,我们希望对 requst字段进行抽取,获取到请求的METHOD、URI以及VERSION,并且将请求URI中的请求的参数变成字段,方便后续进行查询。可以用以下函数来做实现

# 使用正则将request uri抽取
e_regex("源字段名", "正则或有命名捕获正则", "目标字段名或数组(可选)", mode="fill-auto")

# 进行urldecode
url_decoding('值如v("字段名")’)

# 设置字段值
e_set("字段名", "固定值或表达式函数", ..., mode="overwrite")

# 将request_uri中的key=value的组合抽取成字段 值的模式
e_kv("源字段正则或列表", sep="=", prefix="")

实现语句

e_regex("request", "(?<request_method>[^\s]*) (?<request_uri>[^\s]*) (?<http_version>[^\s]*)")
e_set("request_uri", url_decoding(v("request_uri")))
e_kv("request_uri", prefix="uri_")

实现效果

http code状态码映射

每一个http状态码都代表了不同的含义,下面是一份http状态码的映射表, 我们可以通过e_table_map 函数来将状态码的信息扩展到我们的日志字段中,方便后续做统计分析。

涉及到的数据加工函数如下:

# 用来做字段富化,类似sql里join的功能
e_table_map("表格如通过tab_parse_csv(...)解析得到的",
            "源字段列表或映射列表如[('f1', 'f1_new'), ('f2', 'f2_new')]", 
            "目标字段列表")

# 用来把csv文件解析成table对象
tab_parse_csv(CSV文本数据, sep=',', quote='"')

# code的映射关系维度表是一个csv文件,存在oss上,使用res_oss_file
res_oss_file(endpoint="OSS的endpoint", ak_id="OSS的AK_ID", 
             ak_key="OSS的AK_KEY", bucket="OSS的bucket", file="在OSS中存的文件地址", 
             change_detect_interval="定时更新时间,默认为0")

实际使用到的DSL语句如下

# http状态码映射
e_table_map(
      tab_parse_csv(
           res_oss_file(endpoint="oss-cn-shanghai.aliyuncs.com",
              ak_id='',ak_key='',
              bucket="your_oss_bucket", 
              file="http_code.csv", format='text')),
              [("status","code")],
              [("alias","http_code_alias"),
               ("description","http_code_desc"),
               ("category","http_code_category")])

映射后的效果

通过UserAgent判断客户端操作系统

我们想了解客户用的是什么os版本,可以通过user agent里的字段用正则匹配来判断,用到dsl语句

# 取某个字段的值
v("字段名")

# 获取ua相关信息
ua_parse_all("带useragent信息的内容")

# 展开json字段, 因为ua_parse_all得到的是一个json对象,为了展开到一级字段使用e_json做展开
# 模式有 simple、full、parent、root 参考https://help.aliyun.com/document_detail/125488.html#section-o7x-7rl-2qh
e_json("源字段名", fmt="模式", sep="字段分隔符")

# 丢弃临时产生的字段
e_drop_fields("字段1", "字段2")

实际用到的dsl语句

# 通过User Agent解析获得客户端信息
e_set("ua",ua_parse_all(v("http_user_agent")))
e_json("ua", fmt='full',sep='_')
e_drop_fields("ua",regex=False)

加工效果

非200的日志投递到指定logstore

可以使用e_output函数来做日志投递,用regex_match做字段匹配

# 条件判断if
e_if("条件1如e_match(...)", "操作1如e_regex(...)", "条件2", "操作2", ....)

# 判断是否相等
op_ne(v("字段名1"), v("字段名2"))

# output发送到目标名称,目标名称在数据加工保存任务的时候配置对应的logstore信息
e_output(name="指定的目标名称")

实现语句

# 分发非200的日志
e_if(op_ne(v("http_code_alias"),"2xx"), e_output(name="img/nginx-log-bad"))

在预览里看到这个效果。(保存加工的时候,需要设置好对应project、logstore的ak信息

完整的DSL代码以及上线流程

好了,通过一步一步的开发调试,现得到完整的DSL代码如下

# 通用字段抽取
e_regex("content",'(?<remote_addr>[0-9:\.]*) - (?<remote_user>[a-zA-Z0-9\-_]*) \[(?<local_time>[a-zA-Z0-9\/ :\-]*)\] "(?<request>[^"]*)" (?<status>[0-9]*) (?<body_bytes_sent>[0-9\-]*) "(?<refer>[^"]*)" "(?<http_user_agent>[^"]*)"')

# 设置localttime
e_set("local_time", dt_strftime(dt_strptime(v("local_time"),"%d/%b/%Y:%H:%M:%S"),"%Y-%m-%d %H:%M:%S"))

# uri字段抽取
e_regex("request", "(?<request_method>[^\s]*) (?<request_uri>[^\s]*) (?<http_version>[^\s]*)")
e_set("request_uri", url_decoding(v("request_uri")))
e_kv("request_uri", prefix="uri_")

# http状态码映射
e_table_map(
      tab_parse_csv(
           res_oss_file(endpoint="oss-cn-shanghai.aliyuncs.com",
              ak_id='',ak_key='',
              bucket="ali-sls-etl-test", 
              file="http_code.csv", format='text')),
              [("status","code")],
              [("alias","http_code_alias"),
               ("description","http_code_desc"),
               ("category","http_code_category")])

# 通过User Agent解析获得客户端信息
e_set("ua",ua_parse_all(v("http_user_agent")))
e_json("ua", fmt='full',sep='_')
e_drop_fields("ua",regex=False)

# 分发非200的日志
e_if(op_ne(v("http_code_alias"),"2xx"), e_coutput(name="img/nginx-log-bad"))

在页面提交代码以后,保存数据加工 配置目标logstore信息,默认走完加工逻辑的日志都会发送到第一个目标logstore,我们在代码里指定了e_output到指定logstore,因此还需要第二个目标,并且目标的名字和e_output里指定的目标名称一致。

保存完即完成上线,可以在数据处理-加工下看到该任务,点击进去可以看到加工延迟等信息。