Nightingale V2 批量查询接口的 Elasticsearch KQL 扩展:语法、编译原理与实战指南
2026/9/15 17:11:26 网站建设 项目流程

Nightingale V2 批量查询接口的 Elasticsearch KQL 扩展:语法、编译原理与实战指南

【免费下载链接】nightingaleNightingale is to monitoring and alerting what Grafana is to visualization.项目地址: https://gitcode.com/GitHub_Trending/ni/nightingale

本文围绕 Nightingale 开源仓库中的 doc/api/query-batch-v2-kql.md 展开,完整讲解 V2 批量查询接口中 Elasticsearch 数据源的 KQL(Kibana Query Language)过滤能力:如何在请求中启用 KQL、KQL 被编译为标准 Query DSL 的底层原理、已支持的语法全集、词法细节与当前限制,并结合 datasource/commons/eslike/kql.go 等源码与测试用例进行纵深验证。读完本文,你将能够在一次POST /api/n9e/v2/query-batch请求中,用与前端 Kibana 一致的 KQL 文法精确检索 Elasticsearch/OpenSearch 日志,并理解其行为边界。

一、背景:V2 批量查询接口中的 Elasticsearch 查询

Nightingale 的 V2 批量查询接口POST /api/n9e/v2/query-batch(商业版为/api/n9e-plus/v2/query-batch)支持在一次请求中查询多个数据源,并用表达式组合时序结果,详见 doc/api/query-batch-v2.md。接口按请求中datasource.catedatasource.id从已初始化的数据源缓存取得插件并执行,因此 Elasticsearch、OpenSearch 等 cate 天然支持logs/time_series两种结果类型。

在 Elasticsearch 数据源的 query 对象中,原本只支持 Lucene 风格的filter表达式(query_string查询)。KQL 扩展为同一个输入框提供了第二种语法:传filter_language: "kql"即启用,未传或传lucene则保持旧行为。这一设计保证了「同一面板、同一过滤输入框,两种语法随意切换」,与前端 Kibana 的体验对齐。

从 center/router/router_query_batch_v2.go 可以看到,V2 对result_type=logs的请求调用plug.QueryLog,对时序请求调用plug.QueryData;而 Elasticsearch 插件的这两个方法最终都汇聚到eslike包(见 datasource/es/es.go)。也就是说,KQL 支持是在 Elasticsearch/OpenSearch 共用的eslike查询层实现的,两个数据源 cate 一并受益。

二、使用方式:在 query 对象中启用 KQL

datasource.cate = "elasticsearch"(或 OpenSearch)的 query 对象中传入以下三个字段即可:

{ "filter_language": "kql", "filter": "service.name: api AND log.level: \"ERROR\" AND http.response.status_code >= 500", "kql_options": { "case_insensitive": false, "time_zone": "Asia/Shanghai" } }

新增字段说明如下:

字段类型必填说明
filter_languagestringkql启用 KQL;未传或lucene保持旧的 Lucene 行为
filterstringKQL 表达式;留空与 Lucene 一致,表示只按时间范围查全部
kql_options.default_fieldstring已忽略,仅为请求兼容保留;裸词按前端行为查询全字段
kql_options.case_insensitiveboolean已忽略,仅为请求兼容保留;值通配生成query_string,大小写行为由字段的分析器决定
kql_options.time_zonestring仅对本请求date_field的范围条件生效;用于日期文字解释时区,如Asia/Shanghai+08:00

几点需要特别注意的语义:

  • 顶层 V2 的from/to仍是权威时间范围。KQL 中出现的任何时间条件(如@timestamp < now-2d)不会取代顶层时间范围,而是与其共同作为 filter 生效,最终全部合并进bool.filter
  • filter留空(或纯空白)时,KQL 与 Lucene 行为一致:只按时间范围查全部,而不是报错。这一点在 datasource/commons/eslike/kql.go 中有明确注释:切换语法但尚未填写条件的面板不应直接失败。
  • 传入filter_language之外的值(如sql)会返回unsupported filter_language错误。

从源码看,KQLOptions结构体定义了这三个选项的 JSON/mapstructure 标签,DefaultFieldCaseInsensitive被注释为「accepted for request compatibility and ignored」,TimeZone则在编译期被真正使用(见 datasource/commons/eslike/kql.go)。

