JSON数据SLS查询分析实战

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: JSON是一种轻量级的数据交换格式,格式简单清晰,易于理解且便于处理,因此也常常作为日志数据的一种内容格式。本文主要介绍下,如何基于SLS强大的查询分析能力,对JSON格式的日志数据进行分析和处理。

前言

JSON是一种轻量级的数据交换格式,格式简单清晰,易于理解且便于处理,因此也常常作为日志数据的一种内容格式。本文主要介绍下,如何基于SLS强大的查询分析能力,对JSON格式的日志数据进行分析和处理。



举个例子

为了能够说明清楚json日志数据的一些典型的查询分析场景,这里我们首先来构造一个实际系统的例子。


假设我们有一个订单处理系统,客户端程序将订单请求发送到API接入网关,接入网关进行一些鉴权操作后,将请求转发给实际的订单业务服务器,处理完成后,API接入网关会对每一条请求打印记录一条日志,并采集到SLS,供开发人员进行分析。



假设这里的API网关打印的日志是json格式,每一行日志的格式如下图所示:

其中request字段是订单请求信息,一次请求可以包含一个用户的多个订单,订单里包含购买的商品和支付的总价格,简单起见假设每个商品一次只能买一个。 response字段是订单处理结果,并假设由于历史原因,这个字段不总是一个json,成功的时候是"SUCCESS",失败的时候是一个包含code和message的json形式的字符串。


接下来我们就来看看,对于这样的一份系统日志,如何基于SLS进行采集和分析。



采集日志

对于Object类型的JSON日志数据,可以配置logtail的json格式进行快速采集。在logstore中添加logtail配置的时候,选择“JSON-文本日志”

进入到logtail配置页面,对应的模式就是“JSON模式”,这种模式下会自动提取JSON的首层的键值作为日志的字段,对于本文的日志示例,就会采集出request、response两个日志字段。

注意,如果是Array类型的JSON,或者整行日志并不是一个完全标准的JSON,那么就不适用于“JSON模式”采集,这种情况下也可以用“极简模式”或者“完整正则模式”先将对应的日志部分采集上来作为一整个字段,再结合本文后面会提到的JSON函数进行具体的分析。



设置索引

日志采集到SLS之后,要进行查询分析,首先必须要建立索引。

全文索引还是字段索引?

首先索引分为全文索引和字段索引,全文索引就是对整条日志都按照分词符拆分并建立索引,字段索引就是仅针对日志中的某个字段建立索引。具体操作可以参考如何配置索引

建立哪一种索引呢?可以按照以下思路去决定:

(1)如果对日志中所有字段都有查询需求,那么可以建立全文索引;如果能够明确仅对部分字段有查询分析需求,那么也可以只对部分字段建立字段索引,减少一些索引费用。

(2)如果对某些字段有使用SQL进行分析的需求,那么就必须对该字段建立字段索引,并开启统计。

(3)同时建立全文索引和某些字段的索引,对于这部分字段只会以字段索引为准,不用担心会产生两份索引费用。


回到我们的例子,这里我们对示例中的request和response两个字段都有查询分析的需求,因此对这两个字段都要开启字段索引。


字段索引的类型?

字段索引的类型分为四种:text、long、double、json,设置什么类型的索引取决于对应字段的数据类型,具体操作可以参考字段索引类型说明

回到我们的主题,对于JSON字段如何决定设置哪种索引呢?可以按照以下思路来看:

(1)如果一个字段本身并不是标准的json,只是内容可能包含json,那么就设置为text类型;如果是标准的json,就设置为json类型。(注意如果非完整的json但是部分合法,可以设置为json类型,SLS会尝试解析出其中合法的部分)

(2)设置为json类型后,如果对JSON对象中某个叶子节点有进一步的分析需求,可以针对叶子节点单独建立一个索引,这样会大大加速对这个叶子节点的查询分析,同时也会产生额外的索引费用。

(3)不能给JSON数组类型的字段建立索引,也不能给JSON对象中包含叶子节点的中间节点建立索引。


这里我们再结合前面的例子来看一下应该如何进行索引配置。假设我们的分析需求是这样的:

那么相应的,我们的建立索引的决策如下:

  • request字段,需要建立为json类型的字段索引,并开启统计
  • clientIp叶子节点,因为需要经常分析,所以单独建立为子索引,text类型,并开启统计
  • http.path叶子节点,因为很少分析,不需要单独建立子索引。如果需要分析的时候,可以通过json函数解析
  • param子节点,是中间节点,不能建立子索引,跳过
  • userId叶子节点,因为要经常分析,所以单独建立为子索引,text类型,并开启统计
  • orders子节点,是中间节点,而且因为是array类型,不能再建立子索引
  • response字段,有分析需求,但因为不一定是JSON数据,因此建立为text类型索引,并开启统计

