前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >.NET6 使用 NEST 查询Elasticsearch,时间字段传值踩坑

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

作者头像
郑子铭
发布2023-08-30 09:39:55
2250
发布2023-08-30 09:39:55
举报

0x01业务描述

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

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

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

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

0x02主要实现代码

日志索引为: xxxapilog_*

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

代码语言:javascript
复制
/// <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;
        }

接口参数类:

代码语言:javascript
复制
/// <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 时间字段预处理

代码语言:javascript
复制
PS: 如果  StartTime 和 EndTime  都不传值, 那么 默认设置 只查最近的 15分钟 

封装一下 QueryStartTime  和 QueryEndTime 
代码语言:javascript
复制
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 查找问题原因

代码语言:javascript
复制
以上 封装,经过测试, 能够获取到查询数据. 但是,但是 ,但是 坑爹的来了,当 外面传入参数 
代码语言:javascript
复制
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 请求的实际语句
代码语言:javascript
复制
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;
        }
代码语言:javascript
复制

请求分析: 
代码语言:javascript
复制
如果  StartTime 和 EndTime  都不传值 , 请求的 参数为 
代码语言:javascript
复制
{
    "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"
            }
        }
    ]
}
代码语言:javascript
复制
如果  StartTime 和 EndTime  传入 2023-04-28 和 2023-04-28 15:00:00, 请求的 参数为 
代码语言:javascript
复制
{
    "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"
            }
        }
    ]
}
代码语言:javascript
复制
对比后发现 , 时间传值有2种不同的格式 
代码语言:javascript
复制
"@timestamp": { "gte": "2023-04-28T17:44:09.6630219+08:00" }
"@timestamp": {"gte": "2023-04-28T00:00:00" }
代码语言:javascript
复制

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

0x05 测试求证

我做了个测试

代码语言:javascript
复制
//不传参数, 默认结束时间为当前时间
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

进一步测试

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

那么回到题头部分, 当用户输入
代码语言:javascript
复制
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 解决方案

代码语言:javascript
复制


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

那么修改一下参数的取值
代码语言:javascript
复制
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();
            }
        }
代码语言:javascript
复制
好了, 现在问题解决了!!!
代码语言:javascript
复制
==>由此 推测
代码语言:javascript
复制
 return queryContainerDescriptor.DateRange(c => c.Field("timeStamp").GreaterThanOrEquals(DateMath from));
代码语言:javascript
复制
DateMath from 使用了 ToLocalTime .

0x07 简单测试用例

代码语言:javascript
复制

这里贴上简要的测试用例,方便重现问题.
代码语言:javascript
复制
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();
        }
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-07-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x01业务描述
  • 0x02主要实现代码
  • 0x03 时间字段预处理
  • 0x04 查找问题原因
  • 0x05 测试求证
  • 0x06 解决方案
  • 0x07 简单测试用例
相关产品与服务
腾讯云服务器利旧
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档