三、编译方式:KQL → AST → 标准 Query DSL

KQL 并不会被直接转交给 Lucene 的query_string。整个编译链路是:

KQL 文本 │ 词法分析(kqlParser.next) ▼ AST(kqlNode:and / or / not / is / range / nested) │ 前导通配校验(kqlValidateLeadingWildcards) ▼ 标准 Elasticsearch Query DSL(kqlToDSL) │ 与 V2 时间范围合并 ▼ bool.filter 查询体

关键入口是 datasource/commons/eslike/kql.go 的CompileKQL:先以 rune 为单位做词法切分,递归下降解析出表达式树,确认 token 流已消费完毕(kqlEOF),再做前导通配符校验,最后调用kqlToDSL产出 DSL。GetFilterQuery则负责将编译产物与时间范围elastic.RangeQuery拼进同一个bool.filter(datasource/commons/eslike/kql.go)。

这样设计的目的很明确:避免 KQL 与 Lucene 在范围比较、字段存在、转义和布尔语义上的差异。项目支持 ES 7.10 及以上版本(从 datasource/commons/eslike/eslike.go 中按 ES 大/小版本选择fixed_intervalinterval的逻辑可见版本兼容策略),因此不能把仅较新 ES 才提供的原生kqlQuery DSL 作为通用依赖 —— 后端自行编译是保证跨版本可用的稳妥路径。

一个值得注意的实现细节:编译器内部使用哨兵字符串@kuery-wildcard@携带未转义的*(见 datasource/commons/eslike/kql.go),在生成 DSL 或报错时再替换回*,避免通配符在词法/语法处理中被误转义或丢失。对应的回归测试在 datasource/commons/eslike/kql_test.go(TestCompileKQLWildcardMarkerLiteralFieldDoesNotBecomeWildcard)与错误文本还原测试(TestCompileKQLErrorsQuoteTheOriginalText)中均有覆盖。

四、已支持语法全集

下表完整列出 KQL 编译器支持的语法形态及其生成的 DSL:

KQL生成的 DSL示例
字段匹配matchstatus: 200message: timeout error
精确短语match_phrasemessage: "timeout error"
字段存在existstrace.id: *
值通配query_stringservice: api*
字段名通配与前端默认模式相同的字段名 DSLdatastream.*: logs
范围比较rangebytes >= 1024@timestamp < now-2d
布尔运算boola: 1 AND b: 2NOT status: 200
括号与同字段多值bool.shouldstatus: (200 OR 201)
nested 作用域nesteduser:{ first: Alice AND last: White }
全量匹配match_all*:*

从 datasource/commons/eslike/kql.go 的kqlIsDSL可以看出每种形态的精确映射规则:

  • 字段匹配(未加引号的单值)→match;双引号值 →match_phrase
  • 值为单独*exists(字段存在性检测);
  • 值带通配符 → 该字段上的query_string
  • 裸词(无字段名)→multi_match,未加引号用best_fields,加引号用phrase,且带lenient: true
  • 字段名与值均为单独*(即*:**: (*))→match_all
  • OR分支生成bool.should并带minimum_should_match: 1AND生成bool.filterNOT生成bool.must_not
  • nested 作用域生成nested查询并带score_mode: "none",子路径逐层拼接(user.names.first这种多级嵌套同样支持)。

这些映射在 datasource/commons/eslike/kql_test.go(TestCompileKQLFrontendCompatibility)中以「输入 KQL → 期望 DSL」的表格形式逐条锁定,是整个兼容性契约的测试证据。

五、词法细节与转义规则