因此我们建立出来的字段索引如图所示:


建立了json类型索引之后,SLS上原始日志的展示也会自动按照json格式进行展开,查看起来非常方便。


设置索引别名

json叶子节点的索引由于包含的路径比较长,如果经常使用,可以设置一个索引别名,这样书写json会更方便。

需要注意的是,不同的字段之间,索引名或者索引别名之间是不能重复的,否则在SQL里就会产生歧义,不知道引用的是哪一列。

举个例子,我们如果对response字段索引设置一个别名叫request,在点击保存的时候就会报错,提示有重复。


对于json类型索引来说,在重复判定这里有一点特殊之处是,json的叶子节点的名称是按照全路径来判定的。

举个例子,如下图所示,假设想将response字段索引的别名设置为clientIp,是可以设置成功的。因为上面那个json叶子节点索引名称是request.clientIp,和response的别名clientIp是不算重复的。



查询分析——有索引的字段

对于有JSON对象叶子节点索引的字段,其查询分析用法和普通的日志字段相同,唯一要注意的就是在使用字段的时候,要使用从json对象根节点要叶子节点的全路径。此外在sql分析语句中使用时,需要加上双引号。


对于我们例子中的两个json叶子节点索引,在使用的时候要分别用request.clientIprequest.param.userId

比如我们要查询用户186499都有哪些来源ip,就可以用这样的语句,注意其中字段名称的写法:

*and request.param.userId:186499|selectdistinct("request.clientIp")


查询分析——使用JSON函数

使用JSON函数,可以灵活的对JSON数据进行各种动态处理和分析,json相关的处理函数的详细说明可以参考JSON函数,这里我们结合示例日志,对使用json函数中的一些典型问题进行介绍。


什么情况下使用JSON函数?

首先,使用JSON函数是在执行SQL的时候,实时的对JSON数据进行解析,其性能受数据量大小和json结构复杂度的影响。

因此,如果数据量很大或者结构复杂,并且对查询性能有要求,同时json对象的结构相对比较固定,建议优先考虑对JSON叶子节点单独建立字段索引的方式。

而如果数据量比较小,或者出于成本考虑,则可以不建立json叶子节点索引,用json函数来处理。


另外,有些情况只能用json函数去处理:

(1)json对象包含的字段不确定,或者需要先进行一些预处理

比如示例中的response字段,只有在请求失败的时候是json结构,那么现在我们想分析其中的errcode字段的分布情况,就可以先用查询语句过滤出失败的情况,然后在SQL中用json函数动态的提取出相应的字段

*not response:SUCCESS |select json_extract_scalar(response,'$.errcode')


(2)对于json数组,或者json对象的中间节点,这些不能单独建立索引的,只能用json函数实时分析。如例子中的request.param或者request.param.orders部分


json_extract和json_extract_scalar用哪个?

一般来说,json函数最常用的场景就是从json中直接提取出某个字段进行分析。涉及到的函数有两个,json_extract 和 json_extract_scalar,这两个函数的函数名和入参都非常类似,经常有开发者会感到困扰,这两个函数有什么区别呢?


  • 首先区别在于返回值的类型

json_extract返回的是一个json类型,json_extract_scalar返回的是一个varchar类型。

注意,这里的类型指的是SQL中的数据类型,如varchar、bigint、boolean、json、array、date等等,和前面说的索引的类型不是一个概念。

使用typeof函数,可以查看对应的SQL对象是什么样的类型。

  • 其次区别在于能够解析的部分

json_extract可以解析json中任意一块子结构,json_extract_scalar只能解析标量类型(字符串、布尔或者整形值)的叶子节点,返回的是对应的字符串表示(varchar类型)


那么从使用角度有什么区别呢?我们再结合例子看一下。

假设我们现在要提取request字段中的clientIp字段,那么可以用json_extract_scalar函数 和 json_extract函数看上去都能提取出来,也就说,以下这两条语句都能执行成功:

#使用json_extract_scalar函数
*|select json_extract(request,'$.clientIp')#使用json_extract函数
*|select json_extract_scalar(request,'$.clientIp')


现在假设我们想进一步对clientIp进行来源分析,比如我们想对提取出clientIp的第一段,看看ip归属于哪个大的网段。这里我们先使用json_extract_scalar将clientIp提取出来,然后用split_part函数将clientIp按照"."分割并提取出第一段,然后进行分组求和并排序。

