ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Grafana 可观测性仪表盘构建实战:Pollinations 平台的陷阱清单与最佳实践

Grafana 可观测性仪表盘构建实战:Pollinations 平台的陷阱清单与最佳实践
  • AI 应用
  • 媒体生成
  • 后端
  • 前端

【免费下载链接】pollinations

Your Friendly Open-Source Gen-AI Platform

项目地址:https://gitcode.com/gh_mirrors/po/pollinations
点击查看免费下载

本文是 Pollinations 开源仓库中 operations/observability/AGENTS.md 的深度展开。它以 pollinations.ai 生产级 Observability Dashboard(Grafana OSS + Tinybird/ClickHouse)的实战经验为骨架,系统总结仪表盘 JSON 配置、SQL 查询、业务指标建模中的高频陷阱与正确做法。读完本文,你将掌握 timeseries 叠加、百分比单位、图例计算、双 Y 轴、override 属性、ClickHouse 聚合查询、基于队列的转化指标建模等一套可直接复制到生产环境的 Grafana 构建方法论。

一、背景:这套经验从哪来,用在哪儿

本文全部经验来自 Pollinations 平台的 Observability Dashboard,其完整架构记录在 operations/observability/README.md:

Local: Browser -> localhost:4000 -> Pollinations session gateway -> private Grafana at 127.0.0.1:4001 Prod: Browser -> observability.pollinations.ai -> Cloudflare Worker -> private Grafana container (Auth Proxy) -> Tinybird

生产环境的核心链路是:浏览器 → Cloudflare Worker(同源认证代理)→ 私有 Grafana 容器 → Tinybird(ClickHouse)。其中 Grafana 容器闲置 10 分钟后休眠、冷启动时从全新磁盘加载,因此所有仪表盘必须通过 git 文件方式 provisioned,而不能依赖 Grafana 内部数据库持久化。

数据源配置见 operations/observability/provisioning/datasources/datasources.yaml:类型为grafana-clickhouse-datasource,UID 固定为PAD1A0A25CD30D456,host 指向clickhouse.europe-west2.gcp.tinybird.co,密码通过环境变量$__env{TINYBIRD_READ_TOKEN}注入。该仪表盘只读生产环境数据,staging 流量位于独立的pollinations_enter_stagingworkspace,如需 staging 分析必须使用独立的读 token。

当前仓库 provisioned 的仪表盘清单见 operations/observability/provisioning/dashboards/,包括platform-usage-rebuild、core-api-rebuild、byop-apps-rebuild、community-models-rebuild、users-balances-rebuild、websites-rebuild六份 JSON。下文所有示例都能在这六份真实配置中找到对应实现。

二、面板类型与可视化:combo 图必须用 timeseries,而不是 barchart

反模式:不要用barchart类型做组合图(combo chart)。Bar chart 面板无法叠加折线——如果你需要"柱状 + 折线"的混合呈现,barchart 类型根本不支持,强行拼接只会得到错误或残缺的渲染。

推荐做法:使用timeseries类型,并通过fieldConfig.overrides按序列名覆盖custom.drawStyle。同一个 timeseries 面板中,不同序列可以分别渲染为 bars 和 line:

{ "type": "timeseries", "fieldConfig": { "overrides": [ { "matcher": { "id": "byName", "options": "my_bar_series" }, "properties": [ { "id": "custom.drawStyle", "value": "bars" }, { "id": "custom.fillOpacity", "value": 80 } ] }, { "matcher": { "id": "byName", "options": "my_line_series" }, "properties": [ { "id": "custom.drawStyle", "value": "line" }, { "id": "custom.axisPlacement", "value": "right" } ] } ] } }

这在仓库中不是理论,而是大量实际使用:在 platform-usage-rebuild.json 的 "Generation volume by workload" 面板中,默认drawStyle为"bars"、fillOpacity为85、stacking为{"group": "A", "mode": "normal"};而 "Active users and BYOP apps" 面板则把 BYOP apps 序列 override 为"custom.drawStyle": "line"、fillOpacity: 0、lineWidth: 2,并将axisPlacement设为right,实现了"柱状(活跃用户)+ 右轴折线(BYOP 应用)"的经典 combo 图(见 platform-usage-rebuild.json)。

