在最近项目开发的过程中,用到了ELK收集nginx日志,对关键的业务接口进行统计。在开发过程中,遇到了一个非常棘手的问题,那就是有的接口支持get方式,有的接口支持post方式,但是最终都要把请求报文封装成json对象存放到elastic中。而nginx日志中,post请求的参数相对较为简单,通过nginx log_format中的request_body可以直接获取请求体,请求体本身就是json字符串。而get请求,参数是在nginx log_format中request参数所代表的url地址之后,通过?和&拼接,并且由于接口协议较为复杂,所以嵌套对象和集合非常多,较单层对象的解析要复杂得多。 找个客户端拼接的参数还不算全的get请求作为例子:
比如channel对象下,有id和type,但是在get请求中是以channel.id=idVal&channel.type=typeVal这种形式传递的。
技术难点有两个:一个是通过key值,将原本的复杂嵌套对象还原出来。另一个是如何判断每一层是对象属性,还是对象集合属性。
组件难点有一个:ELK中,logstash负责对数据的清洗,而清洗过程使用的是logstash内置的各种filter插件,复杂的处理逻辑通常使用ruby脚本语言进行处理,对于ruby不是很熟,如何进行数据的拼装及json的转换,这些都是需要预先对ruby进行学习的。
1.将key值进行split操作,分层去处理,比如 channel.type 劈成 [“channel”,“type”],代表参数对象中有一级属性channel,channel下有二级属性type。
2.集合属性存在下标,比如user.userIds[0].idType,代表一级属性user下有userIds属性,userIds是数组,而数组元素中有idType属性
3.在学习ruby的过程中,意外读到一篇 Ruby中嵌套对象转换成json的方法 ,给了我很大的启发,ruby中可以使用Hash对象(类似于JAVA中的map),相比于在ruby中定义class再to_json,优点是可以不需要提前知道参数类的模型,可以兼容任何的参数形式,这样我们就可以写一次脚本,兼容项目内的所有接口。并且,在输出到elastic时,ruby中Hash对象可以直接存入,不需要进行to_json操作,而class类,必须要to_json成字符串,再由logstash的json插件进行转换,不能直接存入class对象,因为logstash缺少ruby自定义class的converter转换器(亲自试过,报错)
1.首先把整个请求字符串以’&‘劈开,劈开后每个元素代表的是key=value键值对
2.将key中的 ‘[’ 和 ‘]’ 替换成 ‘.’ ,这样方便接下来对于key的分层处理,但是如果像(图1)中的’placementCodes[0]’ 这种key,最后会变成’placementCodes.0.’,key如果以’]‘结尾,最终会多出一个’.’ ,需要截取为 ‘placementCodes.0’ 。 这个就不多说了,字符串基本操作。
3.进行前两部操作之后,再将key值以’.‘劈开,举个比较全面的例子来说 user.userIds[0].idType 这个key,最终变成了 [‘user’,‘userIds’,‘0’,idType’]这样的数组。我们需要把这样的一个代表key的数组进行递归操作,在元对象下,一层一层的加工,先贴一下核心代码: (注释已经解释的较为清楚了,不太懂的语法稍微百度一下就OK)
#尝试讲get请求拼装成ruby对象,再存入es
require 'json'
#1.声明一个新的hash对象,存放转换后的参数对象
result = Hash.new
#2.定义工具方法,递归调用
#parent:上层对象 keyArray:尚未递归完的key值 val:就是参数的值,给最子级赋值
#type:上层对象的类型,枚举为'hash'or'array'
#如['user','userIds','0',idType']这个keyArray,遍历完user后,元对象变为result['user'],
#而keyArray会减去第一个元素,变为['userIds','0','idType'],见下面注释 *1
def makeUpHash(parent,keyArray,val,type)
#如keyArray长度为1,则代表是 arg1.agr2.agr3遍历到了最后一个元素 arg3
#当然也有可能是这个key本身就是很简单的一层,比如id=1,则keyArray = ['id']
if keyArray.length == 1
if type == 'hash'
#hash的操作是 hash['parentArg1']['childrenArg1'] = val,不同于java中的 obj.parentArg1.childrenArg1 = val
parent[keyArray[0]] = val
elsif type == 'array'
#因为上层类型是array,也就是parent是个数组,则keyArray[0]是个数字的字符串如'0',必须'0'.to_i强转,否则运行异常
parent[keyArray[0].to_i] = val
end
return
end
if keyArray[1] =~ /^\d+$/
array = Array.new
if type == 'hash'
#这里三目表达式,如果本层对象被创建,则不再创建。
#比如 device.id创建了 result['device']['id'],
#但是遍历到device.type时,result['device']这一层不需要再new了,把type属性给 device这层就行
array = parent[keyArray[0]] ? parent[keyArray[0]] : array
parent[keyArray[0]] = array
elsif type == 'array'
array = parent[keyArray[0].to_i] ? parent[keyArray[0].to_i] : array
parent[keyArray[0].to_i] = array
end
# *1. keyArray进行截取,1到最后一个元素,去除下标为0的元素
# 递归调用,逐层处理
makeUpHash(array,keyArray[1..keyArray.length-1],val,'array')
else
hash = Hash.new
if type == 'hash'
hash = parent[keyArray[0]] ? parent[keyArray[0]] : hash
parent[keyArray[0]] = hash
elsif type == 'array'
hash = parent[keyArray[0].to_i] ? parent[keyArray[0].to_i] : hash
parent[keyArray[0].to_i] = hash
end
# *1.keyArray进行截取,1到最后一个元素,去除下标为0的元素
# 递归调用,逐层处理
makeUpHash(hash,keyArray[1..keyArray.length-1],val,'hash')
end
end
#递归函数定义结束
#3.解析get请求的queryString,给result赋值
reqArgs = reqBody.split('&')
#4.遍历所有key=value 键值对
reqArgs.each do |rq|
key = rq.split('=')[0]
val = rq.split('=')[1]
#将 [] 换成 . example : arg1.subArg[0].id => arg1.subArg.0.id
keyWithDot = key.gsub('[','.').gsub(']','.').gsub('..','.')
#如果转换之后,最后是. 则把最后一位的.删除
if keyWithDot[keyWithDot.length-1] == '.'
keyWithDot = keyWithDot[0..keyWithDot.length-2]
end
keyArray = keyWithDot.split('.')
#递归调用函数,对result进行分层拼装
makeUpHash(result,keyArray,val,'hash')
end
event.set('result',result)
如上,ruby脚本中处理get参数的核心代码,在注释中已经将各种操作说明。
这种转换可以兼容本项目下所有的get请求接口
其实写完这块功能之后,不难发现,其实JAVA中也可以这样操作,把ruby中的hash对象类比为map对象,再不确定Controller层接收的参数实体类时,可以将参数封装为map,再将keyArray分层处理封装到map中,下标处理为list。 如下:
还是拿[‘user’,‘userIds’,‘0’,‘idType’]举例:
map的第一层有key:‘user’,map.get(‘user’)的下层是集合userIds,map.get(‘user’).get(‘userIds’)的第一个元素是个map,存在key:‘idType’ 。
最终这个map可以转为json串,{“user”:{“userIds”:[{“idType”:“value”}]}} 将这个json串再parseObject(jsonStr,Param.class), 转为Controller层方法参数中的接收类的实例对象 ,mvc层get请求转换为嵌套复杂对象就完成了~
当然了,这种是比较土、比较粗暴的方式,可以个人尝试。有兴趣的可以看看springMVC的源码,到底如何将get请求封装成我们定义的参数对象