SQL语句如下,执行成功,符合预期。

*|select split_part(json_extract_scalar(request,'$.clientIp'),'.',1)as segment,count(1)as cnt groupby segment


但是如果我们把上面的SQL中的json_extract_scalar 换成 json_extract,执行的时候就会报错

从报错信息中也可以看出来,原因就是split_part是一个字符串处理函数,入参必须要是varchar类型,而json_extract返回的是一个json类型,因此参数类型不匹配导致报错。


画外音:我们在写SQL的过程中,可以先对子语句执行下typeof函数,来确认下当前的对象是什么类型。


一般来说,从json对象中提取字段进行分析,直接使用json_extract_scalar函数即可。原因是这样可以方便的结合其他SQL语句或函数进行进一步的分析。

使用json_extract_scalar要注意一点,就是其返回值始终是varchar类型,比如解析出的是一个数值,那在进一步进行求和之类的分析之前,要先用cast(XXX as bigint)转为数值类型


那么json_extract函数在什么场景下使用呢?

当我们需要对json结构本身进行一些分析处理的时候。

比如前面例子中的request字段中,要统计某个用户的在这次请求中的订单数目,也就是要查看orders子节点这个json数组的长度。

那我们就可以用json_extract函数,先提取出orders这个子节点,然后再用json_array_length函数统计长度。

*|select json_array_length((json_extract(request,'$.param.orders')))


但如果把上面的SQL里的json_extract换成json_extract_scalar,执行出来的结果就是null

原因就在于json_extract_scalar只能将json对象的叶子节点提取为标量类型,并不能正确的提取json对象的中间节点。


json_path应该怎么写?

使用json_extract等函数从json数据中提取字段的时候,需要指定json_path字段,用来表明需要提取json中的哪一部分。

  • 首先json_path的基本格式是类似"$.a.b",$符号代表当前JSON对象根节点,然后通过"."号引用到要提取的节点,这个前面已经写过一些示例sql。


  • 那如果json的key值本身是a.b的形式,或者包含一些其他的特殊字符呢?

这种情况下可以用中括号[]代替.号,中括号里的节点名称要用双引号括起来。

比如示例中request字段中有个“http.path”字段,为了提取这个字段进行分析,可以这样写:

*|select json_extract_scalar(request,'$["http.path"]')

如果是在sdk中写query,那么要对双引号进行转义


  • 那如果是要获提取json数组中的某个元素呢?

这种情况下,可以用中括号[],中括号中用数字来表示数字下标,下标从0开始

比如示例中,我们要查看每个用户下的第一个订单的金额,就可以这样写:

*|select json_extract_scalar(request,'$.param.orders[0].payment')

如果要查看每个用户的第一个订单中购买的第二件商品,可以这样写:

*|select json_extract_scalar(request,'$.param.orders[0].commodity[1]')


json数组如何展开分析?

当日志字段中有json数组时,我们往往需要将json数组中的元素展开来,进行一些聚合统计分析。我们可以结合cast语句和unnest语句来实现。


使用unnest展开json数组

回到前面的示例,假设我们现在要求出所有成功请求的订单的金额之和,示意图如下:

首先用查询语句response字段过滤成功的请求,然后在SQL中提取出request字段中的orders节点,这个是一个json数组,注意这里提取出的是一个json对象,因此要用json_extract函数

*and response: SUCCESS |select json_extract(request,'$.param.orders')

这样查询出的每一行都是一个订单信息,是json类型

接下来我们使用cast语句,将这个json数组转换为sql里的数组,array(json)类型

*and response: SUCCESS |select cast(json_extract(request,'$.param.orders')as array(json))

执行结果的每一行是一个数组,数组的元素是单个的json对象

然后再用unnest语句将数组展开

*and response: SUCCESS |select orderinfo from log, unnest(cast(json_extract(request,'$.param.orders')as array(json)))as t(orderinfo)

执行结果的每一行是一个json对象


接下来就简单了,对每一行用json_extract_scalar提取出payment字段,这是一个varchar类型,再转换为bigint数值类型,然后再求和即可,最终的SQL语句如下:

*and response: SUCCESS |select sum(cast(json_extract_scalar(orderinfo,'$.payment')asbigint))from log, unnest(cast(json_extract(request,'$.param.orders')as array(json)))as t(orderinfo)


多重json数组嵌套

我们再看更复杂一些的情况,如果是JSON数组的元素对象中还有JSON数组,该怎么分析呢?

比如在示例中,要统计所有成功的请求中的,每一种商品被购买的数目

