您当前的位置:首页> 行业动态 >正文
.net 6 使用 NEST 查询,时间字段传值踩坑_全球独家

2023-04-29 11:39:40     来源 : 博客园

0x01业务描述

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

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

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


【资料图】

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

0x02主要实现代码

日志索引为: xxxapilog_*

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

1         ///  2         /// 根据查询条件,封装请求 3         ///  4         ///  5         ///  6         public async Task>> GetSearchResponse(API_Query query) 7         { 8             int size = query.PageSize; 9             int from = (query.PageIndex - 1) * size;10             ISearchResponse> searchResponse1 = await elasticClient.SearchAsync>(searchDescriptor =>11             {12                 Field sortField = new Field("@timestamp");13                 return searchDescriptor.Index("xxxapilog_*")14                 .Query(queryContainerDescriptor =>15                 {16                     return queryContainerDescriptor.Bool(boolQueryDescriptor =>17                     {18                         IList>, QueryContainer>> queryContainers = new List>, QueryContainer>>();19 20                         if (!string.IsNullOrEmpty(query.Level))21                         {22                             queryContainers.Add(queryContainerDescriptor =>23                             {24                                 return queryContainerDescriptor.Term(c => c.Field("Level").Value(query.Level.ToLower()));25                             });26                         }27                         if (query.QueryStartTime.Year>=2020)28                         {29                             queryContainers.Add(queryContainerDescriptor =>30                             {31                                 return queryContainerDescriptor.DateRange(c => c.Field("@timestamp").GreaterThanOrEquals(query.QueryStartTime));32                             });33 34                         }35                         if (query.QueryEndTime.Year >= 2020)36                         {37                             queryContainers.Add(queryContainerDescriptor =>38                             {39                                 return queryContainerDescriptor.DateRange(c => c.Field("@timestamp").LessThanOrEquals(query.QueryEndTime));40                             });41                         }42                        //...省略其他字段 相关查询43 44                         boolQueryDescriptor.Must(x => x.Bool(b => b.Must(queryContainers)));45                         return boolQueryDescriptor;46                     });47                 })48                 .Sort(q => q.Descending(sortField))49                 .From(from).Size(size);50             });51             return searchResponse1;52         }

接口参数类:

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

调用方式:

API_Query query = new API_Query () { PageIndex=1, PageSize=10 }; ISearchResponse> searchResponse = await GetSearchResponse(query);                var hits = searchResponse.HitsMetadata.Hits;                var total = searchResponse.Total;                IReadOnlyCollection> res2 = searchResponse.Documents;                if (total > 0)                {                    return res2.ToList()[0];                }
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;        ///         /// es通用查询类        ///         ///         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 , queryContainers.Add(queryContainerDescriptor => { return queryContainerDescriptor.DateRange(c => c.Field("timeStamp").LessThanOrEquals(end_current)); }); 和 queryContainers.Add(queryContainerDescriptor => { return queryContainerDescriptor.DateRange(c => c.Field("timeStamp").LessThanOrEquals(enNew)); }); 执行的请求 却不一样, end_current 的请求为: 2023-04-28T17:44:09.6630219+08:00, 而 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:00Console.WriteLine(DateTime.Now.ToLocalTime());
如是字符串 DateTime.Parse("2023-04-28 15:00:00").ToLocalTime(), 值为  2023-04-28 23:00:00   (比2023-04-28 15:00:00 多 8 个小时)那么回到题头部分, 当用户输入
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 那么修改一下参数的取值
1   public DateTime QueryStartTime 2         { 3             get 4             { 5                 DateTime dt = DateTime.Now.AddMinutes(-15); 6                 if (!string.IsNullOrEmpty(StartTime) && StartTime.Trim() != "") 7                 { 8                     DateTime p; 9                     DateTime.TryParse(StartTime.Trim(), out p);10                     if (p.Year >= 2020)11                     {12                         dt = p;13                     }14                 }15                 return dt.ToUniversalTime();16             }17         }18 19         public DateTime QueryEndTime20         {21             get22             {23 24                 DateTime dt = DateTime.Now;25                 if (!string.IsNullOrEmpty(EndTime) && EndTime.Trim() != "")26                 {27                     DateTime p;28                     DateTime.TryParse(EndTime.Trim(), out p);29                     if (p.Year >= 2020)30                     {31                         dt = p;32                     }33                 }34                 return dt.ToUniversalTime();35             }36         }
好了, 现在问题解决了!!!
==>由此 推测
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();        }

标签:

X 关闭