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

0x01业务描述

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

然后搭建了 Kibanal  对 Elasticsearch  进行查询.  但是项目组开发人员众多,不是每个人都想要学会如何在 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:00queryContainerDescriptor.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 QueryStartTime2         {3             get4             {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;  // 结果为 trueConsole.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();}  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/44600.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用 HTML、CSS 和 JavaScript 创建实时 Web 编辑器

使用 HTML、CSS 和 JavaScript 创建实时 Web 编辑器 在本文中&#xff0c;我们将创建一个实时网页编辑器。这是一个 Web 应用程序&#xff0c;允许我们在网页上编写 HTML、CSS 和 JavaScript 代码并实时查看结果。这是学习 Web 开发和测试代码片段的绝佳工具。我们将使用ifram…

Redis 工作总结

1.Redis是什么 Redis是互联网技术领域使用最为广泛的存储中间件&#xff0c;它是Remote Dictionary Service的首字母缩写&#xff0c;也就是远程字典服务。 2.Redis的用途&#xff1f; 2.1 计数器 2.2 缓存 2.3 分布式锁 2.4 消息中间件 3.Redis的数据类型 3.1 string&am…

如何找到一个数的所有质因数,以及如何快速判断一个数是不是质数

前情介绍 今天遇到一个需求&#xff1a;找到一个数所有的质因数。 初步解决 先定义一个判断质数的函数&#xff1a; def is_Prime(number):i 2count 0while i < number:if number % i 0 :count 1i 1if count > 0:return Falseelse:return True 接着定义一个寻找质…

搜索引擎 笔记

目录 1. 搜索引擎 笔记1.1. Google 为什么成功1.2. 做搜索引擎, 既复杂, 又简单 1. 搜索引擎 笔记 1.1. Google 为什么成功 科技行业一直有个原则, 即人们不会愿意改变自己的使用习惯。Ramaswamy 在采访中坦言, “我们面临的最大障碍之一, 确实就是扭转用户的固有习惯。人们忘…

Vue 2 Mixin 混入

混入&#xff08;Mixins&#xff09;是一种在Vue组件中重用代码的方式。它允许你定义一些可复用的选项对象&#xff0c;然后将这些选项合并到不同的组件中。混入可以用于在多个组件之间共享逻辑、方法、生命周期钩子等。 示例&#xff1a; <!DOCTYPE html> <html>…

【云原生】【k8s】Kubernetes+EFK构建日志分析安装部署

目录 EFK安装部署 一、环境准备&#xff08;所有主机&#xff09; 1、主机初始化配置 2、配置主机名并绑定hosts&#xff0c;不同主机名称不同 3、主机配置初始化 4、部署docker环境 二、部署kubernetes集群 1、组件介绍 2、配置阿里云yum源 3、安装kubelet kubeadm …

模型数据处理-数据放入 session和@ModelAttribute 实现 prepare 方法详细讲解

&#x1f600;前言 本文详细讲解了模型数据处理-数据放入 session和ModelAttribute 实现 prepare 方法详细讲解 &#x1f3e0;个人主页&#xff1a;尘觉主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是尘觉&#xff0c;希望我的文章可以帮助到大家&#xff0c…

LeetCode //C - 56. Merge Intervals

56. Merge Intervals Given an array of intervals where intervals[i] [ s t a r t i , e n d i ] [start_i, end_i] [starti​,endi​], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the in…

企业数字化转型大数据湖一体化平台项目建设方案PPT

导读&#xff1a;原文《企业数字化转型大数据湖一体化平台项目建设方案PPT》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 喜欢文章&#xff0c;您可以点赞评论转发…

知识体系总结(九)设计原则、设计模式、分布式、高性能、高可用

文章目录 架构设计为什么要进行技术框架的设计 六大设计原则一、单一职责原则二、开闭原则三、依赖倒置原则四、接口分离原则五、迪米特法则&#xff08;又称最小知道原则&#xff09;六、里氏替换原则案例诠释 常见设计模式构造型单例模式工厂模式简单工厂工厂方法 生成器模式…

【精通性能优化:解锁JMH微基准测试】一基本用法

文章目录 1. 什么是JMH1.1 用JMH进行微基准测试1. JmhExample01.java2. 程序输出JmhExample01.java 2.2 JMH的基本用法2.1 Benchmark标记基准测试方法2.2 Warmup以及Measurement1. 设置全局的Warmup和Measurement&#xff08;一&#xff09;2. 设置全局的Warmup和Measurement&a…

设计模式篇---抽象工厂(包含优化)

文章目录 概念结构实例优化 概念 抽象工厂&#xff1a;提供一个创建一系列相关或相互依赖对象的接口&#xff0c;而无须指定它们具体的类。 工厂方法是有一个类型的产品&#xff0c;也就是只有一个产品的抽象类或接口&#xff0c;而抽象工厂相对于工厂方法来说&#xff0c;是有…

Android Studio实现解析HTML获取图片URL,将URL存到list,进行列表展示

目录 效果build.gradle(app)添加的依赖(用不上的可以不加)AndroidManifest.xml错误代码activity_main.xmlitem_image.xmlMainActivityImage适配器ImageModel 接收图片URL效果 build.gradle(app)添加的依赖(用不上的可以不加) dependencies {implementation com.square…

Python可视化在量化交易中的应用(11)_Seaborn折线图

举个栗子&#xff0c;用seaborn绘制折线图。 Seaborn中折线图的绘制方法 在seaborn中&#xff0c;我们一般使用sns作为seaborn模块的别名&#xff0c;因此&#xff0c;在下文中&#xff0c;均以sns指代seaborn模块。 seaborn中绘制折线图使用的是sns.lineplot()函数&#xff…

springboot+grpc+k8s+istio环境

2023年8月17日&#xff0c;本人之前使用过nacosdubbospringboot、eurekafeign等环境。最近学习到了istio服务网格集成到k8s也可以实现分布式微服务。 1. 环境 Kubernetes集群istio集成到k8sjdk17 (8也ok)gPRC服务间通信 2. 微服务 cloud-config服务是spring-cloud-config-s…

中国剩余定理及扩展

目录 中国剩余定理解释 中国剩余定理扩展——求解模数不互质情况下的线性方程组&#xff1a; 代码实现&#xff1a; 互质&#xff1a; 非互质&#xff1a; 中国剩余定理解释 在《孙子算经》中有这样一个问题&#xff1a;“今有物不知其数&#xff0c;三三数之剩二&#x…

【C++】做一个飞机空战小游戏(十)——子弹击落炮弹、炮弹与飞机相撞

[导读]本系列博文内容链接如下&#xff1a; 【C】做一个飞机空战小游戏(一)——使用getch()函数获得键盘码值 【C】做一个飞机空战小游戏(二)——利用getch()函数实现键盘控制单个字符移动【C】做一个飞机空战小游戏(三)——getch()函数控制任意造型飞机图标移动 【C】做一个飞…

leetcode做题笔记88. 合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 nums1 中&#xff0c;使合并后的数组同样按 非递减顺序 排列。 注意&#xff1a;最终&#xff0c;合并后数组…

Mybatis的SqlSource SqlNode BoundSql

学习链接 MyBatis SqlSource解析 【Mybatis】Mybatis源码之SqlSource#getBoundSql获取预编译SQL Mybatis中SqlSource解析流程详解 Mybatis TypeHandler解析 图解 Mybatis的SqlSource&SqlNode - processon DynamicSqlSource public class DynamicSqlSource implement…

notepad++ verilog关键字自动补全

新建verilog.xml放在安装目录下 D:\Program Files (x86)\Notepad\autoCompletion <?xml version"1.0" encoding"Windows-1252" ?> <NotepadPlus><AutoComplete><KeyWord name"accept_on" /><KeyWord name"a…