首页 文章详情

.NET6 使用 NEST 查询Elasticsearch,时间字段传值踩坑

DotNet NB | 3 2023-07-28 13:47 0 0 0
UniSMS (合一短信)

0x01业务描述

说明: 同事搭建的业务系统,最开始使用 log4net  记录到本地日志. 然后多个项目为了日志统一,全部记录在 Elasticsearch ,使用  log4net.ElasticSearchAppender.DotNetCore.

然后搭建了 Kibanal  对 Elasticsearch  进行查询.  但是项目组开发人员众多,不是每个人都想要学会如何在 Kibanal   中查询日志. 

所以 就需要开发一个  有针对性的, 查询用户界面.  最近这个功能就交到我手上了.

方案是: 通过 NEST  查询  Elasticsearch   的接口,  将前端页面传过来的参数组装成 NEST 的查询请求.

 

0x02主要实现代码

日志索引为:  xxxapilog_* 

时间关键字段为:  "@timestamp"

/// <summary>        /// 根据查询条件,封装请求        /// </summary>        /// <param name="query"></param>        /// <returns></returns>        public async Task<ISearchResponse<Dictionary<string, object>>> GetSearchResponse(API_Query query)        {            int size = query.PageSize;            int from = (query.PageIndex - 1) * size;            ISearchResponse<Dictionary<string, object>> searchResponse1 = await elasticClient.SearchAsync<Dictionary<string, object>>(searchDescriptor =>            {                Field sortField = new Field("@timestamp");                return searchDescriptor.Index("xxxapilog_*")                .Query(queryContainerDescriptor =>                {                    return queryContainerDescriptor.Bool(boolQueryDescriptor =>                    {                        IList<Func<QueryContainerDescriptor<Dictionary<string, object>>, QueryContainer>> queryContainers = new List<Func<QueryContainerDescriptor<Dictionary<string, object>>, QueryContainer>>();
if (!string.IsNullOrEmpty(query.Level)) { queryContainers.Add(queryContainerDescriptor => { return queryContainerDescriptor.Term(c => c.Field("Level").Value(query.Level.ToLower())); }); } if (query.QueryStartTime.Year>=2020) { queryContainers.Add(queryContainerDescriptor => { return queryContainerDescriptor.DateRange(c => c.Field("@timestamp").GreaterThanOrEquals(query.QueryStartTime)); });
} if (query.QueryEndTime.Year >= 2020) { queryContainers.Add(queryContainerDescriptor => { return queryContainerDescriptor.DateRange(c => c.Field("@timestamp").LessThanOrEquals(query.QueryEndTime)); }); } //...省略其他字段 相关查询
boolQueryDescriptor.Must(x => x.Bool(b => b.Must(queryContainers))); return boolQueryDescriptor; }); }) .Sort(q => q.Descending(sortField)) .From(from).Size(size); }); return searchResponse1; }

 

接口参数类:

/// <summary>    /// api接口日志查询参数    /// </summary>    public class API_Query    {        /// <summary>        /// 默认第一页        /// </summary>        public int PageIndex { get; set; }
/// <summary> /// 默认页大小为500 /// </summary> public int PageSize { get; set; }
/// <summary> /// WARN 和 INFO /// </summary> public string Level { get; set; }
/// <summary> /// 对应@timestamp 的开始时间,默认15分钟内 /// </summary> public string StartTime { get; set; } /// <summary> /// 对应@timestamp 的结束时间,默认当前时间 /// </summary> public string EndTime { get; set; }

public DateTime QueryStartTime { get; set; } public DateTime QueryEndTime { get; set; } }

 

0x03 时间字段预处理

PS: 如果  StartTime 和 EndTime  都不传值, 那么 默认设置 只查最近的 15分钟 

封装一下 QueryStartTime 和 QueryEndTime
public DateTime QueryStartTime        {            get            {                DateTime dt = DateTime.Now.AddMinutes(-15);                if (!string.IsNullOrEmpty(StartTime) && StartTime.Trim() != "")                {                    DateTime p;                    DateTime.TryParse(StartTime.Trim(), out p);                    if (p.Year >= 2020)                    {                        dt = p;                    }                }                return dt;            }        }
public DateTime QueryEndTime { get {
DateTime dt = DateTime.Now; if (!string.IsNullOrEmpty(EndTime) && EndTime.Trim() != "") { DateTime p; DateTime.TryParse(EndTime.Trim(), out p); if (p.Year >= 2020) { dt = p; } } return dt; } }

0x04 查找问题原因

以上 封装,经过测试, 能够获取到查询数据. 但是,但是 ,但是 坑爹的来了,当 外面传入参数 
API_Query query = new API_Query () { PageIndex=1, PageSize=10,StartTime = "2023-04-28",EndTime = "2023-04-28 15:00:00"}; 
查询的结果集里面居然有 2023-04-28 15:00:00 之后的数据. 使用的人反馈到我这里以后,我也觉得纳闷,啥情况呀.

