ELK开发过程中,遇到get请求复杂参数转为json对象的解决过程及思路

ELK开发过程中,遇到get请求复杂参数转为json对象的解决过程及思路

      • 背景介绍
      • 难点总结:
      • 解决思路:
      • 解决方案:
      • 核心代码:
      • 总结

背景介绍

在最近项目开发的过程中,用到了ELK收集nginx日志,对关键的业务接口进行统计。在开发过程中,遇到了一个非常棘手的问题,那就是有的接口支持get方式,有的接口支持post方式,但是最终都要把请求报文封装成json对象存放到elastic中。而nginx日志中,post请求的参数相对较为简单,通过nginx log_format中的request_body可以直接获取请求体,请求体本身就是json字符串。而get请求,参数是在nginx log_format中request参数所代表的url地址之后,通过?和&拼接,并且由于接口协议较为复杂,所以嵌套对象和集合非常多,较单层对象的解析要复杂得多。 找个客户端拼接的参数还不算全的get请求作为例子:
get请求示例,图1
比如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参数的核心代码,在注释中已经将各种操作说明。

最终效果图如下:
ELK开发过程中,遇到get请求复杂参数转为json对象的解决过程及思路_第1张图片

总结

这种转换可以兼容本项目下所有的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请求封装成我们定义的参数对象

你可能感兴趣的:(ELK开发过程中,遇到get请求复杂参数转为json对象的解决过程及思路)