.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 都不传值 , 请求的 参数为
如果 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: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();
}
.net 6 使用 NEST 查询,时间字段传值踩坑的更多相关文章
- Oracle查询时间字段并排序
select * from geimstatus_history twhere to_date(t.data_time,'YYYY-mm-dd') = to_date(sysdate,'YYYY-mm ...
- springboot传值踩坑
由于我现在写的项目都是前后端分离的,前端用的是vue,后端springboot,于是前后端传值的问题就是一个比较重要的问题,为此我还特意去学了一下vue的传值,其实就是用一个axios组件,其实就是基 ...
- Sql查询今天、本周和本月的记录(时间字段为时间戳)
工作中遇到的问题,小结一下 查询今日添加的记录: select * from [表名] where datediff(day,CONVERT(VARCHAR(20),DATEADD(SECOND,[时 ...
- MySQL 查询数据表里面时间字段为今天添加的计数
一: 下面这条语句查出来的count值 . 查询类型ID(category_id)为18的,今天插入的数据数, created_on: 为数据表中一字段 datetime类型, 记录此条数据添加的时 ...
- Oracle:时间字段模糊查询
需要查询某一天的数据,但是库里面存的是下图date类型 将Oracle中时间字段转化成字符串,然后进行字符串模糊查询 select * from CAINIAO_MONITOR_MSG t WHERE ...
- oracle 根据时间字段查询
oracle 根据时间字段查询数据 ROWNUM 是对前面查询的记录做限制,比如查询的记录 > 2000 条,那么只取前面的 2000 条 ''' SELECT * FROM (SELECT C ...
- MySQL 查询大于“时间字段”15分钟、1小时、1天的数据
以下代码中times为时间字段,类型为datetime 1.查询大于times十五分钟的数据 //大于号后面都是获取times十五分钟后的时间select*from table where now() ...
- Django聚合查询 orm字段及属性
目录 一 聚合查询 1. 级联 级联删除 级联更新 2. 聚合函数 使用 aggregate 使用场景 3. 分组查询 语法 使用 annotate 代码 4. F与Q查询 F查询 Q查询 二 ORM ...
- 1128 聚合查询 orm字段及属性
目录 一 聚合查询 1. 级联 级联删除 级联更新 2. 聚合函数 使用 aggregate 使用场景 3. 分组查询 语法 使用 annotate 代码 4. F与Q查询 F查询 Q查询 二 ORM ...
- orm中的聚合函数,分组,F/Q查询,字段类,事务
目录 一.聚合函数 1. 基础语法 2. Max Min Sum Avg Count用法 (1) Max()/Min() (2)Avg() (3)Count() (4)聚合函数联用 二.分组查询 1. ...
随机推荐
- jieba初
url: https://github.com/fxsjy/jieba/blob/master/ jieba "结巴"中文分词:做最好的 Python 中文分词组件 "J ...
- 持续集成环境(5)-Maven安装和配置
在Jenkins集成服务器上,我们需要安装Maven来编译和打包项目. 安装Maven 1.下载Maven软件到jenkins服务器上 wget https://mirrors.aliyun.com/ ...
- Cloneable的使用
Cloneable的使用 在开发过程中,拷贝实例是常见的一种操作,如果一个类中的字段较多,而我们又采用在客户端中逐字段复制的方 法进行拷贝操作的话,将不可避免的造成客户端代码繁杂冗长,而且也无法对类中 ...
- 排序方法-c语言
在接触过得排序算法中中,较为常见的有冒泡排序.选择排序.归并排序.快速排序法,他们的区别在于稳定性.时间复杂度.空间复杂度等: 现简单复习一下冒泡排序: 思路非常简单,逐个比较相邻的两个元素,前一个元 ...
- C#软件增加混淆防止反编译
使用Visual Studio新建一个名为"test"的命令行项目输入如图所示的代码并生成项目,这个项目很简单,就是程序启动时,调用"GetGUID"函数返回一 ...
- appium 怎么就一直连接不上设备呢
- spring-service.xml
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.spr ...
- CSS pointer-events 属性
pointer-events 属性用于设置元素是否对鼠标事件做出反应. CSS 语法 pointer-events: auto|none; 属性值 属性值 描述 auto 默认值,设置该属性链接可以正 ...
- Flink模式
Per-job Cluster 该模式下,一个作业一个集群,作业之间相互隔离. 在Per-Job模式下,集群管理器框架用于为每个提交的Job启动一个 Flink 集群.Job完成后,集群将关闭,所有残 ...
- Python学习笔记--SQL数据
SQL 本人受到Java的影响,数据库的话,就不按照教程走了,我就直接使用的是Navicat软件的数据库啦! SQL支持注释: 两种单行注释(-- 和# ),和一种多行注释(/* */) 基础的使用语 ...