.net 6 使用 NEST 查询,时间字段传值踩坑

0x01业务描述

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

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

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

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

0x02主要实现代码

日志索引为: xxxapilog_*

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

复制代码
 1         /// <summary>
 2         /// 根据查询条件,封装请求
 3         /// </summary>
 4         /// <param name="query"></param>
 5         /// <returns></returns>
 6         public async Task<ISearchResponse<Dictionary<string, object>>> GetSearchResponse(API_Query query)
 7         {
 8             int size = query.PageSize;
 9             int from = (query.PageIndex - 1) * size;
10             ISearchResponse<Dictionary<string, object>> searchResponse1 = await elasticClient.SearchAsync<Dictionary<string, object>>(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<Func<QueryContainerDescriptor<Dictionary<string, object>>, QueryContainer>> queryContainers = new List<Func<QueryContainerDescriptor<Dictionary<string, object>>, 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         }

接口参数类:

复制代码
    /// <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; }
    }

调用方式:

复制代码
 API_Query query = new API_Query () { PageIndex=1, PageSize=10 };

 ISearchResponse<Dictionary<string, object>> searchResponse = await GetSearchResponse(query);

                var hits = searchResponse.HitsMetadata.Hits;
                var total = searchResponse.Total;
                IReadOnlyCollection<Dictionary<string, object>> 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;
        /// <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: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 QueryEndTime
20         {
21             get
22             {
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();
        }  
相关推荐
腾视科技-AI6 小时前
腾视科技重磅发布AD03行车记录仪DashCam!全维守护,智驭出行新生态
人工智能·科技·ai·行车记录仪·车载智能终端·腾视科技·ai算力盒
wp123_16 小时前
IP67 防水 3.5mm 耳机座解析:CUI SJ‑3541AS‑MT‑TR‑67 与 TONEVEE TY30002‑06A 特性与应用
科技·硬件架构
2401_890095616 小时前
武汉自动意志科技有限公司:人工智能应用软件开发:交付成本与效率怎么平衡:先看清这四条边界
科技·ai智能体·武汉自动意志科技有限公司·智钳claw·外呼机器人·企业ai部署
诗句藏于尽头6 小时前
deepseek harness对接商汤科技免费大模型使用教程
人工智能·科技·学习
极新8 小时前
产品做好固然重要,但海外市场还看品牌故事
人工智能·科技
ws2019078 小时前
产业链协同创新,2026广州汽车零部件展赋能智造未来
人工智能·科技·汽车
蓝速科技10 小时前
蓝速科技智慧讲台多系统兼容方案与场景落地指南
科技
GIS数据转换器11 小时前
村镇无人机物流配送与跨域监测一体化平台
大数据·运维·人工智能·科技·无人机
港股研究社1 天前
重构算力资产想象空间,被MiniMax选中的晋景新能打响升级战
科技·重构
Axis tech1 天前
无标记点动作捕捉 vs 惯性动作捕捉:专业从业者需要了解这些要点
科技·机器人