Lightdash Prometheus 自定义事件指标(Custom Metric Manager)实战:用 JSON 配置把分析事件自动变为 Prometheus 计数器
【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash
本文以 Lightdash 后端packages/backend/src/prometheus模块中的 Custom Metric Manager(配置驱动的自定义指标系统)为核心,讲解如何通过一份 JSON 配置文件,将 Lightdash 内置的 analytics 分析事件(如用户登录、查询执行、图表创建)自动映射为 Prometheus Counter 计数器,并随事件触发实时自增。读者学完后,将掌握:自定义事件指标的配置格式与全部字段含义、标签(label)的提取与兜底规则、Prometheus 端点的启用方式,以及该机制在源码中的完整调用链与安全校验实现。
一、为什么需要"配置驱动"的自定义事件指标
Lightdash 的 Prometheus 监控体系由 PrometheusMetrics 类 统一承载,它内置了大量与业务强相关的指标,例如查询状态计数器lightdash_query_status_total、查询全链路耗时直方图lightdash_query_total_duration_seconds、预聚合(pre-aggregate)命中与物化指标、MotherDuck 实例缓存指标、AI Agent 响应时长指标等。这些指标都是硬编码在类内部的,指标名、标签、帮助文本在编译期就已确定。
但对于自建部署(self-hosted)的团队来说,往往会希望监控自己的业务节奏:比如"今天有多少次登录失败"、"按项目维度统计查询执行量"、"图表创建/仪表盘创建的趋势"等等。如果每个这样的需求都要改源码、重新发版,显然不现实。
Custom Metric Manager 正是为此设计的运行时、配置驱动方案:运维或分析人员只需编写一份 JSON 文件,声明"监听哪个事件、生成哪个计数器、取哪些标签",无需改动任何代码,Lightdash 启动时就会自动完成计数器的注册与订阅。核心实现位于 PrometheusEventMetricManager.ts,其文档化的能力包括:
- 为每个配置的事件创建 Prometheus Counter;
- 订阅 LightdashAnalytics 的 track 调用;
- 从事件 payload 中动态提取标签;
- 事件触发时自动自增对应计数器。
二、整体工作流程与源码调用链
从源码结构看,整套机制的运行时链路可以概括为四步:
LightdashAnalytics.track(payload) │ 内部通过 EventEmitter 发射 ▼ 事件总线发射 "analytics.track.<eventName>" │ PrometheusEventMetricManager 在 initialize() 中订阅 ▼ handleTrackEvent(payload) 提取标签值 │ ▼ counter.inc(labelValues) 自增 prom-client Counter对应到具体源码:
- 埋点入口:LightdashAnalytics.ts 重写了 analytics SDK 的
track行为,在分发事件时调用this.eventEmitter?.emit(\analytics.track.${payload.event}`, payload)(见该文件约 L4252-L4253 处)。事件键名统一为analytics.track.前缀加事件名,由PrometheusEventMetricManager.toAnalyticsEventKey()` 生成。 - 订阅与自增:PrometheusEventMetricManager.ts 在
initialize()中完成计数器注册与事件订阅,handleTrackEvent()负责按配置提取标签并调用counter.inc()。 - 装配入口:Lightdash 应用启动时,App.ts 创建
PrometheusMetrics实例并调用其start()启动指标 HTTP 服务;随后通过monitorEventMetrics(this.analyticsEventEmitter)(约 L353)把 analytics 事件总线交给事件指标管理器,这正是 README 中"初始化由App.start()自动处理"的源码依据。
模块目录packages/backend/src/prometheus/下共包含:
| 文件 | 作用 |
|---|---|
| PrometheusEventMetricManager.ts | 自定义事件指标管理器(本文核心) |
| PrometheusMetrics.ts | 内置 Prometheus 指标、HTTP 服务、事件指标装配 |
| PrometheusMetrics.test.ts | 指标行为单元测试 |
| custom-metrics.config.example.json | 官方示例配置文件 |
| otelHttpMetrics.ts | OTel HTTP 指标序列化 |
三、启用 Prometheus 与自定义事件指标
3.1 相关的环境变量
Prometheus 相关配置在 parseConfig.ts 的prometheus段集中解析(约 L3388-L3420),与本文主题相关的变量如下:
| 环境变量 | 默认值 | 说明 |
|---|---|---|
LIGHTDASH_PROMETHEUS_ENABLED | false | 是否启用 Prometheus 指标系统,必须为true本文机制才会初始化 |
LIGHTDASH_PROMETHEUS_PORT | 9090 | 指标 HTTP 服务监听端口 |
LIGHTDASH_PROMETHEUS_PATH | /metrics | 指标抓取路径 |
LIGHTDASH_PROMETHEUS_PREFIX | 空 | 所有指标的全局前缀,会被拼到每个计数器名前 |
LIGHTDASH_PROMETHEUS_EVENT_METRICS_ENABLED | false | 是否启用"事件→计数器"的自定义事件指标(默认关闭) |
LIGHTDASH_CUSTOM_METRICS_CONFIG_PATH | 空 | 自定义指标 JSON 配置文件路径(推荐) |
CUSTOM_METRICS_CONFIG_PATH | 空 | 同上的兼容别名,两者取一(源码优先读取前者) |
LIGHTDASH_CUSTOM_METRICS_CONFIG_PATH与CUSTOM_METRICS_CONFIG_PATH二选一即可,源码中的读取顺序为:先取前者,为空再取后者。
3.2 最小启用步骤
# 1. 启用 Prometheus 与自定义事件指标 export LIGHTDASH_PROMETHEUS_ENABLED=true export LIGHTDASH_PROMETHEUS_EVENT_METRICS_ENABLED=true # 2. 指向配置文件(二选一) export LIGHTDASH_CUSTOM_METRICS_CONFIG_PATH=/path/to/your/config.json # 或 export CUSTOM_METRICS_CONFIG_PATH=/path/to/your/config.json # 3. 启动 Lightdash,管理器会自动初始化启动后,管理器会在PrometheusMetrics.start()建立的 HTTP 服务上暴露指标,默认地址为http://localhost:9090/metrics(若修改了端口或路径则相应变化)。
需要特别强调的是:LIGHTDASH_PROMETHEUS_EVENT_METRICS_ENABLED与LIGHTDASH_PROMETHEUS_ENABLED必须同时为true。从 PrometheusMetrics.ts 的monitorEventMetrics()(约 L1964 起)可以看到,该函数第一行就做了双重判断:只要 Prometheus 未启用或事件指标未启用,就直接返回、不做任何初始化。
四、JSON 配置文件详解
4.1 完整配置结构
官方示例位于 custom-metrics.config.example.json:
{ "metrics": [ { "eventName": "user.logged_in", "metricName": "lightdash_user_login_total", "help": "Total number of user login events", "labelNames": ["loginProvider"] }, { "eventName": "query.executed", "metricName": "lightdash_query_executed_total", "help": "Total number of query executions", "labelNames": ["context", "projectId"] } ] }4.2 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
metrics | 数组 | 是 | 指标配置列表,每一项声明"一个事件 → 一个计数器"的映射 |
eventName | 字符串 | 是 | 要监听的 Lightdash analytics 事件名,如user.logged_in、query.executed |
metricName | 字符串 | 是 | 生成的 Prometheus 指标名,必须符合 Prometheus 命名规范(小写字母、数字、下划线,建议以_total结尾的计数器命名约定) |
help | 字符串 | 是 | 指标帮助文本,会写入 Prometheus 的# HELP行 |
labelNames | 字符串数组 | 是 | 要从事件 payload 中提取的标签名数组,必须与payload.properties中的属性键完全一致(注意大小写,如loginProvider、projectId) |
配置在读取后还会经过一层严格的 zod 校验。在 PrometheusMetrics.ts 中,schemaprometheusEventMetricsConfigSchema(约 L37-L50)规定:
eventName、metricName、help均为非空字符串(z.string().min(1));labelNames中的每一项必须匹配正则^[a-zA-Z_][a-zA-Z0-9_]*$,即必须是合法的 Prometheus 标签名;- 配置对象使用
.strict(),不允许出现 schema 之外的未知字段。
因此,若配置文件格式非法(如字段缺失、标签名含连字符、多写了未知字段),管理器会打印错误日志并跳过初始化,不会导致 Lightdash 启动失败。
4.3 配置文件加载与安全检查
monitorEventMetrics()加载配置时还做了一层路径穿越防护:它先用path.resolve(process.cwd(), configPath)解析为绝对路径,再校验该路径必须位于工作目录之内(不能以../等方式逃逸出工作目录),否则直接抛出 "path traversal detected" 错误。此外:
- 配置文件不存在 → 记录 warning 并跳过;
- JSON 解析失败 → 记录 error 并跳过;
- 校验不通过 → 记录 error 并跳过。
这些行为与 README 中"如果配置文件缺失或非法,管理器将记录警告并跳过初始化"的描述完全一致。
五、标签提取机制与兜底规则
5.1 提取逻辑
默认情况下,标签值从事件 payload 的properties中按labelNames逐键提取。源码extractLabelValues()(PrometheusEventMetricManager.ts 约 L199-L217)的逻辑为:
for (const labelName of metricConfig.labelNames) { const value: unknown = payload.properties?.[labelName]; labelValues[labelName] = value !== undefined && value !== null ? String(value) : 'unknown'; }两个关键行为值得注意:
- 缺失兜底:若属性不存在或为
null/undefined,标签值统一设置为字符串"unknown",保证计数器不会因缺标签而抛错; - 强制字符串化:payload 中的属性值会通过
String(value)转为字符串(Prometheus 标签值必须是字符串),例如布尔值、数字、UUID 都会被正确序列化。
5.2 一个完整的匹配示例
假如配置了如下指标:
{ "eventName": "query.executed", "labelNames": ["context", "projectId"] }某处代码执行埋点:
analytics.track({ event: 'query.executed', properties: { context: 'api', projectId: 'project-123', }, });那么lightdash_query_executed_total计数器将以标签集{ context: 'api', projectId: 'project-123' }自增 1。若某次事件的properties里缺少projectId,则会以{ context: 'api', projectId: 'unknown' }自增——这也是排查标签口径问题时最常见的现象。
5.3 多个指标监听同一事件
subscribeToAnalyticsEvents()在内部会把配置按eventName分组(metricsByEvent),同一个事件只注册一个事件监听器,但会依次驱动该事件下的所有指标配置。也就是说,允许在配置中为同一个事件声明多个不同metricName、不同labelNames的计数器,而不会产生重复监听。
六、可追踪的常用事件
README 给出的常用事件如下:
| 事件名 | 含义 |
|---|---|
user.logged_in | 用户登录 |
user.created | 用户创建 |
query.executed | 查询执行 |
saved_chart.created | 图表创建 |
dashboard.created | 仪表盘创建 |
需要更完整的事件清单时,可查阅 LightdashAnalytics.ts:该文件定义了全部类型化事件(TypedEvent 联合类型),涵盖用户、项目、查询、图表、仪表盘、调度、AI Agent 等各类埋点,事件名通常采用领域.动作的点分命名风格。你在配置文件里写的eventName必须与这些事件名严格一致(含大小写),否则事件永远不会被匹配到。如果某个行为没有现成事件,则需要扩展该文件新增事件——那是另一项埋点开发工作。
七、指标采集、查询与命名
启用后,访问http://localhost:9090/metrics(或自定义的端口/路径)即可看到输出。管理器通过prom-client的全局 registry 注册计数器,并会在输出中包含# HELP与# TYPE注释。若配置了LIGHTDASH_PROMETHEUS_PREFIX,该前缀会被拼在metricName之前(见initialize()中的`${prefix ?? ''}${metricConfig.metricName}`),便于在混合采集场景下区分指标来源。
假设已按官方示例配置,可用 PromQL 做典型查询:
# 用户登录总数 lightdash_user_login_total # 按登录提供商维度聚合 sum by (login_provider) (lightdash_user_login_total) # 查询执行速率(5 分钟窗口) rate(lightdash_query_executed_total[5m]) # 按项目维度查看查询量 sum by (project_id) (rate(lightdash_query_executed_total[5m]))这里login_provider、project_id是 Prometheus 对标签名(原始为loginProvider、projectId)的规范化表示,不影响配置中必须使用原始属性键的规则。
八、初始化、幂等与清理:源码级原理
PrometheusEventMetricManager的生命周期管理非常规范,值得在自建指标系统时借鉴:
- 幂等保护:
initialize()开头检查isInitialized,重复调用只记录 warning 并直接返回,避免重复注册计数器导致prom-client抛错; - 禁用短路:若
prometheusConfig.enabled为false,记录 info 日志并跳过,与 3.1 节的环境变量约束呼应; - 失败清理:初始化过程中一旦出现异常,会先调用
cleanup()移除已注册的计数器与监听器,再重新抛出错误,保证不会残留半初始化状态; - 优雅清理:
cleanup()会遍历eventListeners用eventEmitter.off()移除全部订阅,并通过prometheus.register.removeSingleMetric()从全局 registry 移除计数器,随后重置isInitialized;该清理在应用关闭流程(App.ts中的prometheusMetrics.stop())中被调用; - 单点错误隔离:
handleTrackEvent()内部对每次counter.inc()都做了 try/catch,单个计数器自增失败不会影响其他计数器和事件总线。
从测试角度,PrometheusMetrics.test.ts 验证了内置指标(如查询阶段耗时直方图、MotherDuck 缓存指标)在 prom-client registry 上的真实注册与取值行为,可作为理解指标系统输出格式的参考样例。
九、典型使用场景与配置建议
结合机制特性,推荐以下实践:
- 业务转化漏斗监控:跟踪
user.created、user.logged_in、saved_chart.created、dashboard.created等事件,观察从注册到产出内容的转化率; - 查询负载的维度拆分:为
query.executed配置projectId、context标签,区分交互式查询与调度查询(API 侧本身也内置了getQueryContextLabel()把上下文归约为interactive/scheduled两类); - 标签基数控制:标签维度越少越好。
projectId、userId这类高基数标签会导致 Prometheus 序列爆炸,建议优先用项目这类中低基数维度,用户级维度谨慎启用; - 与内置指标互补:内置指标已经覆盖查询耗时、队列等待、预聚合命中、缓存命中、AI Agent 延迟等系统级观测,自定义事件指标聚焦业务事件频次,两者配合才构成完整的可观测性拼图。
十、注意事项与限制
- 双重开关:
LIGHTDASH_PROMETHEUS_ENABLED与LIGHTDASH_PROMETHEUS_EVENT_METRICS_ENABLED都必须为true,否则管理器不会初始化; - 容错而非阻断:配置文件缺失、非法只会记录日志并跳过,不会阻止 Lightdash 启动——部署时务必检查日志确认"initialized with N metrics"出现,避免"配了但没生效";
- 初始化时机:管理器必须在 Prometheus 指标服务启动之后初始化,该顺序由
App.start()自动保证,无需人工干预; - 事件名大小写敏感:
eventName必须与LightdashAnalytics.ts中类型化事件的定义完全一致; - 配置路径安全:配置文件必须位于 Lightdash 工作目录内,超出工作目录的路径会被路径穿越防护拒绝。
十一、小结
Custom Metric Manager 是 Lightdash Prometheus 体系中对"内置指标"的重要补充:它用一份 JSON 文件 + 两个环境变量,把埋点事件与 Prometheus 计数器之间的映射从"改代码发版"降维成"改配置重启",同时通过 zod 校验、路径穿越防护、幂等初始化、优雅清理和单点错误隔离,保证了生产环境下的健壮性。理解它的配置字段、标签提取兜底规则和analytics.track.<event>事件总线调用链,你就能为自建 Lightdash 实例快速定制出贴合业务的分析型监控指标。
【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考