三、百分比格式化:杜绝二次乘法的单位错乱

反模式:不要在 SQL 里既返回小数又乘 100。如果 SQL 返回0.85(十进制),就不要再在 SQL 中乘以 100,Grafana 的percentunit单位本身就会完成百分比换算。二次乘法会导致数值被放大 100 倍。

SQL 返回Unit 设置显示结果
0.85percentunit85%✅
85percentunit8500%❌
0.85percent0.85%❌

推荐做法:让 SQL 输出与单位类型严格匹配。

  • SQL 返回0–1 小数→ 使用"unit": "percentunit"
  • SQL 返回0–100 数值→ 使用"unit": "percent"
  • 同时设置边界:percentunit设"max": 1,percent设"max": 100

仓库中 "Models failing after the balance check" 表格面板即按此规范实现:SQL 中round(failed / nullIf(failed + served, 0), 3)产出 0–1 小数,override 中配套设置"unit": "percentunit"与"decimals": 1(见 platform-usage-rebuild.json)。

四、图例计算(Legend Calcs):百分比/比率别求和

反模式:对百分比做 sum。把每日百分比直接相加会产生无意义的大数字(例如4922%),既误导读者也破坏图例可读性。

推荐做法:按指标类型选用合适的 calcs。

"legend": { "calcs": ["sum", "mean"], "displayMode": "table" }
  • sum:适合累加型指标(如 Pollen 消耗总量、调用次数);
  • mean:适合百分比和比率类指标。

实际配置中,"Active users and BYOP apps" 面板对两个近似去重计数序列使用legend.calcs: ["mean", "max"]并以displayMode: "table"展示(见 platform-usage-rebuild.json);而 "Generation volume by workload" 这类累加流量面板则使用calcs: ["sum"]、displayMode: "list"、placement: "bottom"(见 platform-usage-rebuild.json)。两种场景的取舍与文档建议完全一致。

五、X 轴标签:用间隔代替逐点标注

反模式:显示每一个数据点的标签。当数据跨度达到 60+ 天时,逐日标签必然互相重叠,整条轴变成无法阅读的黑块。

推荐做法:增大标签间距。

"options": { "xTickLabelRotation": 0, "xTickLabelSpacing": 200 }

xTickLabelSpacing: 200的效果是大约每 7 天才显示一个标签(约每周一个),而不是每天一个。这是纯展示层配置,不影响数据密度与 tooltip 精度。

六、面板描述:保持单行,别用\n

反模式:在 JSON 的description字段里使用\n换行。Grafana 的 tooltip 不会渲染转义换行符,\n会原样显示为字面量,看起来非常业余。

推荐做法:描述保持简洁的单行文本。

"description": "Daily Pollen: Paid (green) vs Quest (orange)."

仓库中所有面板描述均遵守此约定,例如 platform-usage-rebuild.json 的仪表盘级 description,以及各面板如 "Daily successful generations by event workload from generation_usage_hourly. Workload is the generation_event_v2 event_type; no health or error interpretation is applied."——全部是单行、信息完整、无转义换行。

七、ClickHouse/Tinybird 查询规范

7.1 生成事件只读 V2:V1 已退役

generation_eventV1 已经退役。所有可观测性仪表盘与 Tinybird 查询必须读取generation_event_v2,永远不要恢复或添加 V1 回退。这与仓库事实一致:Tinybird 侧的全部物化管道均以generation_events_classified(基于 V2 的分类视图)为数据源,例如 generation_usage_hourly_mv.pipe 中的FROM generation_events_classified。

7.2 产品路径归因:BYOP 与 BYOM 是两个独立维度

核心业务语义(来自 AGENTS.md):

  • BYOP(Bring Your Own Pollen):开发者应用内的、以终端用户身份认证的消耗,对应is_byop;
  • BYOM(Bring Your Own Model):任何社区模型的消耗,对应is_community;
  • Direct API:既不 BYOP 也不 BYOM 的直接 API 消耗。

