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.cate与datasource.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_language | string | 否 | 传kql启用 KQL;未传或lucene保持旧的 Lucene 行为 |
filter | string | 否 | KQL 表达式;留空与 Lucene 一致,表示只按时间范围查全部 |
kql_options.default_field | string | 否 | 已忽略,仅为请求兼容保留;裸词按前端行为查询全字段 |
kql_options.case_insensitive | boolean | 否 | 已忽略,仅为请求兼容保留;值通配生成query_string,大小写行为由字段的分析器决定 |
kql_options.time_zone | string | 否 | 仅对本请求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 标签,DefaultField与CaseInsensitive被注释为「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_interval或interval的逻辑可见版本兼容策略),因此不能把仅较新 ES 才提供的原生kqlQuery DSL 作为通用依赖 —— 后端自行编译是保证跨版本可用的稳妥路径。
一个值得注意的实现细节:编译器内部使用哨兵字符串@kuery-wildcard@携带未转义的*(见 datasource/commons/eslike/kql.go),在生成 DSL 或报错时再替换回*,避免通配符在词法/语法处理中被误转义或丢失。对应的回归测试在 datasource/commons/eslike/kql_test.go(TestCompileKQLWildcardMarkerLiteralFieldDoesNotBecomeWildcard)与错误文本还原测试(TestCompileKQLErrorsQuoteTheOriginalText)中均有覆盖。
四、已支持语法全集
下表完整列出 KQL 编译器支持的语法形态及其生成的 DSL:
| KQL | 生成的 DSL | 示例 |
|---|---|---|
| 字段匹配 | match | status: 200、message: timeout error |
| 精确短语 | match_phrase | message: "timeout error" |
| 字段存在 | exists | trace.id: * |
| 值通配 | query_string | service: api* |
| 字段名通配 | 与前端默认模式相同的字段名 DSL | datastream.*: logs |
| 范围比较 | range | bytes >= 1024、@timestamp < now-2d |
| 布尔运算 | bool | a: 1 AND b: 2、NOT status: 200 |
| 括号与同字段多值 | bool.should | status: (200 OR 201) |
| nested 作用域 | nested | user:{ 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: 1,AND生成bool.filter,NOT生成bool.must_not;- nested 作用域生成
nested查询并带score_mode: "none",子路径逐层拼接(user.names.first这种多级嵌套同样支持)。
这些映射在 datasource/commons/eslike/kql_test.go(TestCompileKQLFrontendCompatibility)中以「输入 KQL → 期望 DSL」的表格形式逐条锁定,是整个兼容性契约的测试证据。
五、词法细节与转义规则
KQL 的语法细节决定了它能表达什么、不能表达什么,本小节逐条展开:
- 布尔关键字不区分大小写:
AND、OR、NOT大小写均可(如a: 1 or b: 2),但字段条件之间必须显式使用AND或OR。status: 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~2、message: foo^2、message: [one TO two]等同样按字面值处理,不做 Lucene 特殊语法解释。 - 反斜杠转义:支持
\t、\r、\n、\uXXXX(Unicode 转义,4 位十六进制)以及任意普通字符的\x转义。例如http.request.referrer: https\://example.com实际匹配https://example.com,message: \u4e2d匹配中。 - 类型化字面量:未加引号的值除
true/false/null外一律按字符串下发(kqlLiteralValue,见 datasource/commons/eslike/kql.go)。因此bytes >= 1024生成{"gte": "1024"}(字符串形式),数值与日期(含 epoch 毫秒)字段由 Elasticsearch 按 mapping 解析。field: true、field: null则分别生成布尔true与null值。 - 通配符逃逸:内部哨兵
@kuery-wildcard@是「恰好包含该子串的字面量会被还原为通配符」的唯一例外,实际内容中出现该字符串的概率极低,测试TestCompileKQLWildcardMarkerLiteralFieldDoesNotBecomeWildcard也验证了字段名中出现该字面量不会被误判。
六、mapping 无关的编译策略
当前编译器不读取 Elasticsearch mapping,严格复现前端默认转换器buildESQueryFromKuery的 no-mapping 分支,不会按text/keyword字段类型切换查询类型(如keyword走term、text走match这类优化不会发生)。这意味着:
- 裸值统一走
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、*timeout、message: **均编译失败,错误信息包含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=api、log.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"} } } ] }要点回顾:
filter中http.response.status_code >= 500是数值范围比较,编译为range查询;因为目标字段不是date_field,time_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 直接相关的行为需要了解:
- 命中文档的
_source会直接成为records[].fields:result_type=logs时,每条记录的字段就是文档原始_source内容; - 不会返回
_id、_index或sort: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.go:
Query参数结构(FilterLanguage、KQLOptions字段的 JSON 标签)、QueryData/QueryLog中调用GetFilterQuery并拼入时间范围的主流程; - datasource/es/es.go:Elasticsearch 插件如何把 V2 的 payload 委托给
eslike(MakeLogQuery/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_QUERY、DATASOURCE_TIMEOUT、EXPRESSION_*、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),仅供参考