dsh-weknora
Tencent/WeKnora把 WeKnora 知识库接入 dsh 的四个只读工具:列出知识库、混合检索原文片段、按顺序还原单篇文档,以及直接取用 WeKnora 自己带引用的 RAG 或 ReAct agent 回答(含可续聊的 session id)。
DSH Web GUI 的日志查询卡片(Elasticsearch / OpenSearch):一个查询面——按来源方言给查询串与可点例子、字段名补全、筛选芯片、可点柱缩放时间窗的时间分布、把分组的桶点成筛选、带级别染色与堆栈续行标记的结果列表(可选列、可翻页);Docker 容器日志作为采集来源复用 docker 插件。agent 侧五个只读 loglens_* 工具(sources / search / fields / facets / context)与面板共用同一执行层。出站只打到已配置的来源,按解析后的 IP 判定,不跟随重定向,并有体积与超时上限。
中文 | English
DSH 的日志查询面板:把 Elasticsearch / OpenSearch / Docker 归一化成一个查询面——同一张卡片里查、看趋势、按字段下钻;agent 侧是 loglens_* 只读工具。可查后端:Elasticsearch、OpenSearch。Docker 不另做适配器,作为采集来源指向现成的 docker_logs / docker_logs_grep。Loki 本版不从界面提供(适配器与真机验收成果都在仓库里, 但它的能力闸没在真机上验过)——判据与恢复方式见 § 已知限制。
ES 与 OpenSearch 共用一份实现,按 flavor 分流)。Docker 不属于适配器层:它是采集来源,直接复用 docker_logs / docker_logs_grep。归一化与夹取仅由 src/query.ts 一处承担 (时间戳单位、时间窗上限、limit、方向、游标透传),适配器不得重复夹取;面板与 agent 工具共用 src/execute.ts,两侧的行模型与截断信号同源。
capabilities(): BackendCaps(queryLanguage / pagination /aggregations / follow / maxPageSize / maxWindowMs / fields),UI 渲染与参数夹取均以自述为准, 面板不按来源类型推断图表能力。自述为 true 的位必须有对应实现,由 test/capabilities.test.ts 逐个适配器对账(Loki 的 follow 因此声明为 false:stream() 未实现)。
LogCounts.totalKnown 仅在存在精确总数时为真(ES 侧须开启track_total_hits,响应 relation === 'gte' 时置假);截断由 Truncation.reasons 枚举表达, 其中 hit_limit(确定还有)与 hit_limit_maybe(可能还有)是两个独立理由,cursor_expired 与二者分离(游标过期不蕴含「还有更多」)。界面直接呈现上述判据。
loglens D03);ES / OpenSearch 侧为 PIT + search_after,分页基于同一快照,过期时返回 cursor_expired 且不静默重开(重开会改变快照语义)。decode 校验版本与来源,解码失败即报错,不视为「无游标」。
「由正文推断的级别」(levelInferred),两种可信度不合并;嵌套字段摊平为点分标签;正文高亮 使用后端返回的字符区间(line.highlights),界面不对查询串做字面匹配(stream:stderr / -failed / error* 各有匹配规则,字面匹配会产生误标)。
loglens_* 工具全部归 read 档;src/net.ts的围栏按解析后的 IP 判定(链路本地与云元数据地址拒绝),不跟随重定向,单次请求上限 8 MiB、 超时默认 20s(NET_MAX_RESPONSE_BYTES / NET_DEFAULT_TIMEOUT_MS)。完整边界见「已知限制」。
dsh plugin --profile web add @hyzyn/dsh-loglens # npm 安装(发布后)
dsh plugin --profile web add link:$(pwd)/packages/loglens # 仓库开发调试聚合包 @hyzyn/dsh-all(或仓库根 bundle)已包含本插件,一次装齐时不必单独 add。装完重启 dsh web, 设置 → 插件配置 里出现「日志查询」卡片(保存即热生效,无需重启)。
已在 web profile 里装过 @hyzyn/dsh-all 或根 bundle 时不要再 add 本包,否则插件行重复挂载,启动报 duplicate loader entry id。配置存在当前 profile 的 plugin entry 上(settings.update('loglens', …) 的 volatile 路径)。 来源配置在插件整页里默认收起成一行摘要(2 个来源 · 当前 一 + 一颗「编辑来源」), 设置卡里默认展开(那一页的主任务就是配来源)。收起时不渲染那些行(不是 display:none)。
级别、条数都在查询区;查询串独占整行剩余宽度,其余集中在第二行。
FIELD_COMPLETION_MAX),点一条就原位补成字段: 并把光标摆到冒号后。补全不自动发起查询;字段清单在第一次需要时自己拉(按来源缓存, 一个来源一次会话只拉一次)。候选是卡片自己画的浮层,不是浏览器那个原生 datalist。
不限 / trace+ … fatal),用的是宿主既有的 level 入参。warn+ 就是「warn 及以上」——+ 只是缩短标签,完整口径写在同一行下面的边界说明里。 注意它是页内筛:宿主在返回的这一页上过滤,计数与截断提示仍是未筛的整页,界面上写着这句。 要按级别看全量得后端真有级别字段:ES 上 log.level 常常整列是空的(级别是从正文推断的, 行上有 levelInferred),那种来源连分组榜也拿不到级别。
query_string,面板不改写:留空即全量(match_all);"短语"、a OR b(空格分隔的词默认是「或」,要「与」得写大写 AND)、name* 都按 Lucene 走。 level:ERROR 常回 0 条不是坏了——这套索引里 log.level 多半为空,按级别筛请用级别下拉。
[ERROR] 会被 Lucene 当成区间表达式,行为与「含 ERROR 这个词」不是一回事。要精确匹配这个词,写 "ERROR"(短语)或 message:ERROR。
level=error 不是「字段等于值」,多半被当成两个普通词(默认 OR)。真字段筛选请用 字段:值(log.level:ERROR)或面板的筛选行。
这三条合起来解释了一个常见困惑:同一个窗口里 [ERROR] 与 level=error 命中数差一倍, 不是分词把哪个拆错了,是两者都不是「精确匹配 ERROR」。 要一个可复现的口径,就用 `"ERROR"`(短语),并在不同写法之间比对时先怀疑上面三条。
query_string)。ES 的例子是 error / "Failed to" / error AND failed / stream:stderr,Loki 是 {job="app"} |= "error" 这类——点一下填进框里、不自动查(先看清楚,必要时改一改)。 例子本身不翻译(它是代码不是文案)。 ⚠️ Kibana 的 Discover 默认是 KQL,不是 Lucene:本面板是 Lucene,「且」写大写 AND (小写会被当成普通词);写错了不报错,只是数字不对。
HTTP 200,原因在 _shards.failures[].reason 里——会带成一行 [分片] 摆在结果上方(q=level:(ERROR → Failed to parse query [level:(ERROR])。 少了这一个括号与「集群真的出问题」该找的人不同。
结果区顶上一句「下面这份还是上一次查询的」,同时收起「下一页」并禁用「分组」——ES / OpenSearch 的 游标是 PIT + search_after,拿旧游标配新条件翻页不会报错,只会静默少行。先点「查询」把两边对齐。
multiline 时,一次异常会被拆成多个文档(真机实测 MyBatis 固定拆11 条),只有首条带级别,其余是 \tat … / Caused by: / ### Error querying 这类续行。 它们本身没有级别、也不该被算进任何级别,所以行首标一个 ↳,并且按级别筛时会单独报出「其中 N 行是 堆栈续行」——否则它们会静默消失,让人以为 error 数就是那么多。判据只认堆栈语法(不靠"看起来像"), 误标会让一条正常日志被排除在任何级别之外,所以宁可漏标。
[ERROR] 前缀、level=ERROR / log.level":"info"(logfmt 与JSON 两种)、logback 的 18:31:54,850 |-INFO in …、以及 logback 的 14:00:00.123 ERROR 12345 ---。 固定窗口 300 条真机样本里,未分类从 139 条降到 3 条(剩下的是启动横幅这类真没有级别的行)。
+N 浮层 / 字段面板里。其余标签收在行尾的「+N」里:点开是列全部标签的浮层,点任意一条立刻筛 (点背景 / × / Esc / 一滚就收起)。值本身不截断(下发的永远是原值)。
RAW_PREVIEW_MAX)会截断并报出原长。——一眼扫过去先看哪一行。默认「完整」模式(正文全展开),「紧凑」开关则每条只留 3 行,给扫一眼找异常用。
☰ 字段 / ☰ 紧凑 / ⇅ 排序 / 🗑 清空结果。排序只作用于已返回的这一页(宿主没有服务端排序,入口的悬停说明自己写着这句),三档循环:时间 / 级别 / 正文;默认「时间」就是 宿主原序(唯一与翻页游标同源的顺序,不许按 ts 重排——同纳秒的多行是切不开的一组)。
localStorage:loglens.density / loglens.sort),换设备不跟;排序不发请求。
min(720px, 72vh) + 300px 下限,窗口越高露得越多。(当前是 ES / OpenSearch)。两份图都来自后端的聚合,不是数这一页——那一页被 limit 截断过, 数它只会得到一个假的分布 / 假的排名。
悬停哪一根,气泡就贴在那一根正上方,写 19:22 · 315 条 · 占 1%;不悬停时不画。 点一根有数据的柱子 = 放大到那一档(窗口夹到那一段再重查,行、分布、榜一起收窄), 正在放大的那一档高亮着,下拉变成按真实范围现算的「自定义」;再点它就是取消放大。 空桶不给点(点进一个 0 条的窗口只会得到「没有命中」,看不出是自己点的)。 放大窗口有 1 分钟下限,被它夹过会明说一句(静默收窄比报错更难查—— 一档分桶最细是 10 秒,点它夹出来的窗通常空手而归,而界面上只看见「没有命中」)。 相邻桶之间的空档补 0——否则「那段时间一条都没有」看起来会像「一直在刷」。
图的意义是看差异。最大 / 最小 ≥ 3 时走线性,< 3 时从 min × 0.9 起画并明说 「柱高只表示相对高低,不是与条数成正比」;全一样时不裁剪、也不写那句说明。 判据与代价在 pure.js 的 histogramScale 头注释里。
点开那一格看当前来源的字段名候选:不传 `limit` = 不截断,摆出来的就是这个来源的全部字段 (与 Kibana 的 _field_caps 同口径;同来源不重复拉,页脚报「共 N 个字段」并有「刷新」)。 候选里只有「能用的名字」:object / nested 这类没有自己值的容器行不列;同名在不同索引里 类型不一致的字段在行尾标「类型冲突」(悬停列出两种类型)。
(「还有 N 条落在没列出来的桶里」);桶计数是近似值时同样照抄那条警告。 空值键(文档缺这个字段)显示成「(空值)」,而不是一条没有名字的横条。
FIELD_PANEL_WIDTH;高度跟结果区走,字段列表自己滚),结果表始终是整行宽度——正文不因它变窄。点面板外任何地方、或再点一次「字段」收起。收起时不会去拉 `/fields`, 展开才拉一次(同来源缓存)。
labels 数出本页命中的字段(带出现行数),只画这一区(列只能从本页命中加)。cont name 和 container name 都筛得到container.name。搜索时右上角报命中几个,并自动展开全部分组。
title 里。长末段断在 _ / - / . 之后(不是词中间)。
RESULT_COLUMN_MAX),满员后未选的行变灰禁用(已选的仍可取消)。有额外列时结果表改成按列宽排布:正文有 198px 下限,窗口不够就横向滚动。