加和型产品路径图必须暴露四条互斥路径:Direct API、BYOM direct、BYOP with Pollinations models、BYOP with BYOM;同时把Paid 与 Quest 作为独立的资金维度(funding dimension)处理,不要混入产品路径维度。

仓库中的实现验证了这套四分类逻辑。"Paid Pollen consumption by product path" 面板在 SQL 中用multiIf把is_byop/is_community两个布尔量映射成唯一路径字符串:

multiIf( is_byop = 1 AND is_community = 1, 'BYOP · BYOM', is_byop = 1, 'BYOP · Pollinations models', is_community = 1, 'BYOM · Direct', 'Direct API' ) AS product_path

随后以sumMergeIf(gross_consumption, funding_source = 'paid')与funding_source = 'quest'分别产出 Paid 与 Quest 两个资金流(见 platform-usage-rebuild.json),并通过partitionByValues+renameByRegex转换把同一查询结果拆成两张图(Paid / Quest),复用查询、零额外 Tinybird 请求。而is_byop、is_community这两个维度本身就在物化管道中定义:api_key_client_id NOT IN ('', 'undefined') AS is_byop、model_provider_used = 'community' AS is_community(见 generation_usage_hourly_mv.pipe)。

7.3 活跃用户与应用:只统计 funded 请求

关键陷阱:generation_usage_hourly中的users状态与byop_app_daily中的apps状态会统计每一个请求,包括因 Pollen 不足被 402 拒绝的请求。因此统计活跃用户、活跃应用时必须过滤funding_source != 'unfunded':

uniqMergeIf(users, funding_source != 'unfunded') uniqIfMergeIf(apps, funding_source != 'unfunded')

如果不加这个过滤,WAU(周活跃用户)会比 KPI 应用显示的数值高出约 24%。这是文档中给出的实测偏差,务必在查询层直接规避。

仓库证据充分:在 platform-usage-rebuild.json 的 "Platform overview" 统计面板中,active_users与active_byop_apps分别用uniqMergeIf(users, funding_source != 'unfunded')和uniqIfMergeIf(apps, funding_source != 'unfunded')计算,面板描述也明确注明 "402 rejections excluded";"Active users and BYOP apps" 时序面板使用同样的写法(见 platform-usage-rebuild.json)。

7.4 不要猜测字段值

反模式:凭记忆写过滤条件。一个错误的字段值会直接返回零行,而且很难排查。过滤某列之前,先查一下线上真实存在的值。这一点在使用 ClickHouse/Tinybird 这类强类型列式库时尤其重要——它不会"宽容"地忽略未知枚举值。

7.5 用funding_source拆分资金,而不是 meter slug

generation_usage_hourly与byop_app_daily两张表都带有funding_source列,取值paid、quest、unfunded。它由各自的物化管道从selected_meter_slug映射而来(见 generation_usage_hourly_mv.pipe 与 byop_app_daily_mv.pipe):

multiIf( selected_meter_slug IN ('v1:meter:pack', 'local:pack'), 'paid', selected_meter_slug IN ('v1:meter:tier', 'local:tier'), 'quest', 'unfunded' ) AS funding_source

仪表盘 SQL 一律过滤funding_source,而不是 meter slug——meter slug 是底层实现细节,未来新增/改名会破坏查询;funding_source才是稳定的业务语义层。

7.6 不要忘记时间过滤器

反模式:查询里没有$__timeFilter()。少了它,无论仪表盘时间选择器选了什么区间,查询都会返回全量数据——结果就是图表与时间选择器"脱钩",数据量也白白翻倍。

推荐做法:每个查询都带上 Grafana 时间宏,并按环境过滤:

WHERE $__timeFilter(start_time) AND environment = 'production'

仓库中每个面板的rawSql都遵循这一模式,只是由于物化表按小时聚合并同时覆盖 staging 验证夹具,实际写法为WHERE $__timeFilter(hour) AND environment IN ('production', 'debug-prod-copy')(例如 platform-usage-rebuild.json)。debug-prod-copy是隔离的 staging 数据副本,生产仪表盘用同一套查询同时校验 staging 与生产,避免两套 SQL 分叉。

八、双 Y 轴面板:显式指定轴位