需要监听一下 NEST 请求的实际语句
public class ESAPILogHelper    {        ElasticClient elasticClient;        /// <summary>        /// es通用查询类        /// </summary>        /// <param name="address"></param>        public ESAPILogHelper(string address)        {            elasticClient = new ElasticClient(new ConnectionSettings(new Uri(address)).DisableDirectStreaming()                .OnRequestCompleted(apiCallDetails =>                {                    if (apiCallDetails.Success)                    {                        string infos = GetInfosFromApiCallDetails(apiCallDetails);                        //在此处打断点,查看请求响应的原始内容                        Console.WriteLine(infos);                }));        }
private string GetInfosFromApiCallDetails(IApiCallDetails r) { string infos = ""; infos += $"Uri:\t{r.Uri}\n"; infos += $"Success:\t{r.Success}\n"; infos += $"SuccessOrKnownError:\t{r.SuccessOrKnownError}\n"; infos += $"HttpMethod:\t{r.HttpMethod}\n"; infos += $"HttpStatusCode:\t{r.HttpStatusCode}\n"; //infos += $"DebugInformation:\n{r.DebugInformation}\n"; //foreach (var deprecationWarning in r.DeprecationWarnings) // infos += $"DeprecationWarnings:\n{deprecationWarning}\n"; if (r.OriginalException != null) { infos += $"OriginalException.GetMessage:\n{r.OriginalException.Message}\n"; infos += $"OriginalException.GetStackTrace:\n{r.OriginalException.Message}\n"; } if (r.RequestBodyInBytes != null) infos += $"RequestBody:\n{Encoding.UTF8.GetString(r.RequestBodyInBytes)}\n"; if (r.ResponseBodyInBytes != null) infos += $"ResponseBody:\n{Encoding.UTF8.GetString(r.ResponseBodyInBytes)}\n"; infos += $"ResponseMimeType:\n{r.ResponseMimeType}\n"; return infos; }

请求分析:

 

如果  StartTime 和 EndTime  都不传值 , 请求的 参数为 
{    "from": 0,    "query": {        "bool": {            "must": [                {                    "bool": {                        "must": [                            {                                "range": {                                    "@timestamp": {                                        "gte": "2023-04-28T17:44:09.6630219+08:00"                                    }                                }                            },                            {                                "range": {                                    "@timestamp": {                                        "lte": "2023-04-28T17:59:09.6652844+08:00"                                    }                                }                            }                        ]                    }                }            ]        }    },    "size": 10,    "sort": [        {            "@timestamp": {                "order": "desc"            }        }    ]}

 

如果  StartTime  EndTime  传入 2023-04-28  2023-04-28 15:00:00, 请求的 参数为 
{    "from": 0,    "query": {        "bool": {            "must": [                {                    "bool": {                        "must": [                            {                                "range": {                                    "@timestamp": {                                        "gte": "2023-04-28T00:00:00"                                    }                                }                            },                            {                                "range": {                                    "@timestamp": {                                        "lte": "2023-04-28T15:00:00"                                    }                                }                            }                        ]                    }                }            ]        }    },    "size": 10,    "sort": [        {            "@timestamp": {                "order": "desc"            }        }    ]}
对比后发现 , 时间传值有2种不同的格式 
"@timestamp": { "gte": "2023-04-28T17:44:09.6630219+08:00" }"@timestamp": {"gte": "2023-04-28T00:00:00" }
 

  这两种格式 有什么 不一样呢? 

0x05 测试求证

 我做了个测试 

//不传参数, 默认结束时间为当前时间DateTime end_current = DateTime.Now;
//如果传了参数, 使用 DateTime.TryParse 取 结束时间DateTime init = query.QueryEndTime;DateTime endNew = new DateTime(init.Year, init.Month, init.Day, init.Hour, init.Minute, init.Second);

//这一步是 为了 补偿 时间值, 让 enNew 和 end_current 的ticks 一致
long s1_input = endNew.Ticks;long s2_current = end_current.Ticks;endNew = endNew.AddTicks(s2_current - s1_input);
long t1 = endNew.Ticks;long t2 = end_current.Ticks;

//对比 end_current 和 endNew, 现在的确是 相等的.bool isEqual = t1 == t2; // 结果为 true

//但是, 传入 end_current 和 enNew,执行的请求 却不一样,
queryContainerDescriptor.DateRange(c => c.Field("timeStamp").LessThanOrEquals(end_current));
==>请求结果为: 2023-04-28T17:44:09.6630219+08:00
queryContainerDescriptor.DateRange(c => c.Field("timeStamp").LessThanOrEquals(enNew)); ==>请求结果为: 2023-04-28T17:44:09.6630219Z

 

进一步测试 

isEqual = endNew == end_current; //结果 true 
isEqual = endNew.ToUniversalTime() == end_current.ToUniversalTime(); //结果仍然为true
isEqual = endNew.ToLocalTime() == end_current.ToLocalTime(); //结果居然为 fasle !!!
基于以上测试, 算是搞明白了是怎么回事.
比如现在是北京时间 : DateTime.Now 值为 2023-04-28 15:00:00, 那么 DateTime.Now.ToLocalTime() 还是 2023-04-28 15:00:00
Console.WriteLine(DateTime.Now.ToLocalTime());
如是字符串 DateTime.Parse("2023-04-28 15:00:00").ToLocalTime(), 值为  2023-04-28 23:00:00   (比2023-04-28 15:00:008 个小时)

那么回到题头部分, 当用户输入
2023-04-28  2023-04-28 15:00:00, 实际查询的数据范围为  2023-04-28 08:00:00  2023-04-28 23:00:00 自然就显示出了 2023-04-28 15点以后的数据,然后因为是倒序,又分了页
所以看不出日志的开始时间, 只能根据日志的结果时间 发现超了,来诊断.

0x06 解决方案



基于以上测试, 现在统一用 ToUniversalTime,即可保持数据的一致
 isEqual = endNew.ToUniversalTime().ToLocalTime() == end_current.ToUniversalTime().ToLocalTime(); //结果为true 
 Console.WriteLine(isEqual); //结果为 true

那么修改一下参数的取值
public DateTime QueryStartTime        {            get            {                DateTime dt = DateTime.Now.AddMinutes(-15);                if (!string.IsNullOrEmpty(StartTime) && StartTime.Trim() != "")                {                    DateTime p;                    DateTime.TryParse(StartTime.Trim(), out p);                    if (p.Year >= 2020)                    {                        dt = p;                    }                }                return dt.ToUniversalTime();            }        }
public DateTime QueryEndTime { get {
DateTime dt = DateTime.Now; if (!string.IsNullOrEmpty(EndTime) && EndTime.Trim() != "") { DateTime p; DateTime.TryParse(EndTime.Trim(), out p); if (p.Year >= 2020) { dt = p; } } return dt.ToUniversalTime(); } }

 

好了, 现在问题解决了!!!
==>由此 推测
 return queryContainerDescriptor.DateRange(c => c.Field("timeStamp").GreaterThanOrEquals(DateMath from));
DateMath from 使用了 ToLocalTime .

0x07 简单测试用例


这里贴上简要的测试用例,方便重现问题.
static void Main(string[] args)        {            //首先 读取配置             Console.WriteLine("程序运行开始");
try {
//不传参数, 默认结束时间为当前时间 DateTime end_current = DateTime.Now;
//如果传了参数, 使用 DateTime.TryParse 取 结束时间 DateTime init = new DateTime() ; DateTime.TryParse("2023-04-28 15:00:00", out init); DateTime endNew = new DateTime(init.Year, init.Month, init.Day, init.Hour, init.Minute, init.Second);
//这一步是 为了 补偿 时间值, 让 enNew 和 end_current 的ticks 一致
long s1_input = endNew.Ticks; long s2_current = end_current.Ticks; endNew = endNew.AddTicks(s2_current - s1_input);
//对比 end_current 和 enNew, 现在的确是 相等的. long t1 = endNew.Ticks; long t2 = end_current.Ticks; bool isEqual = t1 == t2; // 结果为 true Console.WriteLine(isEqual); isEqual = endNew == end_current; Console.WriteLine(isEqual);
isEqual = endNew.ToUniversalTime() == end_current.ToUniversalTime(); Console.WriteLine(isEqual);

isEqual = endNew.ToLocalTime() == end_current.ToLocalTime(); Console.WriteLine(isEqual);
Console.WriteLine(endNew.ToLocalTime()); Console.WriteLine(end_current.ToLocalTime());
DateTime dinit; DateTime.TryParse("2023-04-28 15:00:00", out dinit); Console.WriteLine(dinit.ToLocalTime());

isEqual = endNew.ToUniversalTime().ToLocalTime() == end_current.ToUniversalTime().ToLocalTime(); Console.WriteLine(isEqual); } catch (Exception ex) { string msg = ex.Message; if (ex.InnerException != null) { msg += ex.InnerException.Message; } Console.WriteLine("程序运行出现异常"); Console.WriteLine(msg); }
Console.WriteLine("程序运行结束"); Console.ReadLine(); }


推荐阅读:
支持多语言、多商店的商城,.Net7 + EF7领域驱动设计架构
开源 .NET 低代码工作流引擎
盘点8个.Net开源项目
.NET Core 工作流WorkFlowCore
使用C#编写.NET分析器-第三部分
.NET Core使用MongoDB开发ToDoList系统(8)-Ant Design Blazor前端框架搭建

点击下方卡片关注DotNet NB

一起交流学习

▲ 点击上方卡片关注DotNet NB,一起交流学习

请在公众号后台

回复 【路线图】获取.NET 2023开发者路线图
回复 【原创内容】获取公众号原创内容
回复 【峰会视频】获取.NET Conf开发者大会视频
回复 【个人简介】获取作者个人简介
回复 【年终总结】获取作者年终总结
回复 加群加入DotNet NB 交流学习群

长按识别下方二维码,或点击阅读原文。和我一起,交流学习,分享心得。



good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter