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 ,
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 查询,时间字段传值踩坑的更多相关文章

  1. Oracle查询时间字段并排序

    select * from geimstatus_history twhere to_date(t.data_time,'YYYY-mm-dd') = to_date(sysdate,'YYYY-mm ...

  2. springboot传值踩坑

    由于我现在写的项目都是前后端分离的,前端用的是vue,后端springboot,于是前后端传值的问题就是一个比较重要的问题,为此我还特意去学了一下vue的传值,其实就是用一个axios组件,其实就是基 ...

  3. Sql查询今天、本周和本月的记录(时间字段为时间戳)

    工作中遇到的问题,小结一下 查询今日添加的记录: select * from [表名] where datediff(day,CONVERT(VARCHAR(20),DATEADD(SECOND,[时 ...

  4. MySQL 查询数据表里面时间字段为今天添加的计数

    一: 下面这条语句查出来的count值 .  查询类型ID(category_id)为18的,今天插入的数据数, created_on: 为数据表中一字段 datetime类型, 记录此条数据添加的时 ...

  5. Oracle:时间字段模糊查询

    需要查询某一天的数据,但是库里面存的是下图date类型 将Oracle中时间字段转化成字符串,然后进行字符串模糊查询 select * from CAINIAO_MONITOR_MSG t WHERE ...

  6. oracle 根据时间字段查询

    oracle 根据时间字段查询数据 ROWNUM 是对前面查询的记录做限制,比如查询的记录 > 2000 条,那么只取前面的 2000 条 ''' SELECT * FROM (SELECT C ...

  7. MySQL 查询大于“时间字段”15分钟、1小时、1天的数据

    以下代码中times为时间字段,类型为datetime 1.查询大于times十五分钟的数据 //大于号后面都是获取times十五分钟后的时间select*from table where now() ...

  8. Django聚合查询 orm字段及属性

    目录 一 聚合查询 1. 级联 级联删除 级联更新 2. 聚合函数 使用 aggregate 使用场景 3. 分组查询 语法 使用 annotate 代码 4. F与Q查询 F查询 Q查询 二 ORM ...

  9. 1128 聚合查询 orm字段及属性

    目录 一 聚合查询 1. 级联 级联删除 级联更新 2. 聚合函数 使用 aggregate 使用场景 3. 分组查询 语法 使用 annotate 代码 4. F与Q查询 F查询 Q查询 二 ORM ...

  10. orm中的聚合函数,分组,F/Q查询,字段类,事务

    目录 一.聚合函数 1. 基础语法 2. Max Min Sum Avg Count用法 (1) Max()/Min() (2)Avg() (3)Count() (4)聚合函数联用 二.分组查询 1. ...

随机推荐

  1. Vue 使用插件nprogress页面加载进度条

    下载 npm i nprogress 在main.js中引入: import App from './App' import VueRouter from 'vue-router' import ro ...

  2. Shell脚本实现模拟并发及并发数控制

    #!/bin/bash #by inmoonlight@163.com #下面的代码控制并发数.其实是利用令牌原理实现 #一个线程要运行,首先要拿到令牌在该代码中即read一行数据,读取不到就会暂停, ...

  3. linux下文件重命名

    Ubuntu下执行上面举例的重命名时,命令是这样的:rename 's/a/xxx/g' *.txt

  4. 九九乘法表打印记一次al面试

    for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { System.out.print(i + "x&quo ...

  5. 配置tlpi_hdr.h 头文件《linux系统编程》(转载)

    https://www.cnblogs.com/pluse/p/6296992.html#:~:text=tlpi_hdr.h%E6%96%87%E4%BB%B6%E5%88%99%E5%8C%85% ...

  6. Spring-传统方式(XML)创建webapp

    如何搭建一个传统的webapp项目[Java后端] 使用xml 来搭建 SSM 环境,要求 Tomcat 的版本必须在 7 以上 QuickStart 1创建工程 创建一个新模块[普通的 Maven ...

  7. MAC范洪攻击-macof

    macof 目的:攻击交换机的路由表,实现网络信息嗅探 macof是dsniff中的一个小工具 概要:交换机中存在着一个记录着MAC地址的表,为了完成数据的快速转发,这个表有着自动学习机制,学习后可以 ...

  8. CF916E 解题报告

    被这道题搞了一个晚上,还好搞出来了qwq 令人耳目一新的阅读体验 题目简述 翻译已经很简单了. 前置知识 DFS序,LCA,线段树,不需要标签中的树剖! DFS序更新信息及判断祖先 如果你还不知道DF ...

  9. Web 开发的常规流程

    Web 开发的常规流程 What is the Web? 简单地说,网络是一个遍布全球的网络,它连接大量设备并允许它们相互通信 Internet 上的网站托管在称为服务器的设备上,当您与 Intern ...

  10. 提交docker镜像

    docker commit -m="提交的描述信息" -a="作者" 容器id 目标镜像名:[TAG]