反模式:忘记设置custom.axisPlacement。不做显式配置时,所有序列默认共用左轴,双 Y 轴面板形同虚设,且不同量纲(如"Pollen 数量"与"百分比")叠加在同一条轴上会互相压扁。

推荐做法:在 override 里把第二条轴放到右侧,并配齐轴标签、单位与边界:

{ "matcher": { "id": "byName", "options": "percentage_series" }, "properties": [ { "id": "custom.axisPlacement", "value": "right" }, { "id": "custom.axisLabel", "value": "%" }, { "id": "unit", "value": "percentunit" }, { "id": "min", "value": 0 }, { "id": "max", "value": 1 } ] }

仓库实战:platform-usage 中 "Active users and BYOP apps" 面板把active_byop_apps放在右轴并配custom.axisLabel: "BYOP apps"(见 platform-usage-rebuild.json);"Request outcomes" 面板把unfunded_users折线放在右轴(axisLabel: "Users",见 platform-usage-rebuild.json)。另外值得注意:测试套件强制所有线性 timeseries 的 y 轴从零开始(日志轴和可负余额面板除外),这保证了右轴百分比/计数不会被"截断放大",见 test/dashboards.test.ts。

九、隐藏辅助列:别让中间计算结果污染图例

反模式:展示仅用于计算的中间列。比如developer_total这类只用来算百分比的列,会出现在图例和 tooltip 中,干扰读者对真实指标的判断。

推荐做法:用 override 隐藏它们。

