- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
导读
Grafana Mimir 在 pkg/mimirtool/printer/printer.go 中通过chroma/v2/quick.Highlight对 Alertmanager 配置、告警规则等 YAML 与模板内容做终端语法高亮,而其词法分析器(Lexer)来自 vendored 依赖github.com/alecthomas/chroma/v2。本文以该依赖目录下的 lexers/README.md 为骨架,系统讲解 Chroma 词法分析器的两种定义形态(XML 声明式为主、Go 代码为辅)、其内部注册与加载机制,以及围绕testdata/*.actual与*.expected展开的快照测试工作流——包括RECORD=true go test ./lexers一键重新生成期望文件的方法,以及 Windows 环境下的等效操作。读完本文,你将掌握在 Chroma 生态中新增词法分析器、编写其测试用例、重跑测试与重建期望文件的完整流程。
一、总原则:词法分析器优先用 XML 定义
原文档开篇即给出 Chroma v2 的一条硬性约定:
All lexers in Chroma should now be defined in XML unless they require custom code.
即:所有词法分析器都应优先以 XML 声明式定义,只有在确实需要自定义逻辑时才编写 Go 代码。这意味着语言规则、状态机、正则模式与 Token 类型全部数据化,便于维护、审阅与自动加载。
这一原则在 Mimir 仓库的 vendor 副本中有直接源码印证。lexers/lexers.go 中的GlobalLexerRegistry初始化逻辑,本质就是一次“批量加载 XML”的过程:
//go:embed embedded var embedded embed.FS var GlobalLexerRegistry = func() *chroma.LexerRegistry { reg := chroma.NewLexerRegistry() paths, err := fs.Glob(embedded, "embedded/*.xml") if err != nil { panic(err) } for _, path := range paths { reg.Register(chroma.MustNewXMLLexer(embedded, path)) } return reg }()要点拆解:
- 借助 Go 1.16+ 的
go:embed指令,把embedded/目录下的全部 XML 文件编译进二进制; fs.Glob(embedded, "embedded/*.xml")收集所有词法器定义文件;chroma.MustNewXMLLexer(embedded, path)将每个 XML 解析为可用的chroma.Lexer,失败即 panic;- 最后统一
Register进全局注册表。
在本仓库的 vendor 副本中,embedded/目录包含从abap.xml、actionscript.xml、bash.xml、c.xml、clojure.xml到python.xml等数百种语言的 XML 定义,覆盖语言数量远超常见的代码高亮需求。这些正是lexers.Names()、lexers.Get("python")、lexers.Match("main.go")等入口函数的数据来源(详见同一文件的公开 API 与 registry.go 中的LexerRegistry实现)。
二、XML 词法器文件的结构剖析
要理解“XML 定义词法器”具体长什么样,直接看一份真实定义最直观。以 embedded/python.xml 为例,其顶层骨架为:
<lexer> <config> <name>Python</name> <alias>python</alias> <alias>py</alias> <filename>*.py</filename> <filename>*.pyi</filename> <filename>BUILD</filename> <filename>WORKSPACE</filename> <mime_type>text/x-python</mime_type> <mime_type>application/x-python</mime_type> </config> <rules> ... </rules> </lexer>2.1<config>:词法器的元信息
<config>段对应 Chroma 的chroma.Config结构(见 lexer.go),常用字段包括:
| XML 元素 | 对应字段 | 作用 |
|---|---|---|
<name> | Name | 词法器显示名,如Python |
<alias> | Aliases | 可重复,用于命令行与代码中引用,如python、py、python3 |
<filename> | Filenames | 文件通配模式,供按文件名自动匹配,如*.py |
<mime_type> | MimeTypes | 供MatchMimeType按 MIME 类型匹配 |
<case_insensitive> | CaseInsensitive | 正则是否大小写不敏感 |
<dot_all> | DotAll | 正则是否让.匹配换行 |
<priority> | Priority | 多词法器竞争时的优先级(默认视为 1) |
<analyse> | Analyse | 内容分析打分规则,用于AnalyseText文本启发式匹配 |
2.2<rules>:状态机与规则
<rules>内是按状态(<state>)组织的规则表,每个<rule>由一个正则pattern与一个<token>组成。继续以 Python 数字状态为例(embedded/python.xml):
<rules> <state name="numbers"> <rule pattern="(\d(?:_?\d)*\.(?:\d(?:_?\d)*)?|(?:\d(?:_?\d)*)?\.\d(?:_?\d)*)([eE][+-]?\d(?:_?\d)*)?"> <token type="LiteralNumberFloat"/> </rule> <rule pattern="0oO+"> <token type="LiteralNumberOct"/> </rule> <rule pattern="0bB+"> <token type="LiteralNumberBin"/> </rule> <rule pattern="0xX+"> <token type="LiteralNumberHex"/> </rule> <rule pattern="\d(?:_?\d)*"> <token type="LiteralNumberInteger"/> </rule> </state> </rules>从中可以看出 Chroma 的状态机设计:
root是默认起始状态,其余状态由<push>、<pop>、<combined>、<include>等转移指令驱动(如 Python 字符串规则中的combined state="dqf"、push state="tdqs");<token>指定命中的 Token 类型,如LiteralNumberFloat、CommentSingle、Keyword等;- 复杂捕获可用
<bygroups>把多个捕获组分别映射到不同 Token 类型,配合<include>复用其他状态的规则。
这种数据驱动结构正是“优先 XML 定义”的底气:新增语言支持时,多数情况下只需新增一个 XML 文件,无需改动 Go 代码。
三、需要自定义代码的词法器:Go 形态与混合形态
“除非需要自定义代码”意味着仍有少量词法器绕过 XML。Mimir 仓库 vendor 副本中的 go.go 就是典型:Go 词法器用MustNewLexer(&Config{...}, goRules)直接在 Go 中声明规则,并额外通过SetAnalyser注册内容打分器:
var Go = Register(MustNewLexer( &Config{ Name: "Go", Aliases: []string{"go", "golang"}, Filenames: []string{"*.go"}, MimeTypes: []string{"text/x-gosrc"}, }, goRules, ).SetAnalyser(func(text string) float32 { if strings.Contains(text, "fmt.") && strings.Contains(text, "package ") { return 0.5 } if strings.Contains(text, "package ") { return 0.1 } return 0.0 }))其规则函数goRules()直接返回chroma.Rules映射表,每条规则同样是“正则 + Token 类型”,与 XML 形态一一对应。SetAnalyser返回的分数(0.0~1.0)会被lexers.Analyse(text)用于在没有明确文件名/别名线索时推断最可能的语言。
另一类更复杂的形态是混合/委托词法器,例如 markdown.go:它包装基础 Markdown 词法器,在Tokenise时先检测文档头部的 YAML frontmatter,将其拆出交给lexers.Get("YAML")处理,其余部分交给 Markdown 规则,最终用Concaterator拼接两个 Token 流。这类需要“多语言协同”的解析逻辑,正是 XML 难以表达、必须落到 Go 代码的典型场景。同理,erb.go、yaml_jinja.go 中的DelegatingLexer也是这类混合定义的实现。
四、词法器测试机制:快照式(Golden File)测试
原文档的核心章节围绕测试展开。Chroma 采用经典的快照测试思路:给定已知输入,校验词法分析器的输出是否符合预期。
4.1 测试数据约定
- 单个词法器
<name>的测试:输入文件放在testdata/<name>.actual,期望输出放在testdata/<name>.expected。测试运行时,把*.actual喂给<name>词法器解析,逐字节比对解析结果与*.expected; - 同一词法器多组用例:在
testdata/<name>/目录下放置多个*.actual文件(例如testdata/go/comments.actual、testdata/go/strings.actual),每个文件独立生成对应的*.expected,实现一次覆盖多个语法特性。
这种“输入-期望输出”成对出现的约定,让测试用例本身就以源码文件形式沉淀在仓库中,新增语言特性时先补.actual用例、再录制.expected,形成可读性极强的回归测试。
说明:本仓库作为依赖方采用 Go module vendor 模式引入 Chroma,vendor 副本按 Go 规范会裁剪
_test.go测试文件与测试数据(testdata/目录在此副本中不存在),上述测试目录与测试驱动代码位于 Chroma 上游源码树中。在 Chroma 源码目录下按如下命令操作即可。
4.2 运行测试
在 Chroma 项目根目录执行:
go test ./lexers命令会编译./lexers包及其依赖,对每个词法器运行快照测试,任何.actual解析结果与.expected不一致都会导致测试失败,并输出差异信息。
4.3 更新测试:RECORD 模式一键重建期望文件
当你新增了.actual测试数据(例如为某个词法器补充新的语法用例)后,需要重新生成对应的.expected文件。原文档给出的标准做法是:
RECORD=true go test ./lexers其工作方式:
- 先通过环境变量
RECORD设为true,告诉测试驱动进入“录制模式”; - 再运行
go test ./lexers,Chroma 不再比对期望文件,而是以当前词法分析器的实际输出为准,直接生成/覆盖对应的*.expected文件; - 录制完成后即可把
RECORD环境变量移除或复位,恢复为普通断言模式。
这是保证.actual与.expected始终成对、且期望文件忠实反映当前词法器行为的自动化手段,也是 Chroma 词法器开发的核心工作流。需要留意:录制出的.expected反映的是当前词法器行为,若因规则修改导致输出变化,应人工审阅 diff 后再接受录制结果,避免把回归悄悄“固化”进期望文件。
4.4 Windows 用户的注意事项
原文档特别提醒:RECORD=true go test ./lexers这种“前置环境变量 + 命令”的写法在 Windows 标准命令提示符与 PowerShell 中均无法直接运行(那是 Unix shell 的语法),需要分两步执行:
- 命令提示符(cmd):先用
set RECORD=true为当前会话设置环境变量,再运行go test ./lexers; - PowerShell:先用
$env:RECORD = 'true'设置,再运行go test ./lexers; - 也可在 Windows 系统设置中手工添加持久化的用户环境变量
RECORD=true,这样后续每次go test ./lexers都处于录制模式,但务必在完成后删除该变量,避免持续覆盖所有.expected。
设置完成并运行go test ./lexers后,Chroma 会重新生成测试文件并把结果打印到控制台窗口。
五、在 Mimir 仓库中的落地:mimirtool 的终端高亮
理解词法器与测试机制后,再看它如何在 Mimir 中被真正消费,会让整条链路更完整。Mimir 并未直接使用lexers包内部的测试设施,而是通过 Chroma 的quick便捷 API 做终端高亮。pkg/mimirtool/printer/printer.go 中,PrintAlertmanagerConfig与PrintRuleGroups两个方法正是对quick.Highlight的调用:
func (p *Printer) PrintAlertmanagerConfig(config string, templates map[string]string) error { // go-text-template if !p.disableColor { err := quick.Highlight(os.Stdout, config, "yaml", "terminal", "swapoff") if err != nil { return err } } ... }这里quick.Highlight的参数依次是:输出流、待高亮源码、语言名(yaml、go-text-template)、格式化器(terminal,即终端 ANSI 着色)、样式名(swapoff)。其内部正是:按语言名在词法器注册表中查找对应 Lexer →Tokenise切分 Token 流 → 交给终端格式化器输出带颜色的文本。同时,printer.go通过disableColor(由New(disableColor, forceColor, isTTY)依据是否 TTY、是否禁用颜色开关决定)来控制是否启用高亮——非交互/禁用场景直接fmt.Println输出纯文本,避免终端转义序列污染脚本输出。
对应测试见 pkg/mimirtool/printer/printer_test.go,它验证了打印机在不同颜色开关组合下的输出行为。这一处真实用例说明:chroma/v2的lexers(XML 定义 + 注册表机制)与quick层在 Mimir 工具链中承担的是“规则输出格式化”职责,与项目对 YAML/模板配置的可读性要求直接相关。
六、总结与最佳实践
回顾原文档与源码,可以沉淀出如下可直接复用的实践清单:
- 定义词法器:优先新增 XML 文件到
lexers/embedded/(<lexer>+<config>+<rules>),只有涉及多语言委托、frontmatter 拆分、内容打分等逻辑时才用 Go 写,并通过Register()挂到全局注册表; - 补测试数据:单个用例用
testdata/<name>.actual,多用例放testdata/<name>/目录;期望文件始终由录制模式生成而非手写; - 跑测试:
go test ./lexers校验所有词法器; - 改规则后重建期望:
RECORD=true go test ./lexers重新录制;Windows 下改用set RECORD=true/$env:RECORD = 'true'两步执行,用完即清理环境变量; - 验收新用例:录制完成后重新执行普通
go test ./lexers,确保新.expected通过断言。
这套“XML 声明式词法器 +*.actual/*.expected快照测试 +RECORD录制重建”的工程化组合,是 Chroma 维护数十种语言高亮质量的关键机制,也是任何计划为其贡献新语言支持或修复高亮缺陷的开发者必须掌握的工作流。
- 时序数据库
- 后端
- 数据存储
- 可观测性
- 云原生
【免费下载链接】mimir
Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.
相关推荐
Chroma Lexers 开发指南:XML 定义、注册机制与 RECORD 快照测试实战
Chroma Lexers 开发指南:XML 定义、注册机制与 RECORD 快照测试实战 导读 Chroma 是 Go 生态中广泛使用的语法高亮库,其 lex
可观测性日志分析后端微服务对象存储云原生Grafana Mimir 认证授权机制深度解析
Grafana Mimir 认证授权机制深度解析 引言 在现代云原生监控体系中,多租户(Multi tenancy)架构已成为大规模部署的必备特性。Grafan
时序数据库后端数据存储可观测性云原生Chroma v2 项目结构解析与开发运行指南:从 XML 词法定义到 chromad 在线 Playground
Chroma v2 项目结构解析与开发运行指南:从 XML 词法定义到 chromad 在线 Playground Chroma 是一个用纯 Go 编写的通用语
可观测性日志分析后端微服务对象存储云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考