这个的解决思路,也是类似的,先将orders字段提取出来,转换成json(array),unnest展开,展开的结果每一行orderinfo是一个json对象,代表一个订单。对展开后的结果,json_extract提取出commodity数组,再转换成json(array),再unnest展开,这样展开出来的结果,每一行是一个商品。然后再进行分组求和统计即可。

中间步骤就不再赘述了,最终的完整SQL和执行结果如下:

*and response: SUCCESS  |select item,count(1)as cnt from(select orderinfo from log, unnest(cast(json_extract(request,'$.param.orders')as array(json)))as t(orderinfo)), unnest(cast(json_extract(orderinfo,'$.commodity')as array(json)))as t(item)groupby item orderby cnt desc



结语

本文从一个实际系统的json日志出发,详细介绍了json日志数据的日志采集、索引选择、查询分析、JSON函数使用等方面的最佳实践,希望可以帮助开发者对基于SLS分析json日志数据有一个全面完整的认识。如果在使用上还有疑问,欢迎通过工单或者答疑群的方式进一步咨询。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
4天前
|
分布式计算 资源调度 DataWorks
MaxCompute操作报错合集之出现“查询运行日志失败”的报错,一般是什么导致的
MaxCompute是阿里云提供的大规模离线数据处理服务,用于大数据分析、挖掘和报表生成等场景。在使用MaxCompute进行数据处理时,可能会遇到各种操作报错。以下是一些常见的MaxCompute操作报错及其可能的原因与解决措施的合集。
|
17天前
|
Python
Python编程实战:利用闭包与装饰器优化日志记录功能
【7月更文挑战第7天】Python的闭包和装饰器简化了日志记录。通过定义如`log_decorator`的装饰器,可以在不修改原函数代码的情况下添加日志功能。当@log_decorator用于`add(x, y)`函数时,调用时自动记录日志。进一步,`timestamp_log_decorator`展示了如何创建特定功能的装饰器,如添加时间戳。这些技术减少了代码冗余,提高了代码的可维护性。
19 1
|
5天前
|
JSON 分布式计算 大数据
MaxCompute产品使用合集之如何解析嵌套的JSON数据
MaxCompute作为一款全面的大数据处理平台,广泛应用于各类大数据分析、数据挖掘、BI及机器学习场景。掌握其核心功能、熟练操作流程、遵循最佳实践,可以帮助用户高效、安全地管理和利用海量数据。以下是一个关于MaxCompute产品使用的合集,涵盖了其核心功能、应用场景、操作流程以及最佳实践等内容。
|
12天前
|
JSON JavaScript 前端开发
技术心得:利用JsonSchema校验json数据内容的合规性
技术心得:利用JsonSchema校验json数据内容的合规性
12 0
|
18天前
|
JSON 数据格式
MysbatisPlus-核心功能-IService开发基础业务接口,MysbatisPlus_Restful风格,新增@RequestBody指定是为了接收Json数据的,使用swagger必须注解
MysbatisPlus-核心功能-IService开发基础业务接口,MysbatisPlus_Restful风格,新增@RequestBody指定是为了接收Json数据的,使用swagger必须注解
|
19天前
|
JSON 数据格式
Unsupported Media Type,传入的字符串数据:这里应该是Json
Unsupported Media Type,传入的字符串数据:这里应该是Json
|
19天前
|
JSON 数据格式
Navicate数据报错之Bad Request,发送端口测试的时候,使用JSON字符串,而不是Text
Navicate数据报错之Bad Request,发送端口测试的时候,使用JSON字符串,而不是Text
|
1月前
|
存储 JSON JavaScript
【chat-gpt问答记录】python将数据存为json格式和yaml格式
【chat-gpt问答记录】python将数据存为json格式和yaml格式
39 1
|
20天前
|
JSON 文字识别 数据格式
文本,文识11,解析OCR结果,paddOCR返回的数据,接口返回的数据有code,data,OCR返回是JSON的数据,得到JSON数据先安装依赖,Base64转换工具网站在21.14
文本,文识11,解析OCR结果,paddOCR返回的数据,接口返回的数据有code,data,OCR返回是JSON的数据,得到JSON数据先安装依赖,Base64转换工具网站在21.14
文本,文识11,解析OCR结果,paddOCR返回的数据,接口返回的数据有code,data,OCR返回是JSON的数据,得到JSON数据先安装依赖,Base64转换工具网站在21.14
|
22天前
|
存储 JSON JavaScript
使用JSONObject解析与生成JSON数据
使用JSONObject解析与生成JSON数据