{ "matcher": { "id": "byName", "options": "developer_total" }, "properties": [ { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": true, "viz": true } } ] }

hideFrom的三个布尔位分别控制从图例、tooltip、可视化图形中隐藏。仓库中还有一种等价思路——用**转换(transformations)**在渲染前直接裁剪字段:Paid Pollen 面板通过filterFieldsByName只保留匹配/^(time.*|paid_pollen.*)$/的字段,再用renameByRegex把paid_pollen (路径名)重命名为路径名,从而让"开发人员"看不到的中间列根本不会进入面板数据流(见 platform-usage-rebuild.json)。

十、堆叠(Stacking):百分比折线与绝对值不要混堆

反模式:把百分比折线堆叠到 Pollen 绝对值柱状图上。两者量纲完全不同——百分比上限是 1(或 100),绝对值可能上千上万,堆叠后比例线会被压缩成一条贴地的直线。

推荐做法:对叠加的覆盖线显式关闭堆叠:

{ "id": "custom.stacking", "value": { "mode": "none" } }

mode支持"normal"(同组堆叠)与"none"(不堆叠)。仓库中 "Request outcomes" 面板是教科书式组合:served/rejected_no_pollen/failed_after_balance_check三根柱状用stacking: {"group": "A", "mode": "normal"}叠加成"全部请求=三部分之和"(见 platform-usage-rebuild.json),而同面板的unfunded_users折线则 override 为"custom.stacking": {"group": "A", "mode": "none"}(见 platform-usage-rebuild.json),确保这条右轴用户曲线不被堆叠。

十一、转化率与业务逻辑指标:先建模,再写 SQL

11.1 反模式:设计结构性恒为零的指标

典型例子是"Paid-only users(纯付费用户)":在"免费 tier 先消费、付费 pack 后消费"的系统中,任何用户都不可能只用 pack 而从不触碰 tier。

业务规则(来自 AGENTS.md):Everyone gets free Pollen → Tier consumed first → Pack consumed second.结果:"Paid-only users" 恒等于 0——因为不使用 tier 就不可能用到 pack,这个指标从定义上就是死指标。

11.2 推荐做法:按真实用户旅程建模——队列式转化

用队列(cohort)式转化衡量"从首次 tier 使用到首次 pack 使用"的时间差,而不是设计一个恒零的布尔指标:

-- Cohort conversion: users who converted within 7 days SELECT countIf(first_pack IS NOT NULL AND dateDiff('day', first_tier, first_pack) <= 7) as converted_7d FROM ( SELECT user_id, minIf(start_time, selected_meter_slug = 'v1:meter:tier') as first_tier, minIf(start_time, selected_meter_slug = 'v1:meter:pack') as first_pack FROM generation_event_v2 WHERE environment = 'production' AND total_price > 0 GROUP BY user_id HAVING first_tier IS NOT NULL )

这套 SQL 是可直接运行的完整模板:minIf取每个用户各自旅程中的首次时间戳,外层dateDiff计算转化窗口,HAVING first_tier IS NOT NULL排除从未进入 tier 的用户。注意这里同时体现了本文的多个原则——V2 表、environment过滤、对 meter slug 的底层引用仅用于"首次事件"这种不可由funding_source表达的粒度。

11.3 推荐做法:用"首次活动"作为注册代理

如果"注册但从未使用产品"的用户没有意义,那么用首次事件时间作为注册时间,而不是真实的注册时间戳。这样既避免了跨数据源 join(注册数据在 D1,事件数据在 Tinybird),又把指标聚焦在活跃用户上。

仓库中确实存在这一模式的独立物化实现:user_first_activity_mv由 user_first_activity_populate.pipe 增量维护,核心就是minState(start_time) AS first_activity_time,并限定traffic_group = 'regular'。文档中的"signup proxy"建议与这条管道互为印证。

十二、仪表盘变量与时间范围约定

每个仪表盘默认展示最近 30 个完整 UTC 日,JSON 配置固定为:

"time": { "from": "now-30d/d", "to": "now-1d/d" }, "timezone": "utc", "refresh": "1h"

这样设计的三个理由(见 AGENTS.md):

  1. 没有任何一天的生成数据是不完整的——to: now-1d/d排除了进行中的今天;
  2. Grafana 的日期与 SQL 的 UTC 日期完全对齐——注册数面板的最后一根柱会在每日约 03:20 UTC 的d1_user同步完成后填充;
  3. 1 小时刷新足以跟进数据更新节奏,又不会频繁打爆 Tinybird。

实际配置验证于 platform-usage-rebuild.json:"refresh": "1h"、"time": {"from": "now-30d/d", "to": "now-1d/d"}、"timezone": "utc",且templating.list为空数组。

kiosk 模式与头部时间选择器:应用以 kiosk 模式嵌入 Grafana(隐藏其导航与时间选择器),头部选择器改为通过 iframe URL 参数下发 7 / 30 / 90 天:from=now-<n>d/d&to=now-1d/d,默认 30 天。前端实现在 frontend/src/dashboards.ts(RANGE_LABELS定义 7d/30d/90d)与 dashboardSrc(拼接?kiosk&hideLogo&from=now-${range}/d&to=now-1d/d)。对应的 URL 解析与回退逻辑(未知 range 一律回退 30d)由 test/dashboards.test.ts 覆盖。

为什么没有自定义过滤变量:自定义 filter 变量被有意移除——保持仪表盘简单、聚焦于回答战略性业务问题,而不是鼓励临时 ad-hoc 过滤。如果将来确实需要加变量,请直接编辑仪表盘 JSON 中的templating.list数组(注意:provisioned 仪表盘通过 API 是只读的,不能在 Grafana UI 里改)。templating.list为空即当前所有仪表盘的状态(见 platform-usage-rebuild.json)。

十三、工作流:从改 JSON 到上线

  1. 编辑provisioning/dashboards/下的仪表盘 JSON。Grafana 会拒绝通过 UI 保存 provisioned 仪表盘——文件才是唯一事实来源。配置侧的证据在 dashboards.yaml:type: file、disableDeletion: true、allowUiUpdates: false、updateIntervalSeconds: 30(文件改动后 30 秒内自动热加载)。
  2. 运行测试:在operations/observability目录执行npm test。测试脚本定义于 package.json(node --experimental-strip-types --test test/*.test.ts),test/dashboards.test.ts 会校验:仪表盘列表排序与 URL 回退逻辑、kiosk 模式 URL 拼接、所有线性 timeseries 的 y 轴从零开始、以及仪表盘 SQL 绝不读取 Grafana 读 token 无权访问的内部 Tinybird pipes(见 dashboards.test.ts)。
  3. 部署:通过 .github/workflows/deploy-applications.yml 从production分支触发。workflow 按变更文件自动发现受影响应用(operations/deployment/discover.cjs),并使用独立的CLOUDFLARE_API_TOKEN_OBSERVABILITY凭据执行部署;Grafana 容器启动时从磁盘加载provisioning/目录,因此部署即生效。

如果想在 UI 中快速建面板做原型,README.md 给出了标准流程:打开本地 Grafana → UI 编辑(会拒绝保存)→ Export →Export as code → Advanced → Model: Classic→ 复制 JSON 替换仓库中对应文件 →docker compose restart grafana验证。注意必须选Classic 模型:默认的 V2 Resource JSON 无法被文件 provisioning 方式加载。

十四、XY 散点图:颜色维度会偷走一条 Y 轴

反模式:用颜色维度表达连续渐变。Grafana XY 图表对任何用作颜色维度的字段都会单独创建一条 Y 轴——即使你只是想给散点染色,它也会把该字段当成第二条数值轴处理,图表立刻失真。

推荐做法:散点图用固定颜色:

{ "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "green" } } }, "options": { "dims": { "x": "tier_cost", "y": "pack_cost" } } }

如果确实需要颜色渐变表达第三维,改用表格 + 单元格着色(cellOptions/thresholds)更合适,而不是在 XY 图上硬塞颜色维度。

十五、快速参考:常用 Override 属性速查表

属性可选值用途
custom.drawStyleline,bars,points逐序列设置图表类型
custom.fillOpacity0-100面积填充透明度
custom.axisPlacementleft,right,hidden双 Y 轴控制
custom.stacking{ "mode": "normal" }或{ "mode": "none" }堆叠开关
custom.lineWidth1-10线条粗细
custom.hideFrom{ "legend": true, "tooltip": true, "viz": true }隐藏序列
unitshort,percent,percentunit,currencyUSD数值格式化
displayNamestring图例标签
color{ "fixedColor": "green", "mode": "fixed" }序列颜色

这九项几乎覆盖了生产仪表盘 90% 的 override 场景,全部可在 operations/observability/provisioning/dashboards/ 的六个 JSON 文件中找到实际用例。

十六、本地开发与调试速查

配合 README.md 的常用命令,可以把上述配置规范落到本地验证:

# 本地启动/查看日志/停止 docker compose up -d docker compose logs -f grafana docker compose down # 重置 admin 密码 docker compose exec grafana grafana cli admin reset-admin-password NEW_PASSWORD # 测试数据源健康 curl -s -u admin:$GF_ADMIN_PASSWORD 'http://localhost:3000/api/datasources/uid/PAD1A0A25CD30D456/health'

常见的三类故障与排查方向(见 README.md):

  • 公网 URL 重定向到 Myceli origin:GF_SERVER_ROOT_URL必须配置为https://observability.pollinations.ai;
  • 插件健康检查失败:检查TINYBIRD_READ_TOKEN是否正确、容器能否连通 Tinybird;
  • 仪表盘加载不出来:核对 dashboard JSON 里的 datasource UID 与 provisioning 配置是否一致(固定为PAD1A0A25CD30D456)。

结语

这份指南的价值在于:每一个"反模式"都来自 pollinations.ai 生产仪表盘的真实踩坑,每一个"推荐做法"都能在仓库的六份 dashboard JSON、Tinybird 物化管道与自动化测试中找到落点。把funding_source过滤、V2-only 事件表、队列式转化建模、kiosk 时间约定、override 属性速查表这五件事做对,你的 Grafana 仪表盘就能同时满足"数字可信"与"可读性好"两个要求。深入源码请从 operations/observability/provisioning/dashboards/、enter.pollinations.ai/observability/materializations/ 与 operations/observability/test/dashboards.test.ts 三个入口开始。

  • AI 应用
  • 媒体生成
  • 后端
  • 前端

【免费下载链接】pollinations

Your Friendly Open-Source Gen-AI Platform

项目地址:https://gitcode.com/gh_mirrors/po/pollinations
点击查看免费下载
上一篇:Fusion Pixel Font:3分钟掌握免费开源像素字体的终极使用指南
下一篇:OpenRocket火箭仿真:从零开始的完整指南,让航空航天设计变得简单有趣 🚀

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进