KQL 的语法细节决定了它能表达什么、不能表达什么,本小节逐条展开:

  • 布尔关键字不区分大小写ANDORNOT大小写均可(如a: 1 or b: 2),但字段条件之间必须显式使用ANDORstatus: 200 level: ERROR这种「空格隐含 AND」的写法会直接编译失败(datasource/commons/eslike/kql_test.go 中被列入 rejected 列表)。这是与前端文法一致的刻意取舍。
  • 括号与多词值:可使用括号明确优先级;字段后的括号可以容纳多词值,例如message: (timeout error)整体作为值匹配。括号内也可以继续放布尔逻辑:message: (timeout AND error)message: (NOT timeout)都是合法形态。
  • 未加引号的多词值:空格是值的一部分,其中可以带通配符。例如message: foo bar*会整体作为一个通配值下发为query_string,而不是拆成两个词。其词法依据在parseLiteralTail(datasource/commons/eslike/kql.go):只有单独一个引号字符串才独立成词。
  • 双引号内的*是普通字符message: "foo*"会按短语匹配(match_phrase),不会展开通配。
  • 未加引号的/~^[]是普通值内容:例如message: /timeout.*/会作为query_string值下发(测试中可见其被转义为\/timeout.*\/),message: foo~2message: foo^2message: [one TO two]等同样按字面值处理,不做 Lucene 特殊语法解释。
  • 反斜杠转义:支持\t\r\n\uXXXX(Unicode 转义,4 位十六进制)以及任意普通字符的\x转义。例如http.request.referrer: https\://example.com实际匹配https://example.commessage: \u4e2d匹配
  • 类型化字面量:未加引号的值除true/false/null外一律按字符串下发(kqlLiteralValue,见 datasource/commons/eslike/kql.go)。因此bytes >= 1024生成{"gte": "1024"}(字符串形式),数值与日期(含 epoch 毫秒)字段由 Elasticsearch 按 mapping 解析。field: truefield: null则分别生成布尔truenull值。
  • 通配符逃逸:内部哨兵@kuery-wildcard@是「恰好包含该子串的字面量会被还原为通配符」的唯一例外,实际内容中出现该字符串的概率极低,测试TestCompileKQLWildcardMarkerLiteralFieldDoesNotBecomeWildcard也验证了字段名中出现该字面量不会被误判。

六、mapping 无关的编译策略

当前编译器不读取 Elasticsearch mapping,严格复现前端默认转换器buildESQueryFromKuery的 no-mapping 分支,不会按text/keyword字段类型切换查询类型(如keywordtermtextmatch这类优化不会发生)。这意味着:

  • 裸值统一走match,值通配统一走query_string,行为完全由 ES 端的分析器决定;
  • 这正是kql_options.case_insensitive被忽略的原因 —— 无 mapping 分支本身不生成case_insensitive选项,大小写是否敏感取决于字段分析器;
  • kql_options.default_field被忽略同理 —— 前端文法对裸词总是查询全字段。

字段名通配(包括*: value*prefix: value)会按前端默认转换器原样传入 DSL,不会受值通配的前导*限制(测试TestCompileKQLFieldLeadingWildcardsMatchesFrontendDefault验证了*: foo*timeout: foo均可编译通过)。范围值中的通配符同样原样传给range(如bytes >= foo*bytes >= *foo),这可能因字段类型而被 Elasticsearch 拒绝或得到非预期结果,建议仅在确有兼容需求时使用。

七、当前限制

与前端导出函数allowLeadingWildcards=false的默认值一致,编译器对非单独*的前导通配默认返回INVALID_QUERY

  • message: *timeout*timeoutmessage: **均编译失败,错误信息包含Leading wildcards are disabled.
  • field: *(字段存在)与*:*(全量匹配)不受此限制
  • 校验只发生在「值(is)转换」这一处(见kqlValidateLeadingWildcards的注释,datasource/commons/eslike/kql.go),字段名与范围值保留前导通配。

