上一篇文章主要介绍的是 Talog 的日志查询,但是其核心查询能力较为简单,只能根据标签找到对应的日志文件,然后将所有日志全部输出。为了满足更加复杂的日志查询场景,我为 Talog 扩展了日志查询能力。
名词解释
为了缩短篇幅,后文会用到几个缩略词,在此先进行说明。
- 正则字段:通过正则表达式对日志文本进行解析,获得的若干个字段。例如:
日志文本:3.252.1.168 - - [28/Jan/2023:14:04:55 +0800] "POST /talog/log/remove HTTP/1.1" 200 31 "https://vbranch.cn/talog/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
正则表达式:[^ ]+ - - \[(?<date>[^\]]+)\] "(?<method>[^ ]+) (?<path>[^ ]+) (?<schema>[^"]+)" (?<code>\d+) (?<length>\d+) "(?<reference>[^"]+)" "(?<agent>[^"]+)"
正则字段:
- date: 28/Jan/2023:14:04:55 +0800
- method: POST
- path: /talog/log/remove
- schema: HTTP/1.1
- code: 200
- length: 31
- reference: https://vbranch.cn/talog/
- agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
- json 字段:若日志索引时是 json 格式,则对日志进行 json 反序列化所获得的复杂对象的字段即为 json 字段
- 标签字段:指日志的标签属性
使用表达式字符串来进一步筛选日志
由于 V.Talog.Core 核心包的日志查询功能较为简单,因此很难满足许多日志查询的应用场景,比如想要查询某个用户在某个时间段(起止日期在同一天)的操作日志。对于这种场景,个人建议使用用户编号以及日期(yyyy-MM-dd 格式)作为标签,此时查询某个用户的目标可以通过设置 userId=xxx
的查询表达式来实现,但是时间段就无法通过核心包来筛选了。此时,我们只能先获取到该用户当天的所有日志(userId=xxx&date=yyyy-MM-dd
),然后再对日志数据进一步筛选。
Talog 的做法就是对每一条日志都进行解析,解析得到正则字段或者 json 字段,然后再根据这些字段做日志筛选。但是由于我希望基于字段的筛选能够支持复杂表达式,因此很难设计出一个比较简洁易用的流式 api,所以我最决定终只支持使用表达式字符串的方式来实现字段筛选。而且日志查询的使用场景一般也都是使用浏览器排查业务日志,应该很少人会在程序中有查询日志的需求,因此只支持使用表达式字符串筛选日志的方式应该是合理的。
表达式字符串解析
Talog 使用 V.QueryParser
来解析表达式字符串,具体解析逻辑可以翻阅文档。值得一提的是,V.QueryParser 对表达式格式要求较为严格,例如 key1 > value1 && key2 == value2 || key3 like value3
,关键词与关键词之间、关键词与运算符之间都需要有空格,否则会解析失败。
扩展查询能力
接下来,将使用以下接口来具体解释 V.Talog.Extension 是如何扩展 Talog 的查询能力的。
/// <summary>
/// 使用正则表达式匹配日志,并支持基于匹配结果做进一步字段筛选
/// <para>支持基于标签、正则字段进行排序</para>
/// </summary>
/// <param name="searcher"></param>
/// <param name="query">标签查询</param>
/// <param name="regex">用于匹配日志的正则表达式</param>
/// <param name="regexQuery">基于正则匹配结果的字段查询</param>
/// <param name="sort">排序表达式</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static List<ParsedLog> SearchLogs(this Searcher searcher, Query query, string regex, string regexQuery = null, string sort = null)
在该扩展接口中,首先会调用 Searcher.SearchLogs(query)
查询出所有日志,需要特别说明的是 Query 在 V.Talog.Extension 扩展包中可以使用表达式字符串生成(使用 V.QueryParser 解析),这个可以通过调用以下接口实现。
/// <summary>
/// 根据查询表达式构建 Query
/// </summary>
/// <param name="talogger"></param>
/// <param name="index"></param>
/// <param name="expression"></param>
/// <returns></returns>
public static Query CreateQueryByExpression(this Talogger talogger, string index, string expression)
说回上一个接口,在调用 SearchLogs 获取到所有日志数据之后,接口会根据 regex 参数将每一条日志都解析成 ParsedLog(ParsedLog 包含正则字段)。随后,会使用 V.QueryParser 将 regexQuery 参数转换成 QueryExpression 对象,然后解析、执行表达式结构,表达式中所使用到的字段名均需要在 ParsedLog 中可以找到,否则将会报错。最后,接口还支持传入排序表达式 sort,格式为:type asc then date desc
,每个单词之间必须用一个空格隔开。排序表达式中的字段可以是正则字段也可以是标签字段。
有了扩展查询接口,就可以灵活得满足各种日志查询场景了,还是用刚刚查询操作日志的例子,可以使用以下代码来达到目的。
// 假设日志文本格式为:[2023-02-23 13:11:05] 用户小明(123456)手机登录
// 日志标签为 {"userId": "123456", "date": "2023-02-23"}
using talogger = new Talogger();
var query = talogger.CreateQueryByExpression("index",
"userId == '123456' && date == '2023-02-23'");
var searcher = talogger.CreateSearcher("index");
var logs = searcher.SearchLogs(query, "\[(?<time>[^\]]+)\] (?<msg>.*))",
"time >= '2023-02-23 13:00:00' && time <= '2023-02-23 14:00:00'",
"time desc");
// logs 为 userId=123456 的用户在 2023-02-23 13:00:00~2023-02-23 14:00:00 的操作日志
配置字段类型
在使用 regexQuery 筛选日志以及使用 sort 排序日志时,都会涉及到字段数据的比较,但是 Talog 大部分情况下是无法知道字段的类型的(除了 json 字段),因此 Talog 需要用户预先告知字段类型,V.Talog.Extension.TaloggerExtension 提供了 SetIndexMapping 接口,接口仅需要传入一个接口参数 IIndexMapping,该接口定义了两个方法分别用于获取标签字段类型以及获取 json/正则字段类型。
public interface IIndexMapping
{
Type GetTagType(string index, string tag);
/// <summary>
/// 获取字段类型
/// </summary>
/// <param name="index"></param>
/// <param name="field">正则参数或 json 参数</param>
/// <returns></returns>
Type GetFieldType(string index, string field);
}
IIndexMapping 接口并不需要返回所有字段的类型,你只需要配置那些会用于筛选、排序的 json/正则字段以及标签字段。当使用 Talog 的扩展方法查询日志时,Talog 无法获取对应字段的类型,则会抛出异常,异常信息大致为:index {index name} 未配置 {字段} 的数据类型
性能提醒
V.Talog.Extension 扩展包虽然为 Talog 带来了更灵活的查询能力,但日志的解析、筛选、排序都是需要实时计算的,并没有任何预先处理,因此可以想象性能是难以得到保障的,特别是随着日志量的增加,效率会明显下降。
以我自己线上的 nginx 日志为例,目前已积累了 30W+ 条日志。
仅通过标签查询时,耗时为 400ms,虽然不快,但也不慢。
但是当我使用到了正则字段查询时,耗时变成了 13s,可以看到查询效率变得特别低,这个就是因为需要对 30W+ 条日志进行实时解析、对比带来了高额的时间成本。这个例子也算是一个使用 Talog 的反面教材吧,因为这 30W+ 条日志是放在同一个 Bucket 下的,如果能打上更合理的标签,应该不会出现这样的情况。
上一篇文章为大家展示了标签过于细致化,导致产生了大量的 Bucket 所带来的性能问题,本文又为大家展示了标签过于笼统,导致一个 Bucket 存放过多日志所带来的性能问题,这两个例子都说明了只有合理地为日志打上标签才能够发挥出 Talog 的性能。
最后
到此为止,Talog 的核心原理基本就介绍完毕了,下一篇文章会为大家介绍一下基于 Talog 开发的应用 V.Talog.Server。