此外,KQL 的括号、连续NOT与 nested 嵌套最多 64 层(常量kqlMaxNestingDepth = 64),超出时报KQL nesting exceeds maximum depth 64。测试TestCompileKQLNestingDepthLimit用 65 层括号、65 层NOT、65 层a:{嵌套分别验证了上限,并用 63 层验证了「上限之内正常通过」。

八、完整请求示例

下面是一次完整的 V2 批量查询请求:在 1 小时时间窗内,从logs-*索引族检索service.name=apilog.level=ERROR、HTTP 状态码 ≥ 500 的日志,按@timestamp倒序取前 100 条:

{ "from": 1784971200, "to": 1784974800, "queries": [ { "kind": "query", "ref_id": "ES_LOGS", "datasource": {"cate": "elasticsearch", "id": 7}, "result_type": "logs", "query": { "index_type": "index", "index": "logs-*", "date_field": "@timestamp", "limit": 100, "ascending": false, "filter_language": "kql", "filter": "service.name: api AND log.level: ERROR AND http.response.status_code >= 500", "kql_options": {"time_zone": "Asia/Shanghai"} } } ] }

要点回顾:

  • filterhttp.response.status_code >= 500是数值范围比较,编译为range查询;因为目标字段不是date_fieldtime_zone不会注入该 range;
  • 若把时间条件(如@timestamp >= "2026-07-29T00:00:00")写入 KQL,且该字段等于date_field(此处为@timestamp),time_zone才会生效 —— 测试TestCompileKQLTimeZoneOnlyForDateField精确验证了「数值 range 不带 time_zone、日期 range 才带」的规则;
  • time_zone支持Asia/Shanghai这类 IANA 名称,也支持+08:00这类偏移写法。

九、查询成功后的响应行为

KQL 查询成功后的日志记录遵循 V2 通用响应信封(HTTP 200、dat.results[]按请求顺序返回,单项失败不影响其他查询)。针对 Elasticsearch/OpenSearch 的 DSL 分支,有两点与 KQL 直接相关的行为需要了解:

  1. 命中文档的_source会直接成为records[].fieldsresult_type=logs时,每条记录的字段就是文档原始_source内容;
  2. 不会返回_id_indexsort:V2 对日志结果做了裁剪,避免把 Elasticsearch 内部元数据泄漏给下游。

底层实现在 center/router/router_query_batch_v2.go:plug.QueryLog返回的 SearchHit 列表经queryBatchV2Records转换后写入Records;DSL 路径与 XPack SQL 路径在此处做了区分(queryBatchV2ElasticsearchSQLPayload)。在 datasource/commons/eslike/eslike.go 的QueryLog中可以看到 ES 6 与 7+ 版本在_source扁平化处理上的差异,V2 在 7+ 路径直接透传 hit。

十、源码导读与可继续深入的入口

如果你想继续深挖本功能的实现,推荐按以下路径阅读:

  • datasource/commons/eslike/kql.go:KQL 编译器全量实现 —— 词法(next/readQuoted/readAtom/readEscape)、语法(parseOr/parseAnd/parseUnary/parsePrimary/parseFieldValue/parseLiteralTail)、DSL 生成(kqlToDSL/kqlIsDSL/kqlRangeKey)、转义器(kqlLuceneEscaper,与前端escapeQueryString转义同一字符类)与入口CompileKQL/GetFilterQuery
  • datasource/commons/eslike/kql_test.go:兼容性契约测试,覆盖语法矩阵、前导通配、范围操作符、时区、嵌套深度、错误文本等全部关键行为;
  • datasource/commons/eslike/eslike.goQuery参数结构(FilterLanguageKQLOptions字段的 JSON 标签)、QueryData/QueryLog中调用GetFilterQuery并拼入时间范围的主流程;
  • datasource/es/es.go:Elasticsearch 插件如何把 V2 的 payload 委托给eslikeMakeLogQuery/MakeTSQuery/QueryData/QueryLog);
  • center/router/router_query_batch_v2.go:V2 执行器如何按result_type分发到QueryLog/QueryData并组装统一响应;
  • center/router/router_query_batch_v2_test.go:V2 接口级测试中直接出现的 KQL 请求示例("filter_language":"kql","filter":"message: timeout*");
  • doc/api/query-batch-v2.md:V2 通用请求/响应协议、表达式引用序列的方式、完整错误码表(INVALID_QUERYDATASOURCE_TIMEOUTEXPRESSION_*DEPENDENCY_*等)。

结语

Nightingale 的 KQL 支持不是简单地把 KQL 字符串透传给 Elasticsearch,而是在后端完整实现了与 Kibana 前端buildESQueryFromKuery默认行为对齐的词法/语法/DSL 编译链路,并以测试矩阵锁定兼容性。对使用者而言,这意味着同一套 KQL 文法在 Kibana 与 Nightingale 查询界面之间可以无缝迁移;对二次开发者而言,eslike/kql.go提供了一份结构清晰、测试完备的参考实现,可以在此基础上扩展新的语法形态(如更复杂的值类型)而不破坏既有契约。

【免费下载链接】nightingaleNightingale is to monitoring and alerting what Grafana is to visualization.项目地址: https://gitcode.com/GitHub_Trending/ni/nightingale

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询